Skip to main content

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:

  1. Render the catalog (products, categories, variants)
  2. Build a cart (client-side or server-side, your call)
  3. Submit the order via API
  4. 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 catalog
    • orders: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.delivered fires.

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

  1. Submit the order via API as usual; it's created in pending with payment_status = pending.
  2. Redirect the customer to your SlickPay / Edahabia checkout URL.
  3. On success, your back-end receives a SlickPay webhook → call PATCH /v1/orders/{id} to flip payment_status to paid (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 — pro is 1500 req/min, pro_plus is 5000, enterprise is 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

ProblemLikely causeFix
401 unauthorized "Missing Authorization header"Forgot the Authorization: Bearer … headerAdd it
401 unauthorized "Invalid API key"Typo, revoked key, or wrong env varRe-mint via dashboard
403 forbidden "Missing scope: orders:write"Key lacks the scopeRe-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 cartDon't submit empty carts
429 rate_limitedYou're polling too aggressivelySwitch 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

  1. Test thoroughly with a pilot key on a test store.
  2. Mint a separate key for production (same scopes, different name).
  3. Deploy your storefront with the prod key in env vars.
  4. Place a real test order; confirm it appears in the dashboard.
  5. Place a refund test if you offer them.
  6. Register your webhooks pointing to your production URL.
  7. Monitor GET /v1/usage daily for the first week to spot anomalies.

Roadmap

FeatureETA
GET /v1/store with wilaya rates + stop desksv1.1
GET /v1/products/{id}/combinations for per-combination stockv1.1
POST /v1/orders/{id}/refund for refunds via APIv1.1
Per-product image upload via R2 presigned URLv1.1
Multi-language fields on product responsesv1.1

If you need any of these sooner, contact support.