Commandes
Les commandes sont le cœur de la plateforme. Chaque commande :
- Appartient à exactement une boutique (scope par votre clé API — vous ne pouvez jamais toucher accidentellement aux données d'un autre marchand).
- A un cycle de vie en 7 états.
- Est liée à un client (dédupliqué par téléphone dans la boutique).
- A 1 ou plus articles, chacun pouvant porter variantes et add-ons.
- A un statut de paiement (
pending,paid,refunded) indépendant du statut de fulfillment.
Cycle de vie
pending → confirmed → processing → shipped → delivered
↘ returned
↘ cancelled
| État | Signification | Stock |
|---|---|---|
pending | Créée via vitrine ou API. En attente de confirmation marchand. | Non engagé |
confirmed | Marchand a confirmé (appel, revue panier). | Engagé (décrémenté) |
processing | En préparation pour le transporteur. | Engagé |
shipped | Remis au transporteur. | Engagé |
delivered | Client a signé. | Engagé |
cancelled | Annulée — stock restitué s'il avait été engagé. | Restitué |
returned | Retournée par le client — stock restitué. | Restitué |
cancelled et returned sont terminaux — pas de transition de sortie.
POST /v1/orders — créer une commande
Créer une commande pour la boutique appelante. Utilisé par les thèmes personnalisés, les apps mobiles / natives, les revendeurs, et toute vitrine headless qui veut contourner l'endpoint legacy /api/orders à base de cookies.
Auth : clé plateforme avec orders:write. Nécessite Idempotency-Key.
La commande est créée en pending. Le stock n'est PAS engagé à la création — la première transition pending → confirmed est ce qui décrémente le stock (même flux que le tableau de bord). Intentionnel : ça laisse à votre équipe le temps de filtrer les fakes / doublons / non-réponses avant de toucher à l'inventaire.
Corps
{
"customer": {
"name": "Sarra Benali",
"phone": "0555000111",
"wilaya_id": 16,
"commune": "Bab Ezzouar",
"address": "12 Rue X, Apt 3"
},
"delivery": {
"type": "home",
"desk_id": null,
"desk_name": null
},
"items": [
{
"product_id": 26,
"quantity": 2,
"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 }
]
}
],
"shipping_cost": 600,
"discount": 0,
"payment_fee": 0,
"payment_method": "cod",
"notes": "Please call before delivery"
}
Référence des champs
customer (objet, requis)
| Champ | Type | Requis | Notes |
|---|---|---|---|
name | string (1–255) | ✅ | Nom complet |
phone | string | ✅ | ^\+?[0-9 ]{6,20}$ — algérien ou international |
email | string | null | Si présent, sera enregistré sur la fiche client | |
wilaya_id | int 1–58 | ✅ | Code wilaya algérienne |
commune | string (1–100) | ✅ | Texte libre, ex. "Bab Ezzouar" |
address | string | Rue + appt ; peut être vide pour stop-desk |
Les clients sont dédupliqués par (store_id, phone). Si un client avec ce téléphone existe déjà dans votre boutique, sa fiche est mise à jour (nom, wilaya, commune, adresse, email) et réutilisée. Sinon, une nouvelle fiche est créée.
delivery (objet, optionnel)
| Champ | Type | Défaut | Notes |
|---|---|---|---|
type | home | desk | digital | home | digital pour produits téléchargeables uniquement |
desk_id | int | null | null | Requis si type = desk et que vous voulez un bureau précis |
desk_name | string | null | null | Libellé humain optionnel |
items (tableau, requis, 1–50 lignes)
| Champ | Type | Requis | Notes |
|---|---|---|---|
product_id | int | ✅ | Doit appartenir à votre boutique (les IDs cross-store sont rejetés 400) |
quantity | int 1–9999 | ✅ | |
variants | tableau d'objets variante | Voir ci-dessous |
Important — tarification autoritative côté serveur. Vous ne précisez pas le prix de la ligne. On tire toujours products.price côté serveur et on additionne variants[].price_adjustment. Si vous envoyez un champ price, il est ignoré. Cela empêche un client compromis de pousser des commandes à des prix bidons bas.
items[].variants (tableau, optionnel)
Chaque objet variante décrit un choix d'option pour un groupe de variantes du produit :
| Champ | Type | Notes |
|---|---|---|
group_name | string | ex. "Color", "Size", "Material" |
option_name | string | ex. "Red", "L", "Cotton" |
color_code | string | null | Couleur hex (variantes de type couleur uniquement) |
price_adjustment | number | Ajouté au prix de base ; peut être négatif pour remise |
Envoyez un objet variante par groupe choisi sur la ligne. Donc "T-shirt rouge taille L" devient 2 entrées (une pour Color/Red, une pour Size/L). DZBuild les rend sur la page commande du dashboard exactement comme si le client avait cliqué sur la vitrine.
Pour les produits utilisant des variantes par pièce (ex. offre "achetez 3 t-shirts, choisissez une couleur par pièce"), utilisez quantity = 1 par ligne et créez une ligne par pièce — c'est le mapping le plus propre.
Champs monétaires de niveau supérieur
| Champ | Type | Défaut | Notes |
|---|---|---|---|
shipping_cost | number ≥ 0 | 0 | Vous le calculez côté client à partir de la wilaya + type de livraison |
discount | number ≥ 0 | 0 | Montant code promo, remise manuelle, etc. |
payment_fee | number ≥ 0 | 0 | Frais de processeur de paiement en ligne |
payment_method | cod | free_digital | digital_payment | auto | Défaut cod pour physique, free_digital pour digital |
notes | string ≤ 1000 | null | Notes client, visibles sur la page commande du dashboard |
Le total est calculé côté serveur : subtotal + shipping_cost - discount + payment_fee (clamp à 0). Le subtotal lui-même = sum(items[].quantity × (price + Σ variants.price_adjustment)).
Requête
curl -X POST 'https://api.dzbuild.app/v1/orders' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customer": {
"name": "Sarra Benali",
"phone": "0555000111",
"wilaya_id": 16,
"commune": "Bab Ezzouar",
"address": "12 Rue X"
},
"items": [
{ "product_id": 26, "quantity": 1,
"variants": [
{ "group_name": "Duration", "option_name": "30 days", "price_adjustment": 0 }
]
}
],
"shipping_cost": 600,
"payment_method": "cod"
}'
Réponse 201
Renvoie la même forme que GET /v1/orders/{id} — entièrement peuplée avec totaux calculés, bloc client normalisé, et lignes que vous venez de créer (avec leurs variantes). id et order_number sont les nouvelles valeurs.
Erreurs
| Code | Cause |
|---|---|
bad_request "customer object is required" | customer manquant |
bad_request "customer.phone is required (digits, optional leading +)" | Téléphone non conforme regex |
bad_request "customer.wilaya_id must be 1-58" | Wilaya invalide |
bad_request "items must be a non-empty array" | Panier vide |
bad_request "items: max 50 lines per order" | Trop de lignes (splitter en plusieurs commandes) |
bad_request "items[N].product_id is required" | product_id manquant |
bad_request "Product N does not belong to this store" | ID cross-boutique |
bad_request "items[N].quantity must be 1-9999" | Quantité invalide |
bad_request "delivery.type must be home, desk, or digital" | Type de livraison invalide |
bad_request "payment_method must be cod, free_digital, or digital_payment" | Méthode de paiement invalide |
bad_request "Monthly order limit reached for this store plan" | Limite de commandes mensuelles atteinte — passez à un plan supérieur |
Idempotence
Chaque POST doit porter un en-tête Idempotency-Key. Si vous rejouez la même requête (même clé, même boutique) sous 24 h, on renvoie la même réponse — la commande est créée exactement une fois. Voir Idempotence.
# rejouable indéfiniment avec la même clé
KEY="$(uuidgen)"
for i in 1 2 3; do
curl -X POST 'https://api.dzbuild.app/v1/orders' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @order.json
done
# une seule ligne dans la table orders
Webhook déclenché
Créer une commande déclenche order.created à tous les webhooks abonnés à cet événement. Voir Événements webhook.
GET /v1/orders
Liste les commandes. Pagination par curseur.
Auth : clé plateforme avec orders:read.
Paramètres de requête
| Param | Type | Notes |
|---|---|---|
limit | int 1–200 | Défaut 50 |
cursor | string | Opaque |
status | un des 7 états | Filtre |
since | ISO 8601 | created_at >= since |
customer_phone | string | Match exact |
Requête
curl 'https://api.dzbuild.app/v1/orders?status=pending&limit=20' \
-H "Authorization: Bearer $DZ_KEY"
Réponse 200
{
"data": {
"items": [
{
"id": 6894,
"order_number": "ORD-13-20260317-AD3C",
"status": "pending",
"payment_status": "pending",
"payment_method": "cod",
"total": 1000,
"customer_name": "John Doe",
"customer_phone": "0555000000",
"wilaya_id": 16,
"commune": "Bab Ezzouar",
"delivery_type": "home",
"created_at": "2026-03-17 15:18:13"
}
],
"next_cursor": null,
"has_more": false
}
}
La vue liste est volontairement compacte (pas d'items, pas de variantes). Appelez GET /v1/orders/{id} pour le détail complet.
GET /v1/orders/{id}
Détail complet avec lignes, variantes et bloc client.
Auth : clé plateforme avec orders:read.
Réponse 200
{
"data": {
"id": 6894,
"order_number": "ORD-13-20260317-AD3C",
"status": "pending",
"payment_status": "pending",
"payment_method": "cod",
"customer": {
"id": 5578,
"name": "John Doe",
"phone": "0555000000",
"email": null,
"wilaya_id": 16,
"commune": "Bab Ezzouar",
"address": "12 Rue X"
},
"delivery": { "type": "home", "desk_id": null, "desk_name": null },
"amounts": {
"subtotal": 1000,
"shipping_cost": 0,
"discount": 0,
"payment_fee": 0,
"total": 1000
},
"items": [
{
"id": 8421,
"product_id": 26,
"price": 1000,
"quantity": 1,
"variants": [
{ "group_name": "Duration", "option_name": "30 days", "color_code": null, "price_adjustment": "0.00" }
]
}
],
"created_at": "2026-03-17 15:18:13",
"updated_at": "2026-03-17 15:18:13"
}
}
Variantes dans la réponse
Le tableau variants de chaque article est la source de vérité de ce que le client a choisi. Chaque entrée a group_name, option_name, color_code (variantes couleur), et price_adjustment (le supplément par pièce). Pour les offres multi-pièces, vous verrez plusieurs lignes variantes sur le même article avec des regroupements effectifs différents — voir notes par pièce ci-dessous.
PATCH /v1/orders/{id} — changer le statut
Auth : clé plateforme avec orders:write. Nécessite Idempotency-Key.
Le corps doit être {"status": "<un des 7>"}. On valide que la transition est autorisée ; sinon on renvoie 400 avec les états suivants légaux.
Requête
curl -X PATCH 'https://api.dzbuild.app/v1/orders/6894' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: confirm-6894-$(date +%s)" \
-d '{"status": "confirmed"}'
Renvoie 200 et le détail commande à jour. La transition est wrappée dans une transaction SELECT ... FOR UPDATE pour que les concurrents collisionnent proprement avec un concurrent control optimiste.
Effets sur le stock
Quand la transition franchit la frontière du stock engagé :
- Non-engagé → engagé (ex.
pending → confirmed) — le stock est décrémenté, les sales count incrémentés, le cache produits invalidé. - Engagé → cancelled / returned — le stock est restitué, cache produits invalidé.
- Les autres transitions ne touchent pas au stock.
Le travail stock utilise exactement OrderController::handleStockChange legacy pour que le comportement matche le dashboard 1:1.
Erreurs
| Code | Cause |
|---|---|
bad_request "status must be one of: pending, confirmed, …" | String invalide |
bad_request "Transition X → Y not allowed. From 'X' you can only go to: …" | Non autorisé par la machine à états |
bad_request "order status changed concurrently; retry" | Une autre requête a changé le statut pendant votre transition. Sûr de retry avec la même clé d'idempotence. |
not_found | ID commande inconnu ou appartient à une autre boutique |
POST /v1/orders/{id}/cancel
Endpoint de commodité. Équivaut à PATCH /v1/orders/{id} avec {"status":"cancelled"} mais avec une règle légèrement plus permissive : tout état non-terminal peut transitionner vers cancelled.
Auth : clé plateforme avec orders:write. Nécessite Idempotency-Key.
curl -X POST 'https://api.dzbuild.app/v1/orders/6894/cancel' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Idempotency-Key: cancel-6894-$(date +%s)"
Variantes — référence complète
Les variantes font qu'un seul produit couvre plusieurs options (couleur, taille, matière, capacité, …). Sur la vitrine, les clients cliquent sur des cartes de variantes pour choisir ; via l'API, vous envoyez les options choisies dans la commande.
Modèle de variantes
Un produit a 0 ou + groupes de variantes. Un groupe a 0 ou + options. Chaque option peut avoir :
- Une
value(nom affiché) - Un
color_code(hex, variantes couleur uniquement) - Un
price_adjustment(ajouté au prix de base) - Un
image_id(image produit liée comme swatch)
Lisez GET /v1/products/{id} pour voir tous les groupes et options d'un produit :
{
"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 }
]
}
]
}
Un produit avec 2 couleurs × 3 tailles a 6 combinaisons.
Comment envoyer les variantes sur POST /v1/orders
Convertissez la sélection client en une entrée variante par groupe choisi. Pour "T-shirt rouge taille L" :
"variants": [
{ "group_name": "Color", "option_name": "Red", "color_code": "#ff0000", "price_adjustment": 0 },
{ "group_name": "Size", "option_name": "L", "color_code": null, "price_adjustment": 0 }
]
Les noms envoyés sont stockés tels quels sur la commande — ils doivent matcher ce qu'a renvoyé GET /v1/products/{id}. price_adjustment est ajouté au prix de la ligne (donc final_price = product.price + Σ price_adjustment).
Stock par variante
Si le marchand active variant_stock_enabled sur un produit, chaque option de variante porte son propre compteur de stock. Lisez-le depuis options[].stock. L'API ne vous bloque pas pour créer une commande avec quantity > stock — c'est l'arbitrage du marchand. Le stock est validé et décrémenté seulement à pending → confirmed.
Stock par combinaison
Si combination_stock_enabled est on, le stock est suivi par combinaison (Red+L = 5, Red+M = 8, etc.). Les combinaisons ne sont pas exposées sur l'endpoint produits public (elles le seront en v1.1 comme tableau combinations[] sur GET /v1/products/{id}/combinations). Pour l'instant, les checks de sous-stock arrivent au confirm-time et sont visibles via l'UI dashboard existante.
Variantes en cascade
L'add-on Cascading Variants permet au marchand de faire dépendre les options du groupe B de la sélection du groupe A (ex. "Marque → Modèle" — choisir "Apple" pour Marque ne montre que "iPhone 15" / "iPhone 14" pour Modèle). L'API n'applique pas les règles de cascade ; on attend que votre client connaisse les règles depuis les données d'extension de GET /v1/products/{id} et n'envoie que des combinaisons valides. Si le marchand a des règles de cascade et vous envoyez une combinaison invalide, la commande se crée quand même (on ne bloque pas) — mais le marchand la rejettera au confirm.
Variantes image-texte
Certains marchands utilisent le type image_text (vignette à côté du libellé). Sur l'API, vous envoyez toujours group_name + option_name — l'image est purement une affaire de vitrine et n'est pas dans la payload de commande.
Offres multi-pièces
Si le marchand fait une offre "Achetez 3, mixez les couleurs", le client choisit une variante différente par pièce. Sur l'API :
"items": [
{ "product_id": 26, "quantity": 1,
"variants": [{ "group_name": "Color", "option_name": "Red" }] },
{ "product_id": 26, "quantity": 1,
"variants": [{ "group_name": "Color", "option_name": "Blue" }] },
{ "product_id": 26, "quantity": 1,
"variants": [{ "group_name": "Color", "option_name": "Green" }] }
]
Trois lignes séparées, chacune quantity = 1. Ainsi la page commande dashboard affiche la couleur de chaque pièce proprement.
Patterns courants
« Soumettre une commande depuis une vitrine React custom »
Vous construisez une vitrine React/Vue/Next.js qui parle à l'API au lieu d'utiliser les thèmes intégrés DZBuild. Flow :
- Lisez
GET /v1/products+GET /v1/products/{id}pour rendre le catalogue. - L'utilisateur ajoute au panier côté client.
- Calculez
shipping_costdepuis lewilaya_id(rates en dur ou viaGET /v1/store— à venir v1.1). - POST
/v1/ordersavec panier + bloc client + shipping cost. - Affichez au client son
order_numberet une page « merci ». - Écoutez les webhooks
order.confirmed/order.shippedpour des push notifications.
Voir le guide Thèmes & vitrines personnalisés pour un walkthrough complet.
« Synchroniser les nouvelles commandes vers mon CRM chaque minute »
Utilisez le filtre since :
curl 'https://api.dzbuild.app/v1/orders?since=2026-04-30T20:00:00Z&limit=200' \
-H "Authorization: Bearer $DZ_KEY"
Encore mieux — enregistrez un webhook pour order.created et zappez le polling. Voir Webhooks.
« Confirmer toutes les commandes pending d'un seul client »
PHONE="0555000000"
curl -sS "https://api.dzbuild.app/v1/orders?status=pending&customer_phone=$PHONE" \
-H "Authorization: Bearer $DZ_KEY" \
| jq -r '.data.items[].id' \
| while read OID; do
curl -sS -X PATCH "https://api.dzbuild.app/v1/orders/$OID" \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: confirm-$OID" \
-d '{"status":"confirmed"}'
done