Orders are the heart of the platform. Every order:
Belongs to exactly one store (scoped by your API key — you can never accidentally touch another merchant's data).
Has a 7-state lifecycle.
Is tied to a customer (deduped by phone within the store).
Has 1+ items, each optionally with variants. Product add-ons (the paid custom-field extras) are not supported by the v1 API — see Add-ons.
Has a payment status (
pending,paid,refunded) independent of the fulfillment status.
Lifecycle
main path pending → confirmed → processing → shipped → delivered legal skips pending → processing confirmed → shipped cancel pending | confirmed | processing → cancelled (terminal) return shipped | delivered → returned (terminal)
That is the exact transition map the API enforces, per state:
From | Allowed next states |
|
|
|
|
|
|
|
|
|
|
| — (terminal) |
| — (terminal) |
Cancellation is legal only from pending, confirmed and processing — a shipped or delivered order can no longer be cancelled.
PATCHing an order to the status it already has is a 200 no-op.
State | Meaning | Stock |
| Created via storefront or API. Awaiting merchant confirmation. | Not committed, unless the store deducts stock as soon as the order arrives |
| Merchant confirmed (called customer, reviewed cart). | Committed (decremented) |
| Being packed / preparing for courier pickup. | Committed |
| Handed to courier. | Committed |
| Customer signed for it. | Committed |
| Order cancelled — stock restored if it had been committed. | Restored |
| Customer returned the item — stock restored. | Restored |
cancelled and returned are terminal — you cannot transition out of them.
POST /v1/orders — create an order
Create a new order for the calling store. Used by custom themes, mobile / native apps, resellers, and any headless storefront that submits orders from its own server instead of using the built-in storefront checkout.
Auth: platform key with orders:write and orders:read. Requires Idempotency-Key. The answer reads the order back, so a key holding orders:write alone gets the order saved and a 403 forbidden answer, and a retry with the same Idempotency-Key replays that 403.
The order is created in pending status. By default stock is NOT committed at create time: the first move into a committed state (confirmed, processing, shipped or delivered) is what decrements stock, same as the dashboard flow. This is intentional: it lets your operations team filter out fake / duplicate / no-answer orders before any inventory is touched. A store whose Orders → Display settings → Stock deduction is set to As soon as the order arrives has its stock taken when the API creates the order instead.
Body
{
"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"
}
Field reference
customer (object, required)
Field | Type | Required | Notes |
| string (1–255) | ✅ | Full name |
| string | ✅ |
|
| string | null | If present, will be saved on customer record | |
| int | ✅ | Algerian wilaya code: 1 to 58, or 1 to 69 on a store set to 69 wilayas (read |
| string (1–100) | ✅ | Free text, e.g. "Bab Ezzouar" |
| string | Street + apt; can be empty for stop-desk |
Customers are deduped by store + phone. If a customer with this phone already exists in your store, their record is updated (name, wilaya, commune, address, email) and reused. If not, a new customer record is created.
delivery (object, optional)
Field | Type | Default | Notes |
|
|
|
|
| int | null | null | Required if |
| string | null | null | Optional human-readable label |
items (array, required, 1–50 lines)
Field | Type | Required | Notes |
| int | ✅ | Must belong to your store (cross-store IDs are rejected with |
| int, 1 to 9999 | Defaults to 1 when omitted | |
| array of variant objects | See below |
Important — server-authoritative pricing. You do not specify the line price. The product's current catalogue price is always used. If you send a price field, it is ignored.
Each variant's price_adjustment is server-authoritative too: for every (group_name, option_name) pair that matches a real option on the product, DZBuild substitutes the catalogue's own price_adjustment. Your value survives only for pairs that don't exist in the catalogue — a fail-open for stale integrations — so a negative "discount" you invent is silently discarded for any real option. Treat price_adjustment as informational on input: echo the value from GET /v1/products/{id} so your client-side total matches the server's.
items[].variants (array, optional)
Each variant object describes a chosen option for a variant group on the product:
Field | Type | Notes |
| string | e.g. "Color", "Size", "Material" |
| string | e.g. "Red", "L", "Cotton" |
| string | null | Hex color (only for color-type variants) |
| number | Added to base price. Overwritten by the catalogue value whenever the group/option pair exists on the product — see the pricing note above |
Send one variant object per chosen group for that line item. So a "Red T-shirt size L" becomes 2 variant entries (one for Color/Red, one for Size/L). DZBuild renders these on the dashboard order page exactly the same way as if a customer picked them on the storefront.
For products using per-piece variants (e.g. a "buy 3 t-shirts, pick a color for each" offer), use quantity = 1 per line and create one line per piece — that's the cleanest mapping.
Top-level money fields
Field | Type | Default | Notes |
| number | Ignored if sent. The server quotes delivery from the store's rate for the wilaya and delivery type, its free-shipping rules and the weight surcharge, the same way as an order created in the dashboard. | |
| number ≥ 0 | 0 | Promo code amount, manual rebate, etc. Capped at subtotal plus shipping |
| number | Ignored if sent; always 0 | |
|
| auto | Defaults to |
| string ≤ 1000 | null | Customer notes, visible on the dashboard order page |
Total is computed server-side as subtotal + shipping_cost - discount (never below 0), where shipping_cost is the server's own quote. subtotal itself is sum(items[].quantity × (price + Σ variants.price_adjustment)).
Only discount is taken from your body. It must be ≥ 0, is capped at the subtotal plus shipping, and is not checked against your store's promo codes, so POST /v1/orders must only ever be called from a trusted server, never from browser or app code a customer can tamper with.
Request
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"
}'
Response 200
Order creation returns HTTP 200, not 201 — do not branch on the status code; check data.id / data.order_number instead. The body is the same shape as GET /v1/orders/{id} — fully populated with computed totals, normalized customer block, and the line items you just created (with their variants).
order_number has the format ORD-{store_id}-{YYYYMMDD}-{8 uppercase hex}. Landing-page orders use LP-{8 uppercase hex}. Orders created before 2026-06-02 carry a legacy 4-hex suffix, so a parser must accept both lengths.
Errors
Code | Cause |
| Malformed JSON, or a bare JSON value such as a string or a number instead of an object (the Content-Type header is not checked) |
| Missing |
| Missing or over-long name |
| Phone failed regex |
| Wilaya outside the store's range; a store set to 69 wilayas answers "customer.wilaya_id must be 1-69" |
| Missing or over-long commune |
| Empty cart |
| Too many lines (split into multiple orders) |
| Missing |
| Cross-store id |
| Bad qty |
| Bad delivery type |
| Bad payment method |
| Negative discount |
| Free plan cap — see below |
Monthly order limit. The Free plan is capped at 30 orders per calendar month; Pro, Unlimited and Enterprise are uncapped. An unrecognised plan name also falls back to the 30/month free cap. The counter is calendar-month and covers all order sources (storefront + landing page + dashboard + API).
Idempotency
Every POST must carry an Idempotency-Key header. If you retry the same request (same Idempotency-Key, same API key, same body bytes) within 24 h we return the same response with Idempotency-Replay: 1, so the order is created exactly once. Error answers are replayed too, except 429 and 5xx, and the same Idempotency-Key with a different body answers 422 idempotency_key_reuse: send a corrected order with a new key. See Idempotency.
# safe to retry indefinitely with the same key
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
# only one order is ever created
Webhook fired
Creating an order fires order.created to all /v1/webhooks subscriptions for that event. See Webhook events.
⚠️ Warning — /v1/webhooks only sees API activity
/v1/webhooks subscriptions fire only for changes made through the v1 API. Orders placed on the storefront or a landing page, and status changes made in the dashboard, do not fire order.created / order.confirmed / order.shipped.
For events covering all order activity, use the Webhooks addon (/dashboard/addons → "Webhooks — Connect n8n, Make & Zapier", requires the Unlimited plan), which picks up every order regardless of where it came from — or keep polling GET /v1/orders?since=….
What the API order path skips
An order created through the API is a lean insert. Compared with a storefront, landing-page or dashboard order, it does not:
Produce any Meta / TikTok pixel or CAPI event.
Increment the customer's
total_orders/total_spentcounters.Check the customer blacklist — a banned customer's API order goes through. Read
is_bannedfrom Customers and reject client-side if you need that.Apply the storefront checkout's automated abuse checks, or the Min/Max Quantity addon rules.
The merchant does get the usual new-order notification, the same one a storefront order sends.
Also note that a customer created by the API stores the whole customer.name string in first_name and leaves last_name empty; for an existing customer matched by phone, only first_name is overwritten and any existing last_name is left untouched.
GET /v1/orders
List orders. Cursor-paginated.
Auth: key with orders:read (part of the default scopes). A key without it gets 403 forbidden "Missing scope: orders:read".
Query parameters
Param | Type | Notes |
| int 1–200 | Default 50 |
| string | Opaque |
| one of the 7 states | Filter |
| ISO 8601 string |
|
| string | Exact match |
An invalid status or an unparseable since is silently ignored — you get the unfiltered list, not a 400. There is no payment_status filter at v1; filter client-side.
Request
curl 'https://api.dzbuild.app/v1/orders?status=pending&limit=20' \ -H "Authorization: Bearer $DZ_KEY"
Response 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
}
}
The list view is intentionally compact (no items, no variants). Call GET /v1/orders/{id} for the full detail.
GET /v1/orders/{id}
Full detail with line items, variants, and customer block.
Auth: key with orders:read. A key without it gets 403 forbidden.
Response 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"
}
}
Shipment fields
store_seq is the order number the merchant sees in the dashboard (#644); an API create numbers the order before it answers, and in the rare case numbering fails it reads null until it is filled shortly after. shipment.sent_to_delivery turns true once a courier accepted the parcel; delivery_company is the courier slug and delivery_tracking its tracking number. shipment.last_send_failure is the most recent refused send for this order, { "at", "provider", "message" } with the courier's own message, or null when no send failed. It is always null once sent_to_delivery is true, so an earlier failure stops showing after a successful send. The same object is returned by create, update and cancel.
Variants in the response
The variants array on each item is the source of truth for what the customer picked. Each entry has order_item_id, group_name, option_name, color_code (for color variants), and price_adjustment (the per-piece add-on price, returned as a JSON string, not a number). For multi-piece offers, you may see multiple variant entries on the same item with different effective groupings — see the per-piece notes below.
What the detail response does not return
notesis write-only at v1 — it is stored on the order and shown in the dashboard, but never returned byGET /v1/orders/{id}.The item's
product_name,skuand linetotalare captured at create time but are not returned either. Join back toGET /v1/products/{id}if you need names.
Add-ons are not exposed
Product add-ons (the paid custom-field extras a merchant configures on a product) have no v1 surface: you cannot send them on POST /v1/orders, and GET /v1/orders/{id} returns no add-on entries. An order placed on the storefront with add-ons reads back through the API with the add-on money already baked into items[].price (and therefore amounts.subtotal) — but with no add-on line, title or file attribution.
PATCH /v1/orders/{id} — change status
Auth: platform key with orders:write and orders:read. Requires Idempotency-Key. Without orders:read the status still changes, but the answer is 403 forbidden.
Body must be {"status": "<one of the 7>"}. We validate the transition against the table in Lifecycle; otherwise we return 400 with the legal next states. PATCHing to the order's current status is a 200 no-op.
Request
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"}'
Returns 200 and the updated order detail. Concurrent transitions on the same order are serialised: if another request changed the status first, yours fails cleanly with bad_request ("order status changed concurrently; retry") instead of applying twice.
Stock side-effects
The committed-stock states are confirmed, processing, shipped and delivered.
Non-committed → committed — stock is decremented the first time the order enters any of those states, including the
pending → processingshortcut. The product's sales count goes up too.On a store whose Stock deduction is set to As soon as the order arrives, stock was already taken when the order was created, so this move takes nothing more.
Committed → cancelled / returned — stock is restored.
Other transitions don't touch stock.
Stock is not validated: the decrement clamps at zero, so overselling is never rejected, and a stock problem never fails or reverts the status change. Stock behaves exactly as it does for orders managed from the dashboard.
Errors
Code | Cause |
| Body omitted |
| Invalid string |
| Not allowed by the state machine (the API emits a plain ASCII |
| Another request changed status while you were transitioning. Retry with a new |
| Order id is unknown or belongs to another store |
POST /v1/orders/{id}/cancel
Convenience endpoint — exactly equivalent to PATCH /v1/orders/{id} with {"status":"cancelled"}. Same transition table, no extra permission. Cancellation is legal only from pending, confirmed and processing; from shipped or delivered you get 400 bad_request (those states can only go to delivered / returned).
Auth: platform key with orders:write and orders:read. Requires Idempotency-Key. Without orders:read the order is still cancelled, but the answer is 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
Hands the order to the store's courier, the same send as the button in the dashboard. A parcel cannot be recalled once the courier has it, so every send takes two calls and the merchant approves the first one.
Auth: key with delivery:send. Requires Idempotency-Key. Keys created at Settings → API do not carry this scope and get 403 forbidden; an app gets it when the merchant approves it at install.
Field | Type | Notes |
| string, optional | Slug of a courier linked to the store. Omit it to use the store's default courier. A courier that is not linked is refused. |
| string | The token from the first call, sent once the merchant approves |
Call without
confirm_token. The answer is422 confirmation_requiredwithconfirm_token(single use, valid600seconds),actionandwill_change: customer, phone, destination, delivery type, total and courier. Show that summary to the merchant.Once they approve, repeat the call with the same
provider, theconfirm_tokenand a newIdempotency-Key(the first key is bound to the body without the token). A token that expired, was already used, or no longer matches the order answers422 confirmation_stalewith a fresh token. Aconfirm: trueflag is not accepted here.
The send normally runs in the background and answers 202 with queued: true. Read GET /v1/orders/{id} until shipment.sent_to_delivery is true with a delivery_tracking, or until shipment.last_send_failure shows the courier's refusal. If the background runner is unavailable the send happens inside the request instead: 200 with sent, provider and tracking, or 422 courier_refused with the courier's message. Once the courier accepts the parcel, a pending or confirmed order moves to processing, which takes the stock if it was not taken yet. Orders with delivery type pickup are never handed to a courier. A background send stopped before the courier is called, such as one for a pickup order or for a courier that is not linked, still answers 202 and changes neither shipment field.
Code | Cause |
| The key lacks |
| Unknown order, or another store's order |
| The order is already with a courier; |
| See the two steps above |
| The courier refused the parcel during an in-request send |
| Courier calls have their own per-store budget on top of the rate limits |
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"}'
Variants — full reference
Variants make a single product cover multiple options (color, size, material, capacity, …). On the storefront, customers click variant cards to choose what they want; on the API, you send the chosen options as part of the order.
Variant model
A product has 0+ variant groups. A group has 0+ options. Each option can have:
A
value(display name)A
color_code(hex color, only for color-type groups)A
price_adjustment(added to base price)An
image_id(linked product image used as the swatch)
Read GET /v1/products/{id} to see all groups and options for a product:
{
"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 }
]
}
]
}
A product with 2 colors × 3 sizes has 6 combinations.
How to send variants on POST /v1/orders
Convert the customer's selection into one variant entry per chosen group. For a "Red T-shirt, size 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 }
]
The names you send are stored exactly as-is on the order — they should match what GET /v1/products/{id} returned. price_adjustment is added to the line price (so final_price = product.price + Σ price_adjustment), but for any pair that exists in the catalogue the server substitutes its own value, so send the catalogue's number.
Per-variant stock
If the merchant has enabled variant_stock_enabled on a product, each variant option carries its own stock counter. Read it from options[].stock. The API itself does not block you from creating an order with quantity > stock; that's the merchant's call. Stock is decremented on the first move into a committed state (confirmed, processing, shipped, delivered), or at create on a store that deducts stock as soon as the order arrives, and is never validated: the decrement clamps at zero, so an oversell is recorded rather than rejected.
Per-combination stock
If combination_stock_enabled is on, stock is tracked per combination (Red+L = 5 units, Red+M = 8, etc.). GET /v1/products/{id} returns them in combinations[] (each with id, sku, stock, is_active and its options), with combination_count; the list stops at 300 entries and combinations_truncated tells you when it was cut. See Products. Combination stock moves at the same moment as the rest of the order's stock.
Cascading variants
The Cascading Variants add-on lets a merchant make Group B's options depend on Group A's selection (e.g. "Brand → Model" — picking "Apple" for Brand only shows "iPhone 15" / "iPhone 14" for Model). The add-on requires the Pro plan.
GET /v1/products/{id} exposes no cascade information — the parent/child mapping is only applied on the storefront and in the dashboard. Your client cannot discover cascade rules through the API at v1: hard-code them, or read them from the dashboard. Sending an invalid combination still creates the order (we don't block it) — but the merchant will reject it on confirm.
Image-text variants
Some merchants use the image_text variant type (a thumbnail next to the option label). On the API, you still send group_name + option_name — the image is purely a storefront concern and is not part of the order payload.
Multi-piece offers
If the merchant runs a "Buy 3, mix colors" offer, the customer picks a different variant for each piece. On the 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" }] }
]
Three separate line items, each quantity = 1. This way the dashboard order page renders each piece's color cleanly.
Common patterns
"Submit an order from a custom React storefront"
You're building a React/Vue/Next.js storefront that talks to the API instead of using DZBuild's built-in storefront themes. Flow:
Read
GET /v1/products+GET /v1/products/{id}to render the catalog.User adds items to a client-side cart.
To show a delivery price before checkout, read the store's rates with
GET /v1/shipping/rates(scopeshipping:read). The order itself is always charged the server's own quote.POST
/v1/orderswith the cart and the customer block;amounts.shipping_costin the response is the delivery cost charged.Show the customer their
order_numberand a "thank you" page.Poll
GET /v1/orders?since=…for fulfilment progress./v1/webhooksorder.confirmed/order.shippedevents fire only when the status was changed through the API — a merchant confirming in the dashboard produces no event.
See the Custom themes & storefronts guide for a full end-to-end walkthrough.
"Sync new orders to my CRM every minute"
Use the since filter:
curl 'https://api.dzbuild.app/v1/orders?since=2026-04-30T20:00:00Z&limit=200' \ -H "Authorization: Bearer $DZ_KEY"
Polling is the right tool here. A /v1/webhooks subscription on order.created only covers orders your own integration created through the API — storefront and landing-page orders never fire it, so it cannot replace the since sweep. See Webhooks.
"Confirm all pending orders for one customer"
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