Skip to main content

Orders

Create, read, transition, and cancel orders. Full variants support, multi-line carts, server-authoritative pricing.

Written by Support

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

pending

confirmed, processing, cancelled

confirmed

processing, shipped, cancelled

processing

shipped, cancelled

shipped

delivered, returned

delivered

returned

cancelled

— (terminal)

returned

— (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

pending

Created via storefront or API. Awaiting merchant confirmation.

Not committed

confirmed

Merchant confirmed (called customer, reviewed cart).

Committed (decremented)

processing

Being packed / preparing for courier pickup.

Committed

shipped

Handed to courier.

Committed

delivered

Customer signed for it.

Committed

cancelled

Order cancelled — stock restored if it had been committed.

Restored

returned

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. Requires Idempotency-Key.

The order is created in pending status. 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.

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 }
      ]
    }
  ],
  "shipping_cost":  600,
  "discount":       0,
  "payment_fee":    0,
  "payment_method": "cod",
  "notes":          "Please call before delivery"
}

Field reference

customer (object, required)

Field

Type

Required

Notes

name

string (1–255)

Full name

phone

string

^\+?[0-9 ]{6,20}$ — Algerian or international

email

string | null

If present, will be saved on customer record

wilaya_id

int 1–58

Algerian wilaya code

commune

string (1–100)

Free text, e.g. "Bab Ezzouar"

address

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

type

home | desk | digital

home

digital is for downloadable products only

desk_id

int | null

null

Required if type = desk and you want a specific pickup desk

desk_name

string | null

null

Optional human-readable label

items (array, required, 1–50 lines)

Field

Type

Required

Notes

product_id

int

Must belong to your store (cross-store IDs are rejected with 400)

quantity

int 1–9999

variants

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

group_name

string

e.g. "Color", "Size", "Material"

option_name

string

e.g. "Red", "L", "Cotton"

color_code

string | null

Hex color (only for color-type variants)

price_adjustment

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

shipping_cost

number ≥ 0

0

You compute this client-side from the wilaya + delivery type

discount

number ≥ 0

0

Promo code amount, manual rebate, etc.

payment_fee

number ≥ 0

0

Online payment processor fee

payment_method

cod | free_digital | digital_payment

auto

Defaults to cod for physical, free_digital for digital orders

notes

string ≤ 1000

null

Customer notes, visible on the dashboard order page

Total is computed server-side as subtotal + shipping_cost - discount + payment_fee (clamped to 0). subtotal itself is sum(items[].quantity × (price + Σ variants.price_adjustment)).

shipping_cost, discount and payment_fee are taken exactly as sent — the only check is that each is ≥ 0. They are not validated against your store's shipping rates or coupons, 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 }
        ]
      }
    ],
    "shipping_cost":  600,
    "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

bad_request "Body must be valid JSON"

Wrong Content-Type or malformed JSON

bad_request "customer object is required"

Missing customer

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

Missing or over-long name

bad_request "customer.phone is required (digits, optional leading +)"

Phone failed regex

bad_request "customer.wilaya_id must be 1-58"

Bad wilaya

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

Missing or over-long commune

bad_request "items must be a non-empty array"

Empty cart

bad_request "items: max 50 lines per order"

Too many lines (split into multiple orders)

bad_request "items[N].product_id is required"

Missing product_id

bad_request "Product N does not belong to this store"

Cross-store id

bad_request "items[N].quantity must be 1-9999"

Bad qty

bad_request "delivery.type must be home, desk, or digital"

Bad delivery type

bad_request "payment_method must be cod, free_digital, or digital_payment"

Bad payment method

bad_request "shipping_cost, discount, payment_fee must be ≥ 0"

Negative money field

bad_request "Monthly order limit reached for this store plan"

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 key, same store) within 24 h we return the same response — the order is created exactly once. 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:

  • Send the merchant any new-order notification (email, Telegram, push). If the merchant must be alerted, subscribe to order.created and fan out yourself.

  • Produce any Meta / TikTok pixel or CAPI event.

  • Increment the customer's total_orders / total_spent counters.

  • Check the customer blacklist — a banned customer's API order goes through. Read is_banned from Customers and reject client-side if you need that.

  • Apply the storefront checkout's automated abuse checks, or the Min/Max Quantity addon rules.

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: any active platform key for the store (orders:read is granted by default and is not separately enforced at v1; only orders:write is checked, on the write endpoints).

Query parameters

Param

Type

Notes

limit

int 1–200

Default 50

cursor

string

Opaque

status

one of the 7 states

Filter

since

ISO 8601 string

created_at >= since

customer_phone

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: any active platform key for the store (orders:read is not separately enforced at v1).

Response 200

{
  "data": {
    "id":             6894,
    "order_number":   "ORD-13-20260317-AD3C91F7",
    "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": [
          { "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"
  }
}

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

  • notes is write-only at v1 — it is stored on the order and shown in the dashboard, but never returned by GET /v1/orders/{id}.

  • The item's product_name, sku and line total are captured at create time but are not returned either. Join back to GET /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. Requires Idempotency-Key.

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 → processing shortcut. The product's sales count goes up too.

  • 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

bad_request "Field \"status\" is required"

Body omitted status

bad_request "status must be one of: pending, confirmed, …"

Invalid string

bad_request "Transition X -> Y not allowed. From 'X' you can only go to: …"

Not allowed by the state machine (the API emits a plain ASCII ->)

bad_request "order status changed concurrently; retry"

Another request changed status while you were transitioning. Safe to retry with same idempotency key.

not_found

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. Requires 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)"


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) 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.). Combinations are not exposed on the public products endpoint yet (they will be in v1.1 as a combinations[] array on GET /v1/products/{id}/combinations). For now, per-combination stock is applied when the order is confirmed and is visible in the dashboard.

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:

  1. Read GET /v1/products + GET /v1/products/{id} to render the catalog.

  2. User adds items to a client-side cart.

  3. Compute shipping_cost from the customer's wilaya_id (you can hardcode rates or query a GET /v1/store for store-wide shipping rates — coming v1.1).

  4. POST /v1/orders with the cart + customer block + shipping cost.

  5. Show the customer their order_number and a "thank you" page.

  6. Poll GET /v1/orders?since=… for fulfilment progress. /v1/webhooks order.confirmed / order.shipped events 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
Did this answer your question?