Custom themes & storefronts
DZBuild ships 5 themes (Brico, Digital, Prestige, Showcase, Starter) 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 on Pro plan or higher (Free plan doesn't include API access — see Plans).
- An API key with the right scopes:
products:read— to render the catalogorders:read— to look up orders the customer placed (for status/tracking pages)orders:write— to create orders from the storefront
- A back-end (or serverless function / edge worker) that holds the API key. Never ship the secret to the browser — see Security.
Step 0 — get a key
In the merchant dashboard:
Dashboard → Developer → API Keys → "+ Create platform key"
• Name: "Custom storefront — production"
• Scopes: products:read, orders:read, orders:write
• Save
Copy the secret (dzpk_live_…) — 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 dzpk_live_..."
# 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}>
<img src={`https://cdn.dzbuild.app/${p.store_id}/products/${p.primary_image}`} />
<h2>{p.name}</h2>
<p>{p.price} DZD</p>
<a href={`/product/${p.slug}`}>View</a>
</li>
))}
</ul>
);
}
Cache the response — products list is edge-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 (the image filename is on options[].image_id — fetch the matching images[].url from the same product response).
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. (The server will recompute on submit — never trust client math for the final charge.)
Step 4 — collect customer info + compute shipping
Standard checkout form: name, phone, wilaya, commune, address. Use the wilaya list from GET /v1/store (coming) or hardcode the 58 wilayas — they're a stable list.
For shipping cost, you have two options:
- Hardcode rates per wilaya + delivery type in your storefront (simplest).
- Read from the merchant's store config via
GET /v1/store(planned for v1.1) and use those rates.
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();
return res.status(201).json({
order_number: createdOrder.order_number,
total: createdOrder.amounts.total
});
}
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.
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 — store it. Every webhook delivery includes an X-DZ-Signature header you must verify. See Verifying signatures.
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 dashboard's existing SlickPay webhook receiver).
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. The Worker rejects HTTP. - Idempotency-Key required on all writes. If you POST without it, you'll get
400 idempotency_key_required. - Rate limits apply per API key —
prois 1500 req/min,pro_plusis 5000,enterpriseis unlimited. See Rate limits. - One key per environment. Don't share dev and prod keys. Mint a separate key for staging with the same scopes; revoke it when staging closes.
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
401 unauthorized "Missing Authorization header" | Forgot the Authorization: Bearer … header | Add it |
401 unauthorized "Invalid API key" | Typo, revoked key, or wrong env var | Re-mint via dashboard |
403 forbidden "Missing scope: orders:write" | Key lacks the scope | Re-mint with the scope |
400 bad_request "Product N does not belong to this store" | Cross-store id (you're using a key for store A but sending product from store B) | Use the correct key |
400 bad_request "items must be a non-empty array" | Empty cart | Don't submit empty carts |
429 rate_limited | You're polling too aggressively | Switch to webhooks; or upgrade plan |
Reference apps
We maintain example apps you can clone:
dzbuild-storefront-nextjs— Next.js 14 + App Router + Tailwind. Demonstrates products listing, product detail with variants, cart, checkout.dzbuild-storefront-flutter— Flutter mobile app pulling from the same API.
(Both are starter templates — adapt freely.)
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 |
|---|---|
GET /v1/store with wilaya rates + stop desks | v1.1 |
GET /v1/products/{id}/combinations for per-combination stock | v1.1 |
POST /v1/orders/{id}/refund for refunds via API | v1.1 |
| Per-product image upload via R2 presigned URL | v1.1 |
| Multi-language fields on product responses | v1.1 |
If you need any of these sooner, contact support.