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, unless the store deducts stock as soon as the order arrives

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

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

✅

Algerian wilaya code: 1 to 58, or 1 to 69 on a store set to 69 wilayas (read wilaya_mode from GET /v1/shipping/rates)

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 | pickup | digital

home

digital is for downloadable products only. pickup and digital carry no delivery charge. When the store has the chosen type turned off for that wilaya and the other one on, the order can switch to the type the store offers; delivery.type in the response shows the final type

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 to 9999

Defaults to 1 when omitted

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

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. pickup and digital orders carry no delivery charge. The amount charged is in amounts.shipping_cost

discount

number ≥ 0

0

Promo code amount, manual rebate, etc. Capped at subtotal plus shipping

payment_fee

number

Ignored if sent; always 0

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

bad_request "Body must be valid JSON"

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)

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"

Wilaya outside the store's range; a store set to 69 wilayas answers "customer.wilaya_id must be 1-69"

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, pickup, or digital"

Bad delivery type

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

Bad payment method

bad_request "discount must be >= 0"

Negative discount

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 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_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.

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

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: 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

  • 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 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 → processing shortcut. 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

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. Retry with a new Idempotency-Key: this 400 is stored under the key you sent, and a retry with it replays the same error for 24 h.

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

provider

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.

confirm_token

string

The token from the first call, sent once the merchant approves

  1. Call without confirm_token. The answer is 422 confirmation_required with confirm_token (single use, valid 600 seconds), action and will_change: customer, phone, destination, delivery type, total and courier. Show that summary to the merchant.

  2. Once they approve, repeat the call with the same provider, the confirm_token and a new Idempotency-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 answers 422 confirmation_stale with a fresh token. A confirm: true flag 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

forbidden (403)

The key lacks delivery:send

not_found (404)

Unknown order, or another store's order

already_sent (409)

The order is already with a courier; error.tracking holds its tracking number

confirmation_required, confirmation_stale (422)

See the two steps above

courier_refused (422)

The courier refused the parcel during an in-request send

rate_limited, too_many_concurrent (429)

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:

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

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

  3. To show a delivery price before checkout, read the store's rates with GET /v1/shipping/rates (scope shipping:read). The order itself is always charged the server's own quote.

  4. POST /v1/orders with the cart and the customer block; amounts.shipping_cost in the response is the delivery cost charged.

  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?