DZBuild ships several ready-made storefront themes and a no-code customizer. But if you want full control — your own React/Vue/Next.js/Flutter front-end, your own pixel-perfect design, your own routing — the API is built for you.
This guide walks through building a headless storefront end-to-end:
Render the catalog (products, categories, variants)
Build a cart (client-side or server-side, your call)
Submit the order via API
Receive webhooks when status changes
By the end, your custom storefront will be 100% interoperable with the merchant's DZBuild dashboard — orders show up, stock decrements, shipping integrates with their courier setup, no compromises.
Architecture
┌──────────────────────┐ ┌──────────────────────────┐
│ Custom storefront │ HTTPS │ api.dzbuild.app/v1/* │
│ (React, Vue, Flutter)│ ──────► │ Authorization: Bearer … │
│ │ │ │
│ - Reads products │ ◄────── │ JSON responses │
│ - Renders cart │ │ │
│ - Submits orders │ └──────────────────────────┘
└──────────────────────┘ │
▼
Merchant DZBuild dashboard
- Confirms orders
- Manages stock
- Integrates with couriers
You own the UI. DZBuild owns the data and the operations. The merchant logs into dzbuild.com/dashboard to manage orders confirmed in your custom UI.
Prerequisites
A DZBuild store on an active Enterprise plan. The API is an Enterprise feature: a key whose store is on another plan, or whose Enterprise subscription has expired, gets
403 forbidden"API access requires an active Enterprise plan"on every call, and works again once the store is back on an active Enterprise plan.An API key generated by the store owner at Settings → API in the dashboard. Keys made there are already enrolled in the pilot. A key that is not enrolled gets
403 forbidden"API is in pilot mode; key not enrolled".A back-end (or serverless function / edge worker) that holds the API key. Never ship the secret to the browser — see Security.
The plan also sets a monthly order cap: a Free-plan store stops accepting orders after 30 in a calendar month, and POST /v1/orders then returns 400 bad_request "Monthly order limit reached for this store plan". Pro, Unlimited and Enterprise have no order cap. Handle that error in your checkout.
Step 0 — get a key
The store owner generates keys in the merchant dashboard at Settings → API (/dashboard/api), on an active Enterprise plan. A store holds up to 3 active keys of its own, and each secret is shown once:
Generate the first key at Settings → API. Keys made there are already enrolled in the pilot.
Mint any further keys yourself from that one, or generate them on the same page:
curl -X POST 'https://api.dzbuild.app/v1/keys' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"type":"platform","name":"Custom storefront — production"}'
# HTTP 200 → { "data": { "key_id", "bearer_token", "signing_secret", "note" } }
The new key copies the calling key's rate-limit tier and pilot flag. Scopes are not selectable: a new platform key gets the default set (store:read, store:write, products:read, products:write, orders:read, orders:write, customers:read, landing_pages:read, landing_pages:write, promos:read, promos:write, pixels:read, pixels:write, shipping:read, shipping:write, webhooks:read, webhooks:write, usage:read, analytics:read, whatsapp:read, whatsapp:send), limited to the scopes the calling key holds, so isolate environments with separate keys, not with narrower permissions.
Copy the bearer_token — it's shown once at creation. Lost it? Revoke and mint a new one.
Test with:
curl https://api.dzbuild.app/v1/whoami \
-H "Authorization: Bearer $DZ_KEY"
# expect: { "data": { "key_id": "...", "store_id": 13, "type": "platform", "scopes": [...] } }
Step 1 — render the catalog
// pages/index.js (Next.js)
export async function getServerSideProps() {
const res = await fetch('https://api.dzbuild.app/v1/products?limit=50&status=active', {
headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },
});
const { data } = await res.json();
return { props: { products: data.items } };
}export default function Home({ products }) {
return (
<ul>
{products.map(p => (
<li key={p.id}>
{p.primary_image && (
<img src={p.primary_image} alt={p.name} />
)}
<h2>{p.name}</h2>
<p>{p.price} DZD</p>
<a href={`/product/${p.slug}`}>View</a>
</li>
))}
</ul>
);
}
Two things about images:
primary_imageis a full CDN URL (for examplehttps://cdn.dzbuild.app/uploads/products/<store_id>/<file>). Put it in theimgtag as it is, without adding a prefix.The list item does not include
store_id. Read it once fromGET /v1/storeorGET /v1/whoamiand keep it in config.primary_imageisnullwhen a product has no image. Guard for it, as above.
Cache the response on your side: the products list is served fresh on every call, so repeated reads at scale cost a round trip each.
Step 2 — render a product detail page with variants
const res = await fetch(`https://api.dzbuild.app/v1/products/${id}`, {
headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },
});
const { data: product } = await res.json();
The response includes variants[] — each entry is a variant group with options:
"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 }
]
}
]
Render one picker per group. For type: color, render swatches with the color_code; for type: text, render labels; for type: dropdown, render a select; for type: image_text, render thumbnails: options[].image_id is a numeric id that matches an entry's images[].id in the same product response. A type: selectable group is optional and lets the customer pick several options. Each option also carries price_adjustment (the amount added to the product price, used in Step 3) and is_active; hide options where is_active is false.
images[].url is a full CDN URL, like primary_image in Step 1, so put it in an img tag as it is.
Out-of-stock handling: options[].stock is null if the merchant doesn't track per-variant stock. If it's a number ≤ 0, grey out that option. The API does not refuse an order for an out-of-stock option, so check stock in your UI before checkout. Per-combination stock is in combinations[] of the same response.
Step 3 — build a cart
Cart is client-side (React state, Vuex, Pinia, localStorage, …). You don't need an API call to add to cart. Each cart line is:
{
product_id: 26,
product_name: "T-shirt", // for display
base_price: 1500,
quantity: 1,
selected_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 }
]
}
Compute the line total client-side: (base_price + sum(price_adjustment)) × quantity. Show the customer the cart total, but treat it as display only:
Any price you send in the item is ignored (the API field is
price). The server always uses the catalog price forproduct_id.price_adjustmentis re-resolved from the catalog by the(group_name, option_name)pair. On a mismatch the catalog value wins. Only when the pair doesn't resolve at all does your value get used — which is exactly what happens after a merchant renames a variant option, so keep yourgroup_name/option_namestrings in sync with the catalog.group_nameandoption_nameare silently truncated to 100 characters, andcolor_codeto 7.
Step 4 — collect customer info + compute shipping
Standard checkout form: name, phone, wilaya, commune, address. Read the wilaya list once from GET /v1/wilayas (names in Arabic, French and English) and the communes of a wilaya from GET /v1/wilayas/{id}/communes. The list has 58 wilayas, or 69 on a store set to 69 wilayas, and that store's orders accept wilaya ids up to 69.
GET /v1/store is live and worth calling once at boot: it returns the store's name, slug, language, description, logo, favicon, banner, theme colours and font, subdomain, custom domain (and whether it's verified), public_url, hide_branding and created_at. It is served fresh on every call.
Shipping is not in GET /v1/store; it has its own endpoints: GET /v1/shipping/rates returns the merchant's home and desk price per wilaya, GET /v1/shipping/settings the free-shipping rules, and GET /v1/shipping/coverage?wilaya_id=16 the stop desks of the store's courier for that wilaya (see the stop-desk picker below). Use them to show the customer an estimate before checkout.
Do not send a shipping price: POST /v1/orders computes it from the merchant's rates for the wilaya and delivery type, applies free shipping and the weight surcharge, and ignores any shipping_cost in the body. If the chosen delivery type is turned off for that wilaya and the other one is on, the order switches to the one the merchant offers. pickup and digital orders carry no shipping charge. Show the customer amounts.shipping_cost and amounts.total from the reply.
Step 5 — submit the order
// On your back-end (Next.js API route, Edge function, server, …)
async function placeOrder(req, res) {
const cart = req.body; const order = {
customer: {
name: cart.name,
phone: cart.phone,
email: cart.email || null,
wilaya_id: cart.wilaya_id,
commune: cart.commune,
address: cart.address || ''
},
delivery: {
type: cart.delivery_type, // "home" | "desk" | "pickup" | "digital"
desk_id: cart.desk_id || null,
desk_name: cart.desk_name || null
},
items: cart.items.map(line => ({
product_id: line.product_id,
quantity: line.quantity,
variants: line.selected_variants
})),
// no shipping_cost: the server computes shipping from the merchant's rates
discount: 0,
payment_method: 'cod',
notes: cart.notes || null
}; const idempKey = req.headers['x-checkout-id'] || crypto.randomUUID(); const apiRes = await fetch('https://api.dzbuild.app/v1/orders', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DZ_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempKey
},
body: JSON.stringify(order)
}); if (!apiRes.ok) {
const err = await apiRes.json();
return res.status(apiRes.status).json(err);
}
const { data: createdOrder } = await apiRes.json(); // HTTP 200, not 201
return res.json({
order_number: createdOrder.order_number,
total: createdOrder.amounts.total
});
}
POST /v1/orders answers HTTP 200 on success, and the body is the full canonical order detail, the same shape GET /v1/orders/{id} returns. Branch on apiRes.ok, never on status === 201.
The customer sees their order_number on the success page. The merchant's dashboard now shows the new order in the pending list, ready to confirm.
Validation limits
Every rule below throws 400 bad_request with the message inline, so surface them in your checkout form rather than discovering them in production:
Field | Rule |
| 1–50 lines, non-empty array |
| Must belong to the key's store |
| 1–9999 |
| 1–255 characters |
| Must match |
| 1 to 58, or 1 to 69 on a store set to 69 wilayas |
| 1–100 characters |
|
|
|
|
| Must be |
| Truncated to 1000 characters (not an error) |
Plus the plan cap: on a Free-plan store, order 31 in a calendar month returns 400 bad_request "Monthly order limit reached for this store plan".
Step 6 — receive webhooks (optional but powerful)
Register a webhook so your storefront can react to order events:
curl -X POST 'https://api.dzbuild.app/v1/webhooks' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://yourstorefront.com/api/dz-webhook",
"events": ["order.created", "order.confirmed", "order.shipped", "order.delivered"]
}'
You'll receive a secret in the response (HTTP 200): store it. Every delivery is signed with it, so check X-DZ-Signature before you act on a delivery. See Verifying signatures for the recipe and code.
Two limits worth knowing before you build on this: API v1 order.created fires only for orders your storefront creates through the API (not for anything placed in the merchant's own DZBuild storefront), and status events fire only for status changes made through the API. See the Event catalog.
Use webhooks to:
Send the customer an SMS when their order is confirmed.
Update your CRM / Google Sheet / analytics.
Invalidate a "thank you page" once the merchant confirms.
Trigger downloadable-product delivery once
order.deliveredfires.
Common patterns
Multi-language storefront
Keep your translations in your front-end. The API returns product names + descriptions exactly as the merchant entered them. If the merchant uses the Multi-language add-on, GET /v1/products/{id} still returns only the canonical name and description; the add-on's translations are not part of the API response, so pick what your customer wants client-side.
Stop-desk picker
For delivery.type = "desk":
// 1. Read the stop desks of the store's courier for the chosen wilaya:
// GET /v1/shipping/coverage?wilaya_id=16 (scope shipping:read)
// desks[] holds desk_id, name, address, phone, commune_id;
// 422 no_courier_linked means the store has no courier linked// 2. Show the customer a list, let them pick:
selectedDesk = { id: 7842, name: "Yalidine Bab Ezzouar" };// 3. Pass to the order:
order.delivery = {
type: "desk",
desk_id: selectedDesk.id,
desk_name: selectedDesk.name
};
Online payment (SlickPay / Edahabia)
For payment_method = "digital_payment":
Submit the order via API as usual; it's created in
pendingwithpayment_status = pending.Redirect the customer to your SlickPay / Edahabia checkout URL.
On success, your back-end receives the payment provider's confirmation. You can then move the order on with
PATCH /v1/orders/{id}and{"status": "confirmed"}.payment_statuscannot be set through the API, and the store's own SlickPay setup only marks orders paid that were paid at the store's own checkout, so it does not mark an order created through the API as paid.
Returns & refunds
Currently handled in the dashboard. Through the API you can mark an order returned with PATCH /v1/orders/{id} and {"status": "returned"}, allowed from shipped or delivered. A refund endpoint is not available.
Security
Never put the API key in browser-facing code. Keys belong on your server / serverless function / edge worker. The browser calls your endpoint, your endpoint calls DZBuild.
HTTPS only between your storefront and
api.dzbuild.app. Plain HTTP is rejected.Idempotency-Key required on every
POST,PATCHandDELETE(optional onPUT). Without it you get400with codebad_requestand the message"Idempotency-Key header is required for write requests". The code isbad_request, notidempotency_key_required. The value must be ≤ 64 characters from[A-Za-z0-9_-:.];crypto.randomUUID()passes, but base64 and most hash encodings don't (+,/,=are rejected).Rate limits are counted per store: every key of the store shares one budget of 600 requests per minute, so extra keys do not raise it. Only the Enterprise plan has API access, and a store that leaves it gets
403until it is back on an active Enterprise plan. Note these limits apply only to API calls; the storefront you build serves your shoppers without consuming them. See Rate limits for the current caps.One key per environment. Don't share dev and prod keys. Mint a separate key for staging; revoke it when staging closes. (You cannot choose scopes, and a key keeps the scopes it was minted with, so separation comes from the key itself.)
Troubleshooting
Problem | Likely cause | Fix |
| Forgot the | Add it |
| Token has no dot — usually a truncated copy-paste that kept only the | Use the full 73-character bearer token |
| Typo, revoked or expired key, or wrong env var | Generate a new key at Settings → API |
| Key exists but DZBuild hasn't enrolled it in the pilot | Contact support |
| The store is not on Enterprise, or its Enterprise subscription expired | The merchant renews or upgrades to Enterprise; the same key works again |
| Cross-store id (you're using a key for store A but sending product from store B) | Use the correct key |
| Empty cart | Don't submit empty carts |
| Free-plan store hit its 30-orders-per-month cap | Merchant upgrades to Pro or above |
| You're polling too aggressively | Switch to webhooks; back off using the |
Example code
Everything you need is on this page — catalog listing, variant rendering, cart shape, order submission, webhook registration. Copy from the sections above; there are no separate starter repositories to clone.
Going live
Test thoroughly with a
pilotkey on a test store.Mint a separate key for production (same scopes, different name).
Deploy your storefront with the prod key in env vars.
Place a real test order; confirm it appears in the dashboard.
Place a refund test if you offer them.
Register your webhooks pointing to your production URL.
Monitor
GET /v1/usagedaily for the first week to spot anomalies.
Roadmap
Feature | Status |
Wilaya shipping rates and stop desks | Live: |
Per-combination stock | Live: |
| Not available |
Product image upload | Live: |
Multi-language fields on product responses | Not available |
If you need any of these sooner, contact support.