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
Un compte marchand DZBuild. Le plan d'abonnement de la boutique n'a aucune incidence sur l'accès à l'API — passer à un plan supérieur ne l'accorde pas, et un plan expiré ne le retire pas.
Une clé API inscrite au pilote. C'est la vraie barrière : tant que l'API est en pilote, DZBuild doit inscrire la clé. Sans inscription, chaque appel renvoie
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é.
Un endroit où le plan du marchand compte bel et bien : 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é
Il n'existe aucune page « Developer → API Keys » dans le tableau de bord marchand. Pendant le pilote :
Demandez au support DZBuild d'émettre la première clé de la boutique et de l'inscrire au pilote.
Créez vous-même les clés suivantes à partir de celle-ci :
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 clé plateforme porte toujours l'ensemble par défaut complet (products:read, products:write, orders:read, orders:write, store:read, store:write, customers:read, landing_pages:read, landing_pages:write, webhooks:read, webhooks:write, usage:read) : 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={`https://cdn.dzbuild.app/${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 un chemin de stockage (généralementuploads/products/<store_id>/<file>), donc l'URL CDN est simplementhttps://cdn.dzbuild.app/+ ce chemin — n'insérez pas un segment/{store_id}/products/de votre cru. Les produits plus anciens peuvent porter un simple nom de fichier ; ceux-là se résolvent surhttps://cdn.dzbuild.app/uploads/products/<store_id>/<filename>.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 — la liste des produits est mise en cache pendant 30 s, donc les lectures répétées à grande échelle sont peu coûteuses.
É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: 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.
Attention au nom du champ — images[].url est un chemin de stockage, pas une URL. Préfixez-le par https://cdn.dzbuild.app/ exactement comme primary_image à l'étape 1 avant de le mettre 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. Le check définitif se fait côté serveur au confirm-time.
É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. Hardcodez les 58 wilayas — c'est une liste stable.
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. C'est l'un des GET mis en cache (30 s).
Ce qu'il ne porte pas — et c'est là le vrai point v1.1 — ce sont les tarifs de livraison par wilaya et la liste des stop desks du marchand. Donc pour le coût de livraison aujourd'hui :
Hardcodez les tarifs par wilaya + type de livraison dans la vitrine (le plus simple), ou
Gardez-les dans votre propre configuration, synchronisée avec le marchand hors API.
Passez le shipping calculé à l'API de commandes en shipping_cost. Le serveur ne recalcule pas le shipping pour vous actuellement ; ce que vous passez fait partie du total de la commande.
É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,
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
})),
shipping_cost: cart.shipping_cost,
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 (aucun endpoint de création v1 ne renvoie 201), 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 |
| 1 à 100 caractères |
|
|
|
|
| Doivent ê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. Notez que les livraisons API v1 ne sont pas signées avec ce secret : vous ne pouvez donc pas encore les vérifier. Gardez l'URL du webhook indevinable et relisez la commande avec GET /v1/orders/{id} avant d'agir. Voir Vérifier les signatures pour les détails.
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} retournera (en v1.1) name_ar, name_fr, description_ar, description_fr à côté des champs canoniques. En attendant, seul le canonique est exposé — choisissez côté client ce que veut votre client.
Sélecteur stop-desk
Pour delivery.type = "desk" :
// 1. Lisez les stop desks du marchand pour la wilaya choisie
// (prévu v1.1 : GET /v1/store/desks?wilaya=16)
// En attendant, requêtez directement le transporteur (Yalidine, ZR, etc.)// 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 un webhook SlickPay → appelez
PATCH /v1/orders/{id}pour passerpayment_statusàpaid(prévu v1.1 ; en attendant le paiement est mis à jour par l'intégration SlickPay existante de la boutique).
Retours & remboursements
Gérés actuellement dans le dashboard. Le support API pour POST /v1/orders/{id}/refund est sur la roadmap.
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 toutes les écritures. Sans lui vous obtenez un
400avec 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 clé API et le plafond découle du plan actuel de la boutique — les montées et descentes de plan s'appliquent immédiatement, sans changer de clé. Rien n'est illimité à la minute. 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. (Les scopes sont identiques sur toute clé plateforme — 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 mauvais env var | Créez une nouvelle clé avec |
| La clé existe mais DZBuild ne l'a pas inscrite au pilote | Contactez le support |
| 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 | ETA |
Tarifs de livraison par wilaya + liste des stop desks sur | v1.1 |
| v1.1 |
| v1.1 |
Upload image produit via presigned URL | v1.1 |
Champs multilingues sur réponses produits | v1.1 |
Besoin d'une de ces features plus tôt ? Contactez le support.