Passer au contenu principal

Commandes

Créer, lire, transitionner et annuler des commandes. Support complet des variantes, paniers multi-lignes, tarification autoritative côté serveur.

Écrit par Support

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 des variantes. Les add-ons produit (les extras payants à champs personnalisés) ne sont pas supportés par l'API v1 — voir Add-ons.

  • A un statut de paiement (pending, paid, refunded) indépendant du statut de fulfillment.

Cycle de vie {#lifecycle}

chemin principal  pending → confirmed → processing → shipped → delivered
sauts légaux      pending → processing        confirmed → shipped
annulation        pending | confirmed | processing → cancelled   (terminal)
retour            shipped | delivered → returned                 (terminal)

Voici la carte exacte des transitions appliquée par l'API, état par état :

Depuis

États suivants autorisés

pending

confirmed, processing, cancelled

confirmed

processing, shipped, cancelled

processing

shipped, cancelled

shipped

delivered, returned

delivered

returned

cancelled

— (terminal)

returned

— (terminal)

L'annulation n'est légale que depuis pending, confirmed et processing — une commande shipped ou delivered ne peut plus être annulée.

Un PATCH vers le statut que la commande porte déjà est un 200 sans effet.

État

Signification

Stock

pending

Créée via vitrine ou API. En attente de confirmation marchand.

Non engagé, sauf si la boutique déduit le stock dès la réception de la commande

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 soumet ses commandes depuis son propre serveur au lieu d'utiliser le checkout intégré de la vitrine.

Auth : clé plateforme avec orders:write et orders:read. Nécessite Idempotency-Key. La réponse relit la commande : une clé qui n'a que orders:write enregistre la commande mais reçoit 403 forbidden, et un nouvel essai avec la même Idempotency-Key renvoie ce même 403.

La commande est créée en pending. Par défaut, le stock n'est PAS engagé à la création : c'est la première entrée dans un état engagé (confirmed, processing, shipped ou delivered) qui décrémente le stock, comme dans le flux du tableau de bord. Intentionnel : ça laisse à votre équipe le temps de filtrer les fakes / doublons / non-réponses avant de toucher à l'inventaire. Une boutique dont Commandes → Paramètres d'affichage → Déduction du stock est réglé sur Dès la réception de la commande voit son stock pris dès que l'API crée la commande.

Corps

{
  "customer": {
    "name":      "Sarra Benali",
    "phone":     "0555000111",
    "email":     "[email protected]",
    "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 }
      ]
    }
  ],
  "discount":       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

✅

Code wilaya algérienne : de 1 à 58, ou de 1 à 69 sur une boutique réglée sur 69 wilayas (lisez wilaya_mode via GET /v1/shipping/rates)

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 boutique + télé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 | pickup | digital

home

digital pour produits téléchargeables uniquement. pickup et digital ne portent aucun frais de livraison. Si la boutique a désactivé le type choisi pour cette wilaya et activé l'autre, la commande peut passer au type que la boutique propose ; delivery.type dans la réponse indique le type final

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, de 1 à 9999

Vaut 1 si omis

variants

tableau d'objets variante

Voir ci-dessous

Important — tarification autoritative côté serveur. Vous ne précisez pas le prix de la ligne. C'est toujours le prix catalogue courant du produit qui est appliqué. Si vous envoyez un champ price, il est ignoré.

Le price_adjustment de chaque variante est lui aussi autoritatif côté serveur : pour chaque paire (group_name, option_name) qui correspond à une vraie option du produit, DZBuild substitue le price_adjustment du catalogue. Votre valeur ne survit que pour les paires absentes du catalogue — une tolérance volontaire pour les intégrations obsolètes — donc une « remise » négative que vous inventez est silencieusement écartée pour toute option réelle. Traitez price_adjustment en entrée comme purement informatif : renvoyez la valeur lue sur GET /v1/products/{id} pour que votre total côté client corresponde à celui du serveur.

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. Écrasé par la valeur du catalogue dès que la paire groupe/option existe sur le produit — voir la note tarification ci-dessus

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

Ignoré s'il est envoyé. Le serveur calcule la livraison à partir du tarif de la boutique pour la wilaya et le type de livraison, de ses règles de livraison gratuite et du supplément de poids, comme pour une commande créée dans le tableau de bord. Les commandes pickup et digital ne portent aucun frais de livraison. Le montant facturé figure dans amounts.shipping_cost

discount

number ≥ 0

0

Montant code promo, remise manuelle, etc. Plafonné au sous-total plus la livraison

payment_fee

number

Ignoré s'il est envoyé ; toujours 0

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 (jamais sous 0), où shipping_cost est le montant calculé par le serveur lui-même. Le subtotal lui-même = sum(items[].quantity × (price + Σ variants.price_adjustment)).

Seul discount est repris de votre corps de requête. Il doit être ≥ 0, il est plafonné au sous-total plus la livraison, et il n'est pas vérifié contre les codes promo de votre boutique : POST /v1/orders ne doit donc être appelé que depuis un serveur de confiance, jamais depuis du code navigateur ou applicatif qu'un client peut altérer.

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 }
        ]
      }
    ],
    "payment_method": "cod"
  }'

Réponse 200

La création d'une commande renvoie HTTP 200, pas 201 — ne testez pas le code de statut ; vérifiez data.id / data.order_number. Le corps a la même forme que GET /v1/orders/{id} — entièrement peuplé avec totaux calculés, bloc client normalisé, et lignes que vous venez de créer (avec leurs variantes).

order_number a le format ORD-{store_id}-{YYYYMMDD}-{8 caractères hex majuscules}. Les commandes issues d'une landing page utilisent LP-{8 caractères hex majuscules}. Les commandes créées avant le 2026-06-02 portent un suffixe hérité de 4 caractères hex : un parseur doit accepter les deux longueurs.

Erreurs

Code

Cause

bad_request "Body must be valid JSON"

JSON malformé, ou corps qui est une valeur JSON isolée comme une chaîne ou un nombre au lieu d'un objet (l'en-tête Content-Type n'est pas vérifié)

bad_request "customer object is required"

customer manquant

bad_request "customer.name is required (1-255 chars)"

Nom manquant ou trop long

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 hors de la plage de la boutique ; une boutique réglée sur 69 wilayas répond "customer.wilaya_id must be 1-69"

bad_request "customer.commune is required (1-100 chars)"

Commune manquante ou trop longue

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, pickup, 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 "discount must be >= 0"

Remise négative

bad_request "Monthly order limit reached for this store plan"

Plafond du plan Free — voir ci-dessous

Plafond mensuel de commandes. Le plan Free est plafonné à 30 commandes par mois calendaire ; Pro, Unlimited et Enterprise sont sans plafond. Un nom de plan non reconnu retombe également sur le plafond Free de 30/mois. Le compteur est mensuel calendaire et couvre toutes les sources de commandes (vitrine + landing page + tableau de bord + API).

Idempotence

Chaque POST doit porter un en-tête Idempotency-Key. Si vous rejouez la même requête (même Idempotency-Key, même clé API, mêmes octets de corps) sous 24 h, on renvoie la même réponse avec Idempotency-Replay: 1 : la commande est créée exactement une fois. Les réponses d'erreur sont rejouées aussi, sauf 429 et 5xx, et la même Idempotency-Key avec un corps différent répond 422 idempotency_key_reuse : envoyez une commande corrigée avec une nouvelle clé. 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 commande est créée

Webhook déclenché

Créer une commande déclenche order.created vers tous les abonnements /v1/webhooks à cet événement. Voir Événements webhook.

⚠️ Attention — /v1/webhooks ne voit que l'activité API

Les abonnements /v1/webhooks se déclenchent uniquement pour les changements effectués via l'API v1. Les commandes passées sur la vitrine ou une landing page, et les changements de statut faits dans le tableau de bord, ne déclenchent pas order.created / order.confirmed / order.shipped.

Pour des événements couvrant toute l'activité commandes, utilisez l'add-on Webhooks (/dashboard/addons → « Webhooks — Connect n8n, Make & Zapier », nécessite le plan Unlimited), qui capte toutes les commandes quelle que soit leur origine — ou continuez à interroger GET /v1/orders?since=….

Ce que le chemin de commande API saute

Une commande créée via l'API est un insert allégé. Comparée à une commande vitrine, landing page ou tableau de bord, elle ne :

  • Produit aucun événement Pixel ou CAPI Meta / TikTok.

  • N'incrémente pas les compteurs total_orders / total_spent du client.

  • Ne consulte pas la blacklist client — la commande API d'un client blacklisté passe. Lisez is_banned depuis Clients et refusez côté client si nécessaire.

  • N'applique ni les contrôles anti-abus automatiques du checkout de la vitrine, ni les règles de l'add-on quantité min/max.

Le marchand reçoit bien la notification habituelle de nouvelle commande, la même que pour une commande vitrine.

Notez aussi qu'un client créé par l'API stocke la chaîne customer.name entière dans first_name et laisse last_name vide ; pour un client existant retrouvé par téléphone, seul first_name est écrasé et tout last_name existant reste intact.


GET /v1/orders

Liste les commandes. Pagination par curseur.

Auth : clé avec orders:read (incluse dans les scopes par défaut). Une clé sans ce scope reçoit 403 forbidden « Missing scope: 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

Un status invalide ou un since non analysable est silencieusement ignoré — vous recevez la liste non filtrée, pas un 400. Il n'y a pas de filtre payment_status en v1 ; filtrez côté client.

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-AD3C91F7",
        "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é avec orders:read. Une clé sans ce scope reçoit 403 forbidden.

Réponse 200

{
  "data": {
    "id":             6894,
    "order_number":   "ORD-13-20260317-AD3C91F7",
    "store_seq":      644,
    "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 },
    "shipment": {
      "sent_to_delivery":    true,
      "sent_to_delivery_at": "2026-09-06 16:04:30",
      "delivery_company":    "colivraison",
      "delivery_tracking":   "TRK-0000000000",
      "last_send_failure":   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": [
          { "order_item_id": 8421, "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"
  }
}

Champs d'expédition

store_seq est le numéro affiché dans le tableau de bord (#644) ; une création via l'API numérote la commande avant de répondre, et dans le rare cas où la numérotation échoue il vaut null jusqu'à ce qu'il soit rempli peu après. shipment.sent_to_delivery passe à true quand le transporteur a accepté le colis ; delivery_company est le transporteur (son slug) et delivery_tracking son numéro de suivi. shipment.last_send_failure est le dernier envoi refusé pour cette commande (at, provider, message du transporteur), ou null si aucun envoi n'a échoué. Il vaut toujours null dès que sent_to_delivery est true : un échec antérieur n'apparaît plus après un envoi réussi. La création, la mise à jour et l'annulation renvoient le même objet.

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 order_item_id, group_name, option_name, color_code (variantes couleur), et price_adjustment (le supplément par pièce, renvoyé comme string JSON et non comme nombre). Pour les offres multi-pièces, vous verrez plusieurs entrées de variantes sur le même article avec des regroupements effectifs différents — voir notes par pièce ci-dessous.

Ce que la réponse détail ne renvoie pas

  • notes est en écriture seule en v1 — il est stocké sur la commande et visible dans le tableau de bord, mais jamais renvoyé par GET /v1/orders/{id}.

  • Les product_name, sku de l'article et le total de la ligne sont capturés à la création mais ne sont pas renvoyés non plus. Rejoignez GET /v1/products/{id} si vous avez besoin des noms.

Les add-ons ne sont pas exposés {#add-ons-are-not-exposed}

Les add-ons produit (les extras payants à champs personnalisés qu'un marchand configure sur un produit) n'ont aucune surface en v1 : vous ne pouvez pas les envoyer sur POST /v1/orders, et GET /v1/orders/{id} ne renvoie aucune entrée d'add-on. Une commande passée sur la vitrine avec des add-ons se relit via l'API avec le montant des add-ons déjà intégré dans items[].price (et donc dans amounts.subtotal) — mais sans ligne d'add-on, sans titre et sans attribution de fichier.


PATCH /v1/orders/{id} — changer le statut

Auth : clé plateforme avec orders:write et orders:read. Nécessite Idempotency-Key. Sans orders:read, le statut change quand même, mais la réponse est 403 forbidden.

Le corps doit être {"status": "<un des 7>"}. On valide la transition contre le tableau du Cycle de vie ; sinon on renvoie 400 avec les états suivants légaux. Un PATCH vers le statut courant de la commande est un 200 sans effet.

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. Les transitions concurrentes sur une même commande sont sérialisées : si une autre requête a changé le statut avant vous, la vôtre échoue proprement en bad_request (« order status changed concurrently; retry ») au lieu de s'appliquer deux fois.

Effets sur le stock

Les états à stock engagé sont confirmed, processing, shipped et delivered.

  • Non-engagé → engagé — le stock est décrémenté la première fois que la commande entre dans l'un de ces états, y compris via le raccourci pending → processing. Le compteur de ventes du produit augmente également.

  • Sur une boutique dont Déduction du stock est réglé sur Dès la réception de la commande, le stock a déjà été pris à la création de la commande : ce passage ne prend rien de plus.

  • Engagé → cancelled / returned — le stock est restitué.

  • Les autres transitions ne touchent pas au stock.

Le stock n'est pas validé : la décrémentation est bornée à zéro, donc une survente n'est jamais rejetée, et un problème de stock ne fait jamais échouer ni annuler le changement de statut. Le stock se comporte exactement comme pour les commandes gérées depuis le tableau de bord.

Erreurs

Code

Cause

bad_request "Field \"status\" is required"

Le corps n'a pas de status

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 (l'API émet un -> ASCII simple)

bad_request "order status changed concurrently; retry"

Une autre requête a changé le statut pendant votre transition. Réessayez avec une nouvelle Idempotency-Key : ce 400 est enregistré sous la clé envoyée, et un nouvel essai avec elle rejoue la même erreur pendant 24 h.

not_found

ID commande inconnu ou appartient à une autre boutique


POST /v1/orders/{id}/cancel

Endpoint de commodité — exactement équivalent à PATCH /v1/orders/{id} avec {"status":"cancelled"}. Même table de transitions, aucune permission supplémentaire. L'annulation n'est légale que depuis pending, confirmed et processing ; depuis shipped ou delivered vous obtenez 400 bad_request (ces états ne peuvent aller que vers delivered / returned).

Auth : clé plateforme avec orders:write et orders:read. Nécessite Idempotency-Key. Sans orders:read, la commande est quand même annulée, mais la réponse est 403 forbidden.

curl -X POST 'https://api.dzbuild.app/v1/orders/6894/cancel' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Idempotency-Key: cancel-6894-$(date +%s)"


POST /v1/orders/{id}/send-to-delivery

Remet la commande au transporteur de la boutique, le même envoi que le bouton du tableau de bord. Un colis ne peut pas être rappelé une fois chez le transporteur : chaque envoi demande donc deux appels, et le marchand approuve le premier.

Auth : clé avec delivery:send. Nécessite Idempotency-Key. Les clés créées dans Paramètres → API ne portent pas ce scope et reçoivent 403 forbidden ; une application l'obtient quand le marchand l'approuve à l'installation.

Champ

Type

Notes

provider

string, optionnel

Slug d'un transporteur lié à la boutique. Omettez-le pour utiliser le transporteur par défaut de la boutique. Un transporteur non lié est refusé.

confirm_token

string

Le jeton du premier appel, envoyé une fois que le marchand a approuvé

  1. Appelez sans confirm_token. La réponse est 422 confirmation_required avec confirm_token (usage unique, valable 600 secondes), action et will_change : client, téléphone, destination, type de livraison, total et transporteur. Montrez ce résumé au marchand.

  2. Une fois qu'il approuve, refaites l'appel avec le même provider, le confirm_token et une nouvelle Idempotency-Key (la première clé est liée au corps sans le jeton). Un jeton expiré, déjà utilisé ou qui ne correspond plus à la commande répond 422 confirmation_stale avec un nouveau jeton. Un drapeau confirm: true n'est pas accepté ici.

L'envoi tourne normalement en arrière-plan et répond 202 avec queued: true. Lisez GET /v1/orders/{id} jusqu'à ce que shipment.sent_to_delivery soit true avec un delivery_tracking, ou que shipment.last_send_failure montre le refus du transporteur. Si le traitement en arrière-plan n'est pas disponible, l'envoi se fait dans la requête : 200 avec sent, provider et tracking, ou 422 courier_refused avec le message du transporteur. Une fois le colis accepté par le transporteur, une commande pending ou confirmed passe en processing, ce qui prend le stock s'il ne l'était pas encore. Les commandes de type de livraison pickup ne sont jamais remises à un transporteur. Un envoi en arrière-plan arrêté avant l'appel au transporteur, par exemple pour une commande pickup ou un transporteur non lié, répond quand même 202 et ne change aucun des deux champs shipment.

Code

Cause

forbidden (403)

La clé n'a pas delivery:send

not_found (404)

Commande inconnue, ou commande d'une autre boutique

already_sent (409)

La commande est déjà chez un transporteur ; error.tracking contient son numéro de suivi

confirmation_required, confirmation_stale (422)

Voir les deux étapes ci-dessus

courier_refused (422)

Le transporteur a refusé le colis pendant un envoi dans la requête

rate_limited, too_many_concurrent (429)

Les appels transporteur ont leur propre budget par boutique, en plus des limites de taux

curl -X POST 'https://api.dzbuild.app/v1/orders/6894/send-to-delivery' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: send-6894-approved" \
  -d '{"confirm_token": "cft_REPLACE_WITH_TOKEN"}'


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), mais pour toute paire présente dans le catalogue le serveur substitue sa propre valeur : envoyez donc le chiffre du catalogue.

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 décrémenté à la première entrée dans un état engagé (confirmed, processing, shipped, delivered), ou dès la création sur une boutique qui déduit le stock dès la réception de la commande, et n'est jamais validé : la décrémentation est bornée à zéro, donc une survente est enregistrée plutôt que rejetée.

Stock par combinaison

Si combination_stock_enabled est on, le stock est suivi par combinaison (Red+L = 5, Red+M = 8, etc.). GET /v1/products/{id} les renvoie dans combinations[] (chacune avec id, sku, stock, is_active et ses options), avec combination_count ; la liste s'arrête à 300 entrées et combinations_truncated indique si elle a été coupée. Voir Produits. Le stock par combinaison bouge au même moment que le reste du stock de la commande.

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). Cet add-on nécessite le plan Pro.

GET /v1/products/{id} n'expose aucune information de cascade — le mappage parent/enfant n'est appliqué que sur la vitrine et dans le tableau de bord. Votre client ne peut pas découvrir les règles de cascade via l'API en v1 : codez-les en dur, ou lisez-les depuis le tableau de bord. Envoyer une combinaison invalide crée quand même la commande (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 :

  1. Lisez GET /v1/products + GET /v1/products/{id} pour rendre le catalogue.

  2. L'utilisateur ajoute au panier côté client.

  3. Pour afficher un prix de livraison avant la validation, lisez les tarifs de la boutique avec GET /v1/shipping/rates (scope shipping:read). La commande elle-même est toujours facturée au montant calculé par le serveur.

  4. POST /v1/orders avec panier + bloc client ; amounts.shipping_cost dans la réponse est le coût de livraison facturé.

  5. Affichez au client son order_number et une page « merci ».

  6. Interrogez GET /v1/orders?since=… pour suivre l'avancement du fulfillment. Les événements /v1/webhooks order.confirmed / order.shipped ne se déclenchent que si le statut a été changé via l'API — un marchand qui confirme dans le tableau de bord ne produit aucun événement.

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"

Le polling est le bon outil ici. Un abonnement /v1/webhooks sur order.created ne couvre que les commandes créées par votre propre intégration via l'API — les commandes vitrine et landing page ne le déclenchent jamais, il ne peut donc pas remplacer le balayage since. 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
Avez-vous trouvé la réponse à votre question ?