Two key types.
Platform key (server-to-server, full CRUD)
Use a Bearer token from your backend:
Authorization: Bearer <key_id>.<key_secret>
The format is key_id.key_secret, both halves are required. key_id is the 24-character identifier that already begins with dzpk_live_ (for example dzpk_live_xxxxxxxxxxxxxx.<48-hex secret>), so don't add the prefix again. The key secret is 48 hex characters and is shown once at creation, never again.
Public key (external sites & apps; HMAC-signed)
For counting end-user signups and custom events from a site or app you run outside DZBuild. Your backend calls POST /v1/signups / POST /v1/events and signs each request — never expose the signing secret to a browser.
Headers:
Authorization: DZ-Public <key_id> X-DZ-Timestamp: <unix_seconds> X-DZ-Nonce: <32-hex> X-DZ-Signature: hex(hmac_sha256(signing_secret, key_id + "\n" + nonce + "\n" + ts + "\n" + sha256(body)))
Here too, key_id already begins with dzpub_live_ — pass it exactly as returned. The signing secret is 64 hex characters.
The nonce is single-use per key for one hour and prevents replay. The timestamp must be within ±5 minutes.
⚠️ Warning — A new public key is inert until it is activated
A type: public key you create through POST /v1/keys returns 401 until it has been activated for API traffic. Contact support right after creating one to have it activated. Platform (Bearer) keys are not affected — they work immediately.
Scopes
Every key has a list of scopes. Default platform scopes: 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. A key created in the dashboard gets exactly this list. delivery:send and ai:generate are not in it, so a key created in the dashboard or through POST /v1/keys gets 403 on POST /v1/orders/{id}/send-to-delivery and POST /v1/landing-pages/generate.
Default public scopes: signups:write, events:write.
Scopes are checked on reads as well as writes: a key without orders:read gets 403 with Missing scope: orders:read on GET /v1/orders. store:write covers the store settings, design, theme and home page writes. signups:write and events:write cover POST /v1/signups and POST /v1/events.
Key management (/v1/keys) is gated by key type, not by scope: a public key calling it gets 403 — "Key management requires a platform key".
A platform key minted through POST /v1/keys gets only the scopes the calling key already holds (403 when they share none). A type: public key always gets signups:write and events:write.
Creating a key
The owner of a store on an active Enterprise plan creates personal keys in the dashboard at Settings → API (/dashboard/api). A new key is enrolled in the pilot automatically and works at once: there is nothing to ask support for. The page shows two values, once, when the key is created:
Bearer token: the whole credential, already in the
key_id.key_secretform. Send it as it is,Authorization: Bearer <bearer token>, without adding anything to it.Signing secret: never goes in the
Authorizationheader, and Bearer calls don't use it. Keep it private like the token.
A store holds at most 3 active personal keys. Keys held by installed apps, Copilot or a connector don't take one of those slots.
Assistant connections
A connection made for Copilot or a connected AI assistant (Claude or ChatGPT) can cover several of the merchant's stores, with one key per store. Two endpoints let a key held by such a connection read which stores it covers and change which one is active. Any other key, including one created in the dashboard, gets 404 not_found with This key does not belong to an assistant connection.
GET /v1/connection
Needs store:read.
curl https://api.dzbuild.app/v1/connection \ -H "Authorization: Bearer $DZ_KEY"
{ "data": { "connection_id": 100, "client_name": "Claude", "active_store_id": 10,
"stores": [ { "id": 10, "name": "My store", "slug": "my-store" },
{ "id": 20, "name": "My second store", "slug": "my-second-store" } ] },
"meta": { "request_id": "...", "api_version": "v1" } }
client_name is the name of the assistant app. A connection approved before connections could cover several stores lists its one store.
POST /v1/connection/active-store
Needs store:write and an Idempotency-Key. store_id in the body names the store to make active.
curl -X POST https://api.dzbuild.app/v1/connection/active-store \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: active-store-20" \
-d '{"store_id": 20}'
The answer is the connection after the move, in the same shape as GET /v1/connection. The active store is a pointer, not a permission: every request still acts on the one store its key belongs to. A store outside the connection answers 403 store_not_in_connection, and a body without store_id answers 400 bad_request.