Skip to main content

For resellers

How to use the DZBuild API to resell stores, manage clients at scale, and integrate with your own back-office.

Written by Support

If you build storefronts for clients — agencies, freelancers, fulfillment companies, marketplace operators — the DZBuild API is designed to be your operational backbone.

This guide is for you if any of these apply:

  • You manage 5+ DZBuild stores for different clients.

  • You bill clients per-order or per-month and need a reliable usage signal.

  • You build custom themes or custom storefronts on behalf of clients.

  • You sell DZBuild as a white-label layer ("MyAgency Commerce, powered by DZBuild").

  • You want to script bulk operations: bulk-import products, bulk-update prices, bulk-confirm orders.

How resellers actually use the API

A typical reseller setup:

                                ┌─────────────────────────┐
                                │  Your back-office /     │
                                │  agency dashboard       │
                                │  (your code, your UI)   │
                                └──────────┬──────────────┘
                                           │  Bearer dzpk_live_…
                                           ▼
                            ┌─────────────────────────────┐
                            │  api.dzbuild.app/v1/*       │
                            └──────────┬──────────────────┘
                                       ▼
                           ┌──────────┬─────────┬──────────┐
                           │ Store A  │ Store B │ Store C  │   ← clients you manage
                           └──────────┴─────────┴──────────┘

Each client has their own DZBuild store (their merchant account, their plan, their orders). You hold an API key per store and orchestrate everything — onboarding, customizing, ordering reports — from your own dashboard.

Key model for resellers

There's no special "reseller key" type, and no cross-store key. Every reseller works with one platform key per client store, and POST /v1/keys is hard-scoped to the store the calling key already belongs to.

A) The client's first key is issued by DZBuild, then handed to you

Keys are created from the merchant dashboard at Settings → API (/dashboard/api) by the store owner (Enterprise plan, up to 3 active keys per store) — so onboarding a client means the client generates a key and hands it to you, or you generate it together. While the pilot lasts, DZBuild support can also issue and enrol keys. You store all client secrets in your reseller back-office, encrypted, and each operation against client X uses client X's key.

Pros: clear consent, client owns the key, can revoke any time. Cons: onboarding friction — a human step per store.

B) You self-mint additional keys for a store you already hold a key for

Once you have one key for a store, you can mint more for that same store without contacting anyone:

curl -X POST 'https://api.dzbuild.app/v1/keys' \
  -H "Authorization: Bearer $CLIENT_X_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"type":"platform","name":"agency-reporting"}'
# HTTP 200 → { "data": { "key_id", "bearer_token", "signing_secret", "note" } }

The new key inherits the caller's rate-limit tier and pilot flag. Useful for separating your reporting jobs from your write jobs on the same store.

What does not exist: there is no "agency umbrella" configuration, on any plan, that lets you mint keys for stores you don't already have a key for. Onboarding a new client store always starts with DZBuild issuing that store's first key.

Bulk product imports

Building a catalog for a client? Use a CSV → API loop:

import requests, csv, uuid, osKEY = os.environ['DZ_KEY_CLIENT_X']
HEADERS = {
    'Authorization': f'Bearer {KEY}',
    'Content-Type':  'application/json',
}with open('products.csv') as f:
    for row in csv.DictReader(f):
        body = {
            'name':           row['name'],
            'price':          float(row['price']),
            'compare_price':  float(row['compare_price']) if row['compare_price'] else None,
            'sku':            row['sku'],
            'description':    row['description'],
            'stock_quantity': int(row['stock']),
            'track_stock':    True,
            'status':         'active',
        }
        r = requests.post(
            'https://api.dzbuild.app/v1/products',
            headers={**HEADERS, 'Idempotency-Key': str(uuid.uuid4())},
            json=body,
        )
        if r.ok:                       # 200, NOT 201 — no v1 create returns 201
            print(f"OK {row['sku']} → id {r.json()['data']['id']}")
        else:
            print(f"FAIL {row['sku']}: {r.text}")

Two bugs to avoid in that loop, both of which the naive version has:

  • Don't test for 201. POST /v1/products returns 200; a == 201 check prints FAIL for every product it successfully created.

  • Don't use uuid4() for the idempotency key. A fresh key on every run means a re-run creates duplicates instead of replaying. Use a deterministic key such as import-{client}-{sku}-{run} — which is also what the tip below says, so keep the code and the tip consistent.

Tips:

  • Use a deterministic idempotency key (e.g. import-{client}-{sku}-{run}) so retries skip already-imported rows. Each key's response is cached for 24 hours, errors included — change the key after you fix a bad request.

  • Every client store you manage through the API must be on an active Enterprise plan (the API is Enterprise-only), which allows 600 req/min per store (shared across all its keys) — so ~1000 product creates still take a couple of minutes of wall clock. Pace at roughly half the cap to leave headroom; going over returns 429 rate_limited with a Retry-After header. Note that product images carry their own tighter budget — 10/min per store — so bulk imports with photos are paced by that, not by the general limit.

  • Image uploads go through the dashboard for now; the API will support presigned upload URLs in v1.1.

Bulk order operations

⚠️ Warning — limit=200 is the ceiling, not "all of them"

limit is clamped to 200. Both scripts below stop there and tell you nothing about what they missed. Every list response carries has_more and next_cursor — loop on them:

CURSOR=""
while :; do
  PAGE=$(curl -sS "https://api.dzbuild.app/v1/orders?status=pending&limit=200${CURSOR:+&cursor=$CURSOR}" \
    -H "Authorization: Bearer $KEY")
  echo "$PAGE" | jq -r '.data.items[].id'
  [ "$(echo "$PAGE" | jq -r '.data.has_more')" = "true" ] || break
  CURSOR=$(echo "$PAGE" | jq -r '.data.next_cursor')
done

Order reads are also never cached (only products, landing pages and store are), so a sweep across 50 client stores is 50 uncached round-trips, each one counting against that store's per-minute budget. Use ?since= windows rather than re-reading the whole backlog.

Confirm all pending orders for one client

KEY="$(cat /etc/secrets/client-x.key)"
# 1. List pending
curl -sS 'https://api.dzbuild.app/v1/orders?status=pending&limit=200' \
  -H "Authorization: Bearer $KEY" \
| jq -r '.data.items[].id' \
| while read OID; do
    # 2. Confirm each
    curl -sS -X PATCH "https://api.dzbuild.app/v1/orders/$OID" \
      -H "Authorization: Bearer $KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: confirm-$OID" \
      -d '{"status":"confirmed"}'
  done

Daily revenue report across all clients

clients = json.load(open('clients.json'))  # [{name, key}, ...]
today = date.today().isoformat() + 'T00:00:00Z'for c in clients:
    orders = requests.get(
        f'https://api.dzbuild.app/v1/orders?since={today}&limit=200',
        headers={'Authorization': f'Bearer {c["key"]}'},
    ).json()['data']['items']
    revenue = sum(o['total'] for o in orders if o['status'] == 'delivered')
    print(f"{c['name']}: {len(orders)} orders, {revenue} DZD")

White-labeling

You want your storefronts to feel like your brand, not DZBuild's. The API enables that:

  • Build the front-end yourself with whatever stack/branding you want (see Custom themes & storefronts).

  • Use your own domain (shop.yourbrand.com) — point it at your custom storefront, your storefront talks to DZBuild via API.

  • The merchant dashboard at dzbuild.com/dashboard stays DZBuild-branded — that's where you (or your client) confirm orders. Your end customers never see DZBuild branding.

There is no white-label merchant dashboard, on any plan. The one branding control that exists is hide_branding (Unlimited plan and up), which removes DZBuild branding from the merchant's storefront — not from the dashboard. Its current value is exposed on GET /v1/store. If you need something beyond that, talk to sales rather than planning around a product that doesn't exist yet.

Webhooks for resellers

Register one webhook per client store pointing to your reseller back-office:

curl -X POST 'https://api.dzbuild.app/v1/webhooks' \
  -H "Authorization: Bearer $CLIENT_X_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url":    "https://api.youragency.com/dz-hooks?store=client-x",
    "events": ["order.created","order.confirmed","order.shipped","order.delivered","order.cancelled"]
  }'

Use the ?store=client-x query parameter so a single endpoint can handle all clients — and keep the rest of the path unguessable, because API v1 signatures cannot currently be verified with the per-client secret (deliveries are not signed with it). Confirm anything important by re-reading GET /v1/orders/{id} with that client's key. See Verifying signatures.

Also plan for the coverage gap: API v1 order.created fires only for orders created through POST /v1/orders, and status events only for status changes made through the API. If your clients take orders on their DZBuild storefront and confirm them in the dashboard, you will see nothing here — either poll GET /v1/orders?since=... per store, or have each client enable the no-code Webhooks addon at /dashboard/webhooks (Unlimited plan and up), which covers every order source.

Multi-store accounts

Some clients run multiple stores in one DZBuild account (the Multi-store feature). Each store gets its own API key — they're independent. Keep them clearly named in your secrets manager:

DZ_KEY_CLIENT_X_BRAND_A=dzpk_live_...
DZ_KEY_CLIENT_X_BRAND_B=dzpk_live_...

Plan tiers — what to recommend

Every store you manage through the API must be on an active Enterprise plan. The API is Enterprise-only: on any other plan — or with an expired Enterprise subscription — every call returns 403 forbidden, no matter whose key it is, and new keys cannot be minted for the store at all.

For client stores you do not automate (they use the dashboard and storefront only), the other plans still apply as usual:

Client size

Recommended plan

Why

0–30 orders/month

Free

Validate the idea before committing — but the Free plan caps the store at 30 orders per calendar month, after which POST /v1/orders returns 400 bad_request "Monthly order limit reached for this store plan"

30–500 orders/month

Pro

Removes the monthly order cap

500–2000 orders/month

Unlimited

No order cap, plus hide_branding on the storefront

2000+ / multi-brand / any API automation

Enterprise

Custom contract, priority support — and the only plan with API access

ℹ️ Info — The plan gates API access

A store's subscription plan gates its API access: only stores on an active Enterprise plan can mint keys or make API calls (600 req/min per store, shared across its keys — see Rate limits). Downgrade a client, or let their Enterprise subscription expire, and their keys stop working immediately (403 forbidden); move them back to Enterprise and the same keys resume — nothing to re-issue. Pilot enrolment is still required while the pilot lasts — without it, every call returns 403 forbidden "API is in pilot mode; key not enrolled".

Billing your own clients

The API gives you accurate usage signals so you can bill confidently:

  • GET /v1/usage — current month's API call count, broken down by endpoint group

  • GET /v1/usage/history — hourly rows (period_hour, endpoint_group, count, billable_count) under data.rows. It defaults to the last 7 days and accepts a maximum 90-day window via ?from=&to=; anything wider returns 400 bad_request "range too large (max 90 days)". A full year means paging through five 90-day windows.

  • GET /v1/orders?status=delivered&since=... — for per-order commission models (remember the 200-row page cap and next_cursor)

Most resellers charge clients:

  • A flat monthly fee for storefront hosting + DZBuild license re-sale

  • A % commission on delivered orders (read from ?status=delivered)

  • Or a bundled subscription (e.g. "10000 DZD/month for everything")

Pick whichever model your market expects.

Operational tips

  • One environment per client. Dev secrets in dev. Prod secrets in prod. Never share keys across.

  • Use deterministic Idempotency-Keys for any retry loop (especially bulk imports / bulk confirms).

  • Cache product reads. GET /v1/products, GET /v1/landing-pages and GET /v1/store are cached for 30 s, and you can layer your own 5-min cache on top — products rarely change minute-to-minute. Order reads are never cached, so budget for a full round-trip on every one.

  • Rate-limit yourself. Don't spike to your tier's ceiling the moment you can — leave headroom for the merchant's organic traffic.

  • Set up Telegram alerts on your end for 429 rate_limited, 5xx, or webhook delivery failures.

  • Quarterly audit: rotate keys for stores you no longer manage. Use DELETE /v1/keys/{key_id}.

Reference

Did this answer your question?