Skip to main content

Authentication

Learn how to authenticate API requests with platform keys and public keys, including Bearer tokens and HMAC signatures for secure DZBuild API access.

Written by Support

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_secret form. Send it as it is, Authorization: Bearer <bearer token>, without adding anything to it.

  • Signing secret: never goes in the Authorization header, 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.

Did this answer your question?