DZBuild livre plusieurs thèmes de vitrine prêts à l'emploi et un customizer no-code. Mais si vous voulez un contrôle total — votre propre front-end React/Vue/Next.js/Flutter, votre design pixel-perfect, votre routing — l'API est faite pour vous.
Ce guide construit une vitrine headless de bout en bout :
Rendu du catalogue (produits, catégories, variantes)
Construction du panier (côté client ou serveur, à votre choix)
Soumission de la commande via l'API
Réception des webhooks au changement de statut
À la fin, votre vitrine custom sera 100% interopérable avec le tableau de bord marchand DZBuild — les commandes apparaissent, le stock décrémente, la livraison s'intègre avec les transporteurs du marchand, sans compromis.
Architecture
┌──────────────────────┐ ┌──────────────────────────┐
│ Vitrine custom │ HTTPS │ api.dzbuild.app/v1/* │
│ (React, Vue, Flutter)│ ──────► │ Authorization: Bearer … │
│ │ │ │
│ - Lit les produits │ ◄────── │ Réponses JSON │
│ - Affiche le panier │ │ │
│ - Soumet les commandes│ └──────────────────────────┘
└──────────────────────┘ │
▼
Tableau de bord marchand DZBuild
- Confirme les commandes
- Gère le stock
- S'intègre aux transporteurs
Vous possédez l'UI. DZBuild possède la donnée et l'opérationnel. Le marchand se connecte à dzbuild.com/dashboard pour gérer les commandes confirmées dans votre UI custom.
Prérequis
Une boutique DZBuild sur un plan Enterprise actif. L'API est une fonctionnalité Enterprise : une clé dont la boutique est sur un autre plan, ou dont l'abonnement Enterprise a expiré, reçoit
403 forbidden"API access requires an active Enterprise plan"à chaque appel, et fonctionne de nouveau dès que la boutique revient sur un plan Enterprise actif.Une clé API générée par le propriétaire de la boutique dans Paramètres → API du tableau de bord. Les clés créées là sont déjà inscrites au pilote. Une clé non inscrite reçoit
403 forbidden"API is in pilot mode; key not enrolled".Un back-end (ou serverless / edge worker) qui détient la clé API. Ne livrez jamais le secret au navigateur — voir Sécurité.
Le plan fixe aussi un plafond mensuel de commandes : une boutique en plan Free cesse d'accepter des commandes au-delà de 30 par mois calendaire, et POST /v1/orders renvoie alors 400 bad_request "Monthly order limit reached for this store plan". Pro, Unlimited et Enterprise n'ont aucun plafond de commandes. Gérez cette erreur dans votre checkout.
Étape 0 — obtenir une clé
Le propriétaire de la boutique génère les clés depuis le tableau de bord marchand, dans Paramètres → API (/dashboard/api), sur un plan Enterprise actif. Une boutique détient au plus 3 clés actives qui lui sont propres, et chaque secret n'est affiché qu'une fois :
Générez la première clé dans Paramètres → API. Les clés créées là sont déjà inscrites au pilote.
Créez vous-même les clés suivantes à partir de celle-ci, ou générez-les sur la même page :
curl -X POST 'https://api.dzbuild.app/v1/keys' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"type":"platform","name":"Custom storefront — production"}'
# HTTP 200 → { "data": { "key_id", "bearer_token", "signing_secret", "note" } }
La nouvelle clé copie le tier de rate limit et le flag pilote de la clé appelante. Les scopes ne sont pas sélectionnables : une nouvelle clé plateforme reçoit l'ensemble par défaut (store:read, store:write, products:read, products:write, orders:read, orders:write, customers:read, landing_pages:read, landing_pages:write, promos:read, promos:write, pixels:read, pixels:write, shipping:read, shipping:write, webhooks:read, webhooks:write, usage:read, analytics:read, whatsapp:read, whatsapp:send), limité aux scopes que détient la clé appelante ; isolez donc vos environnements avec des clés séparées, pas avec des permissions plus étroites.
Copiez le bearer_token — affiché une seule fois à la création. Perdu ? Révoquez et créez-en un.
Test :
curl https://api.dzbuild.app/v1/whoami \
-H "Authorization: Bearer $DZ_KEY"
# attendu : { "data": { "key_id": "...", "store_id": 13, "type": "platform", "scopes": [...] } }
Étape 1 — rendre le catalogue
// pages/index.js (Next.js)
export async function getServerSideProps() {
const res = await fetch('https://api.dzbuild.app/v1/products?limit=50&status=active', {
headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },
});
const { data } = await res.json();
return { props: { products: data.items } };
}export default function Home({ products }) {
return (
<ul>
{products.map(p => (
<li key={p.id}>
{p.primary_image && (
<img src={p.primary_image} alt={p.name} />
)}
<h2>{p.name}</h2>
<p>{p.price} DZD</p>
<a href={`/product/${p.slug}`}>Voir</a>
</li>
))}
</ul>
);
}
Deux choses à propos des images :
primary_imageest une URL CDN complète (par exemplehttps://cdn.dzbuild.app/uploads/products/<store_id>/<file>). Mettez-la telle quelle dans la baliseimg, sans ajouter de préfixe.L'élément de liste n'inclut pas
store_id. Lisez-le une fois depuisGET /v1/storeouGET /v1/whoamiet gardez-le en configuration.primary_imagevautnullquand un produit n'a pas d'image. Protégez-vous, comme ci-dessus.
Cachez la réponse de votre côté : la liste des produits est servie à neuf à chaque appel, donc les lectures répétées à grande échelle coûtent un aller-retour chacune.
Étape 2 — page produit avec variantes
const res = await fetch(`https://api.dzbuild.app/v1/products/${id}`, {
headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },
});
const { data: product } = await res.json();
La réponse inclut variants[] — chaque entrée est un groupe de variantes avec ses options :
"variants": [
{ "id": 11, "name": "Color", "type": "color",
"options": [
{ "id": 14, "value": "Red", "color_code": "#ff0000", "image_id": 28, "stock": 12 },
{ "id": 15, "value": "Blue", "color_code": "#0000ff", "image_id": 29, "stock": 5 }
]
},
{ "id": 12, "name": "Size", "type": "text",
"options": [
{ "id": 16, "value": "S", "stock": 10 },
{ "id": 17, "value": "M", "stock": 10 },
{ "id": 18, "value": "L", "stock": 5 }
]
}
]
Affichez un sélecteur par groupe. Pour type: color, des swatches avec color_code ; type: text, des labels ; type: dropdown, une liste déroulante ; type: image_text, des thumbnails : options[].image_id est un id numérique qui correspond au images[].id d'une entrée de la même réponse produit. Un groupe type: selectable est facultatif et permet au client de choisir plusieurs options. Chaque option porte aussi price_adjustment (le montant ajouté au prix du produit, utilisé à l'étape 3) et is_active ; masquez les options dont is_active vaut false.
images[].url est une URL CDN complète, comme primary_image à l'étape 1 : mettez-la telle quelle dans une balise img.
Gestion stock épuisé : options[].stock vaut null si le marchand ne suit pas le stock par variante. S'il est ≤ 0, grisez l'option. L'API ne refuse pas une commande pour une option en rupture de stock : vérifiez donc le stock dans votre UI avant le checkout. Le stock par combinaison est dans combinations[] de la même réponse.
Étape 3 — construire un panier
Le panier est côté client (React state, Vuex, Pinia, localStorage…). Pas besoin d'appel API pour ajouter au panier. Chaque ligne :
{
product_id: 26,
product_name: "T-shirt",
base_price: 1500,
quantity: 1,
selected_variants: [
{ group_name: "Color", option_name: "Red", color_code: "#ff0000", price_adjustment: 0 },
{ group_name: "Size", option_name: "L", color_code: null, price_adjustment: 200 }
]
}
Calculez le total ligne côté client : (base_price + sum(price_adjustment)) × quantity. Affichez le total panier au client, mais traitez-le comme de l'affichage seulement :
Tout prix que vous envoyez dans l'item est ignoré (le champ de l'API est
price). Le serveur utilise toujours le prix catalogue duproduct_id.price_adjustmentest re-résolu depuis le catalogue via la paire(group_name, option_name). En cas d'écart, la valeur catalogue l'emporte. Votre valeur n'est utilisée que lorsque la paire ne se résout pas du tout — c'est exactement ce qui arrive après qu'un marchand a renommé une option de variante, alors gardez vos chaînesgroup_name/option_namesynchronisées avec le catalogue.group_nameetoption_namesont silencieusement tronqués à 100 caractères, etcolor_codeà 7.
Étape 4 — collecte des infos client + calcul livraison
Formulaire checkout standard : nom, téléphone, wilaya, commune, adresse. Lisez une fois la liste des wilayas avec GET /v1/wilayas (noms en arabe, français et anglais) et les communes d'une wilaya avec GET /v1/wilayas/{id}/communes. La liste compte 58 wilayas, ou 69 sur une boutique en mode 69 wilayas, dont les commandes acceptent alors des ids de wilaya jusqu'à 69.
GET /v1/store est bien en service et mérite un appel au démarrage : il renvoie le nom de la boutique, son slug, sa langue, sa description, son logo, sa favicon, sa bannière, les couleurs et la police du thème, le sous-domaine, le domaine personnalisé (et s'il est vérifié), public_url, hide_branding et created_at. Il est servi à neuf à chaque appel.
La livraison n'est pas dans GET /v1/store ; elle a ses propres endpoints : GET /v1/shipping/rates renvoie le tarif domicile et stop desk du marchand par wilaya, GET /v1/shipping/settings les règles de livraison gratuite, et GET /v1/shipping/coverage?wilaya_id=16 les stop desks du transporteur de la boutique pour cette wilaya (voir le sélecteur stop-desk plus bas). Utilisez-les pour montrer au client une estimation avant le checkout.
N'envoyez pas de prix de livraison : POST /v1/orders le calcule à partir des tarifs du marchand pour la wilaya et le type de livraison, applique la livraison gratuite et la surcharge de poids, et ignore tout shipping_cost dans le body. Si le type de livraison choisi est désactivé pour cette wilaya et l'autre activé, la commande bascule sur celui que le marchand propose. Les commandes pickup et digital ne portent aucun frais de livraison. Affichez au client amounts.shipping_cost et amounts.total de la réponse.
Étape 5 — soumettre la commande
// Sur votre back-end (Next.js API route, Edge function, server…)
async function placeOrder(req, res) {
const cart = req.body; const order = {
customer: {
name: cart.name,
phone: cart.phone,
email: cart.email || null,
wilaya_id: cart.wilaya_id,
commune: cart.commune,
address: cart.address || ''
},
delivery: {
type: cart.delivery_type, // "home" | "desk" | "pickup" | "digital"
desk_id: cart.desk_id || null,
desk_name: cart.desk_name || null
},
items: cart.items.map(line => ({
product_id: line.product_id,
quantity: line.quantity,
variants: line.selected_variants
})),
// pas de shipping_cost : le serveur calcule la livraison à partir des tarifs du marchand
discount: 0,
payment_method: 'cod',
notes: cart.notes || null
}; const idempKey = req.headers['x-checkout-id'] || crypto.randomUUID(); const apiRes = await fetch('https://api.dzbuild.app/v1/orders', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DZ_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempKey
},
body: JSON.stringify(order)
}); if (!apiRes.ok) {
const err = await apiRes.json();
return res.status(apiRes.status).json(err);
}
const { data: createdOrder } = await apiRes.json(); // HTTP 200, pas 201
return res.json({
order_number: createdOrder.order_number,
total: createdOrder.amounts.total
});
}
POST /v1/orders répond HTTP 200 en cas de succès, et le body est le détail canonique complet de la commande, la même forme que renvoie GET /v1/orders/{id}. Branchez sur apiRes.ok, jamais sur status === 201.
Le client voit son order_number sur la page de succès. Le dashboard marchand affiche maintenant la nouvelle commande dans la liste pending, prête à confirmer.
Limites de validation
Chaque règle ci-dessous lève un 400 bad_request avec le message en clair : exposez-les dans votre formulaire de checkout plutôt que de les découvrir en production.
Champ | Règle |
| 1 à 50 lignes, tableau non vide |
| Doit appartenir à la boutique de la clé |
| 1 à 9999 |
| 1 à 255 caractères |
| Doit correspondre à |
| 1 à 58, ou 1 à 69 sur une boutique en mode 69 wilayas |
| 1 à 100 caractères |
|
|
|
|
| Doit être |
| Tronqué à 1000 caractères (pas une erreur) |
Plus le plafond de plan : sur une boutique en plan Free, la 31e commande d'un mois calendaire renvoie 400 bad_request "Monthly order limit reached for this store plan".
Étape 6 — recevoir les webhooks (optionnel mais puissant)
Enregistrez un webhook pour réagir aux events de commande :
curl -X POST 'https://api.dzbuild.app/v1/webhooks' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://yourstorefront.com/api/dz-webhook",
"events": ["order.created", "order.confirmed", "order.shipped", "order.delivered"]
}'
Vous recevrez un secret dans la réponse (HTTP 200) : stockez-le. Chaque livraison est signée avec lui : vérifiez X-DZ-Signature avant d'agir sur une livraison. Voir Vérifier les signatures pour la recette et le code.
Deux limites à connaître avant de construire là-dessus : en API v1, order.created se déclenche uniquement pour les commandes que votre vitrine crée via l'API (pas pour celles passées sur la vitrine DZBuild du marchand), et les événements de statut ne se déclenchent que pour les changements de statut faits via l'API. Voir le Catalogue d'événements.
Utilisez les webhooks pour :
Envoyer un SMS au client à la confirmation.
Mettre à jour CRM / Google Sheet / analytics.
Invalider une page « merci » après confirmation marchand.
Déclencher la livraison de produit téléchargeable après
order.delivered.
Patterns courants
Vitrine multilingue
Stockez vos traductions en front. L'API renvoie les noms et descriptions tels que saisis par le marchand. Si le marchand utilise l'add-on Multi-language, GET /v1/products/{id} ne renvoie toujours que le nom et la description canoniques ; les traductions de l'add-on ne font pas partie de la réponse de l'API, donc choisissez côté client ce que veut votre client.
Sélecteur stop-desk
Pour delivery.type = "desk" :
// 1. Lisez les stop desks du transporteur de la boutique pour la wilaya choisie :
// GET /v1/shipping/coverage?wilaya_id=16 (scope shipping:read)
// desks[] contient desk_id, name, address, phone, commune_id ;
// 422 no_courier_linked signifie qu'aucun transporteur n'est lié à la boutique// 2. Affichez la liste, le client choisit :
selectedDesk = { id: 7842, name: "Yalidine Bab Ezzouar" };// 3. Passez à la commande :
order.delivery = {
type: "desk",
desk_id: selectedDesk.id,
desk_name: selectedDesk.name
};
Paiement en ligne (SlickPay / Edahabia)
Pour payment_method = "digital_payment" :
Soumettez la commande via l'API normalement ; créée en
pendingavecpayment_status = pending.Redirigez le client vers SlickPay / Edahabia checkout.
Au succès, votre back-end reçoit la confirmation du prestataire de paiement. Vous pouvez alors faire avancer la commande avec
PATCH /v1/orders/{id}et{"status": "confirmed"}.payment_statusne peut pas être modifié via l'API, et l'intégration SlickPay de la boutique ne marque payées que les commandes réglées sur le checkout de la boutique elle-même : elle ne marque donc pas payée une commande créée via l'API.
Retours & remboursements
Gérés actuellement dans le dashboard. Via l'API, vous pouvez marquer une commande comme retournée avec PATCH /v1/orders/{id} et {"status": "returned"}, depuis shipped ou delivered. Aucun endpoint de remboursement n'est disponible.
Sécurité
Ne mettez jamais la clé API dans du code exposé au navigateur. Les clés appartiennent à votre serveur / serverless / edge. Le navigateur appelle votre endpoint, votre endpoint appelle DZBuild.
HTTPS only entre vitrine et
api.dzbuild.app. Le HTTP simple est rejeté.Idempotency-Key requis sur chaque
POST,PATCHetDELETE(facultatif surPUT). Sans lui vous obtenez un400avec le codebad_requestet le message"Idempotency-Key header is required for write requests": le code estbad_request, pasidempotency_key_required. La valeur doit faire ≤ 64 caractères pris dans[A-Za-z0-9_-:.];crypto.randomUUID()passe, mais pas le base64 ni la plupart des encodages de hash (+,/,=sont rejetés).Les limites de taux sont comptées par boutique : toutes les clés de la boutique partagent un même budget de 600 requêtes par minute, donc des clés supplémentaires ne l'augmentent pas. Seul le plan Enterprise a accès à l'API, et une boutique qui le quitte reçoit
403jusqu'à son retour sur un plan Enterprise actif. Notez que ces limites ne s'appliquent qu'aux appels API ; la vitrine que vous construisez sert vos acheteurs sans les consommer. Voir Limites de taux pour les plafonds actuels.Une clé par environnement. Pas de partage dev/prod. Créez une clé staging séparée ; révoquez-la après staging. (Vous ne pouvez pas choisir les scopes, et une clé garde ceux reçus à sa création : la séparation vient de la clé elle-même.)
Dépannage
Problème | Cause probable | Fix |
| En-tête | Ajoutez |
| Le token n'a pas de point — souvent un copier-coller tronqué qui n'a gardé que le key id | Utilisez le bearer token complet de 73 caractères |
| Typo, clé révoquée ou expirée, ou mauvais env var | Générez une nouvelle clé dans Paramètres → API |
| La clé existe mais DZBuild ne l'a pas inscrite au pilote | Contactez le support |
| La boutique n'est pas en Enterprise, ou son abonnement Enterprise a expiré | Le marchand renouvelle ou passe en Enterprise ; la même clé fonctionne de nouveau |
| ID cross-store (clé store A, produit store B) | Bonne clé |
| Panier vide | Ne soumettez pas vide |
| Boutique en plan Free au plafond de 30 commandes/mois | Le marchand passe en Pro ou au-dessus |
| Polling trop agressif | Passez aux webhooks ; temporisez avec l'en-tête |
Exemples de code
Tout ce dont vous avez besoin est sur cette page — liste du catalogue, rendu des variantes, forme du panier, soumission de commande, enregistrement de webhook. Copiez depuis les sections ci-dessus ; il n'existe pas de dépôts de démarrage séparés à cloner.
Mise en production
Testez à fond avec une clé
pilotsur un store de test.Créez une clé séparée prod (mêmes scopes, autre nom).
Déployez avec la clé prod en env vars.
Passez une vraie commande test ; confirmez sur dashboard.
Testez un remboursement si vous en offrez.
Enregistrez vos webhooks pointant sur l'URL prod.
Surveillez
GET /v1/usagechaque jour la première semaine.
Roadmap
Feature | Statut |
Tarifs de livraison par wilaya et stop desks | En service : |
Stock par combinaison | En service : |
| Non disponible |
Upload d'image produit | En service : |
Champs multilingues sur réponses produits | Non disponible |
Besoin d'une de ces features plus tôt ? Contactez le support.