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 merchant account. The store's subscription plan has no bearing on API access — upgrading doesn't grant it, and an expired plan doesn't revoke it.
A pilot-enrolled API key. This is the real gate: while the API is in pilot, DZBuild must enrol the key. Without enrolment every call returns
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.
One place where the merchant's plan does matter: 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
There is no "Developer → API Keys" page in the merchant dashboard. During the pilot:
Ask DZBuild support to issue the store's first key and enrol it in the pilot.
Mint any further keys yourself from that one:
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 platform key always carries the full default set (products:read, products:write, orders:read, orders:write, store:read, store:write, customers:read, landing_pages:read, landing_pages:write, webhooks:read, webhooks:write, usage:read), 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={`https://cdn.dzbuild.app/${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 storage path (usuallyuploads/products/<store_id>/<file>), so the CDN URL is justhttps://cdn.dzbuild.app/+ that path — do not insert a/{store_id}/products/segment of your own. Older products can carry a bare filename instead; those resolve athttps://cdn.dzbuild.app/uploads/products/<store_id>/<filename>.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 — the products list is cached for 30 s, so repeated reads at scale are cheap.
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: image_text, render thumbnails: options[].image_id is a numeric id that matches an entry's images[].id in the same product response.
Careful with the field name — images[].url is a storage path, not a URL. Prefix it with https://cdn.dzbuild.app/ exactly like primary_image in Step 1 before putting it in an img tag.
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. Real-time availability is also enforced server-side at confirm time, so a stale UI is fine.
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. Hardcode the 58 wilayas — they're a stable list.
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's one of the cached GETs (30 s).
What it does not carry — and this is the real v1.1 item — is shipping rates per wilaya and the merchant's stop-desk list. So for shipping cost today:
Hardcode rates per wilaya + delivery type in your storefront (simplest), or
Keep them in your own config synced with the merchant out-of-band.
Pass the computed shipping cost to the order API as shipping_cost. The server doesn't currently re-compute shipping for you; whatever you pass becomes part of the order total.
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" | "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
})),
shipping_cost: cart.shipping_cost,
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 (no v1 create endpoint returns 201), 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–58 |
| 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. Note that API v1 deliveries are not signed with that secret, so you cannot verify them yet: keep the webhook URL unguessable and re-read the order with GET /v1/orders/{id} before acting. See Verifying signatures for the details.
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} will (in v1.1) return name_ar, name_fr, description_ar, description_fr alongside the canonical fields. Until then, only the canonical name/description is exposed — pick what your customer wants client-side.
Stop-desk picker
For delivery.type = "desk":
// 1. Read merchant's stop desks for the chosen wilaya
// (planned for v1.1: GET /v1/store/desks?wilaya=16)
// Until then, query the courier directly (Yalidine, ZR, etc.)// 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 a SlickPay webhook → call
PATCH /v1/orders/{id}to flippayment_statustopaid(planned for v1.1; until then, payment status is updated by the store's existing SlickPay integration).
Returns & refunds
Currently handled in the dashboard. API support for POST /v1/orders/{id}/refund is on the roadmap.
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 all writes. Without it you get
400with 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 API key and the ceiling follows the store's current plan — upgrades and downgrades apply immediately, with no key change needed. Nothing is unlimited per minute. 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. (Scopes are identical on every platform key — 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 key, or wrong env var | Mint a new key with |
| Key exists but DZBuild hasn't enrolled it in the pilot | Contact support |
| 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 | ETA |
Wilaya shipping rates + stop-desk list on | v1.1 |
| v1.1 |
| v1.1 |
Per-product image upload via presigned URL | v1.1 |
Multi-language fields on product responses | v1.1 |
If you need any of these sooner, contact support.