This guide covers how to safely carry your DZBuild API credentials into your application — local dev, staging, production. Whether you're building a Node.js storefront, a Laravel/Symfony back-office, a Python data pipeline, a Go microservice, or a serverless edge function, the rules are the same.
Before you start
Your key must be pilot-enrolled. API v1 is pilot-gated in production. A key that DZBuild hasn't enrolled returns
403 forbidden"API is in pilot mode; key not enrolled"on every call, no matter how correct your setup is.Always point
DZBUILD_API_BASEathttps://api.dzbuild.app/v1.dzbuild.com/api/v1/...is only an alias for the same API: some request paths can be blocked there, and it does not get the 30-second read cache. Per-key rate limits and idempotency replay apply on both hosts.
What you need to store
For a typical storefront / back-office integration:
Variable | Example | Notes |
|
| The full bearer token. Treat as a password. |
|
| Base URL. Use this host in prod, never |
| 64-char lowercase hex | Per-webhook secret returned at registration. No prefix. See Webhook secrets for what it can and can't do today. |
Anatomy of the bearer token
Merchants truncate this constantly, so it's worth spelling out. A platform key's bearer token is:
dzpk_live_<14 hex>.<48 hex>
73 characters, all lowercase after the prefix, with one dot in the middle. The dzpk_live_… part before the dot is the key id — an identifier, not a credential. Authorization: Bearer needs the whole 73-character string.
Get this wrong and the two layers disagree, which is a useful diagnostic:
A token with no dot at all →
401 unauthorized "Invalid bearer format".A well-formed but unknown / revoked token →
401 unauthorized "Invalid or revoked API key".
For public-key flow (signups / events from a public client):
Variable | Example | Notes |
|
| Safe to ship in client code (the id is public, the secret is not) |
| 64-char lowercase hex, no prefix | Server-side only — used to HMAC-sign each request body |
.env files
The simplest pattern:
# .env (in your project root, gitignored) DZBUILD_API_KEY=dzpk_live_3f9c1b7a4e02d5.4d1c8a90f7b23e6510ac7fd9b48e2c31a05f6d7e8b9c0a1d DZBUILD_API_BASE=https://api.dzbuild.app/v1 DZBUILD_WEBHOOK_SECRET=fc9b5f0b51b4a93c1d6f8e29b6a2e30c7c2c44a4f2a6c8d8e0e1b9d4f6c1a8b3
# .gitignore .env .env.local .env.*.local
Commit a .env.example file with placeholder values so other developers know which vars to set:
# .env.example (committed) DZBUILD_API_KEY=dzpk_live_REPLACE_ME DZBUILD_API_BASE=https://api.dzbuild.app/v1 DZBUILD_WEBHOOK_SECRET=REPLACE_ME_64_HEX
Loading .env per language
Node.js / Next.js / Express
// next.config.js — Next.js auto-loads .env, .env.local, .env.production // For plain Node: import 'dotenv/config'; const key = process.env.DZBUILD_API_KEY;
Python
# pip install python-dotenv from dotenv import load_dotenv import os load_dotenv() key = os.environ['DZBUILD_API_KEY']
PHP / Laravel
// Laravel auto-loads .env
$key = env('DZBUILD_API_KEY');// Plain PHP:
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$key = $_ENV['DZBUILD_API_KEY'];
Go
// go get github.com/joho/godotenv
godotenv.Load()
key := os.Getenv("DZBUILD_API_KEY")
Flutter / mobile
Don't ship the platform key in your mobile app. Instead:
Your mobile app calls your back-end.
Your back-end holds the API key and proxies requests to DZBuild.
If you need the mobile client to talk directly to DZBuild (e.g. for signups), use the public-key flow — only the public key id ships, never the platform secret. See Public-key endpoints.
Production environments
In production, don't use a .env file. Use the platform's native secret store:
Vercel / Netlify / Cloudflare Pages
Project → Settings → Environment Variables DZBUILD_API_KEY = dzpk_live_... DZBUILD_API_BASE = https://api.dzbuild.app/v1 DZBUILD_WEBHOOK_SECRET = <64-char lowercase hex>
Mark prod-only vars as "Production" scope. Mark staging vars as "Preview" or a separate environment.
Cloudflare Workers
wrangler secret put DZBUILD_API_KEY # (paste the value when prompted)
Available in the worker as env.DZBUILD_API_KEY (with [vars] declared in wrangler.toml).
AWS Lambda / API Gateway
Use AWS Secrets Manager or Parameter Store:
import boto3, json
secret = json.loads(
boto3.client('secretsmanager').get_secret_value(SecretId='dzbuild/prod')['SecretString']
)
key = secret['DZBUILD_API_KEY']
Don't store the key in Lambda env vars in plaintext (they appear in CloudTrail logs). Reference the secret manager.
Docker / Docker Compose
# docker-compose.yml
services:
app:
image: yourapp:latest
env_file:
- .env.production # NOT committed
environment:
- NODE_ENV=production
For Kubernetes, use a Secret:
apiVersion: v1 kind: Secret metadata: name: dzbuild-creds type: Opaque stringData: DZBUILD_API_KEY: dzpk_live_... DZBUILD_WEBHOOK_SECRET: <64-char lowercase hex>
Then reference in the deployment:
envFrom:
- secretRef:
name: dzbuild-creds
GitHub Actions
# .github/workflows/deploy.yml
env:
DZBUILD_API_KEY: ${{ secrets.DZBUILD_API_KEY }}
Set the secret in Repo → Settings → Secrets and variables → Actions.
Development vs production keys
Always mint two separate keys:
One named
devorstaging— used in.env.local/ dev environmentsOne named
production— used only in your real prod deployment
If your dev key leaks, you only burn the dev key. Production data stays intact.
How you actually get a key
There is no "Developer → API Keys" page in the merchant dashboard. During the pilot:
Your first key is issued by DZBuild on request — contact support or your account manager. Ask for a key named after the environment it will live in (
myapp-dev,myapp-prod), and for it to be enrolled in the pilot.Further keys you can mint yourself from an existing key:
bash curl -X POST 'https://api.dzbuild.app/v1/keys' \ -H "Authorization: Bearer $DZBUILD_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"type":"platform","name":"myapp-dev"}' # HTTP 200 → { "data": { "key_id", "bearer_token", "signing_secret", "note" } }
The new key inherits the calling key's rate-limit tier and pilot flag verbatim.
Scopes are not selectable. POST /v1/keys accepts only type and name. A platform key always gets the full default set — store:read, store:write, products:read, products:write, orders:read, orders:write, customers:read, landing_pages:read, landing_pages:write, webhooks:read, webhooks:write, usage:read. A public key always gets signups:write and events:write. Separation between dev and prod comes from using two different keys, not from narrowing permissions.
Webhook secrets
When you register a webhook, the response includes a secret:
curl -X POST 'https://api.dzbuild.app/v1/webhooks' \
-H "Authorization: Bearer $DZBUILD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://yourapp.com/webhook",
"events": ["order.created", "order.confirmed"]
}'
Response — HTTP 200, and only three fields:
{
"data": {
"id": 42,
"secret": "fc9b5f0b51b4a93c1d6f8e29b6a2e30c7c2c44a4f2a6c8d8e0e1b9d4f6c1a8b3",
"note": "Save the secret now — it is not retrievable after this response."
},
"meta": { "request_id": "...", "api_version": "v1" }
}
The secret is a bare 64-character lowercase hex string — there is no dzwh_sec_ prefix. It's shown once; save it immediately to your secret store. If you lose it, delete the webhook and register a new one.
⚠️ Warning — You can't verify API v1 deliveries with this secret yet
API v1 does not sign deliveries with the per-webhook secret above, so HMAC code written against DZBUILD_WEBHOOK_SECRET rejects every genuine delivery. Store the secret for later, but authenticate v1 deliveries by other means — and re-read the record via the API before acting on it.
The full explanation, plus verification code that does work (for the merchant Webhooks addon), lives in Verifying signatures — one source of truth, in 4 languages.
Public-key flow (signups / events)
The public-key flow exists for narrow-scope endpoints (signup tracking, event tracking) where you don't want a platform key in the loop. Only the public key id is non-secret — the signing secret stays on your back-end.
⚠️ Warning — Don't call /v1/signups straight from a browser
Three things break a direct browser call today:
No CORS. The preflight succeeds, but the actual POST response carries no
Access-Control-Allow-Origin, so the browser discards it.Idempotency-Keyis mandatory on every POST, and it's easy to forget in client code.crypto.randomUUID()is not a valid nonce. The nonce must be exactly 32 lowercase hex characters; a dashed 36-character UUID returns401 unauthorized "Invalid nonce format".
Proxy through your own back-end instead — the pattern below.
// Browser: talk to YOUR endpoint, never to api.dzbuild.app
await fetch('/api/track-signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, external_user_id: externalId })
});
// Your back-end (Node): holds the signing secret, signs and forwards.
import crypto from 'node:crypto';const KEY_ID = process.env.DZBUILD_PUBLIC_KEY_ID;
const SECRET = process.env.DZBUILD_SIGNING_SECRET;export async function trackSignup({ email, external_user_id }) {
const nonce = crypto.randomBytes(16).toString('hex'); // 32 lowercase hex
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({ email, external_user_id, source: 'web', nonce });
const hash = crypto.createHash('sha256').update(body).digest('hex');
const sig = crypto.createHmac('sha256', SECRET)
.update(`${KEY_ID}\n${nonce}\n${ts}\n${hash}`).digest('hex'); await fetch('https://api.dzbuild.app/v1/signups', {
method: 'POST',
headers: {
'Authorization': `DZ-Public ${KEY_ID}`,
'X-DZ-Timestamp': ts,
'X-DZ-Nonce': nonce,
'X-DZ-Signature': sig,
'Idempotency-Key': nonce,
'Content-Type': 'application/json'
},
body
});
}
The signing secret never leaves your back-end. The public key id can be inspected by anyone — that's by design. See Signups for the full contract.
Local-dev tunneling for webhooks
DZBuild webhooks need a public HTTPS URL. To test locally, use a tunnel:
# ngrok ngrok http 3000# Cloudflare Tunnel cloudflared tunnel --url http://localhost:3000# tailscale serve tailscale serve https / http://localhost:3000
Register the tunnel URL as your webhook target. Update it whenever the tunnel restarts (ngrok free changes URL each time; pay $8/mo for a stable subdomain).
Rotation policy
Rotate keys:
Quarterly for production keys (set a calendar reminder)
Immediately if a key may have leaked (committed to git, screenshotted, sent in chat)
When a team member leaves if they had access to the secret store
Rotation procedure:
Mint a new key with
POST /v1/keys(see How you actually get a key). It inherits the old key's tier and pilot flag.Update your secret store / env vars to the new key.
Deploy. Verify traffic on the new key via
GET /v1/usage.Once you confirm 24h of clean traffic on the new key, revoke the old one:
DELETE /v1/keys/{old_key_id}.
Common mistakes
Mistake | What happens | Fix |
Committed | Key is now in git history forever; rotate immediately |
|
Used the platform key in browser code | Customers can read your network tab → see and steal the key | Move to back-end / serverless function; rotate the leaked key |
Hardcoded | Same as above | Use env vars; rotate |
Same key for dev + prod | A dev mistake hits prod data | Mint two keys; never share |
Lost webhook secret | Can no longer verify deliveries | Delete the webhook, register a new one |
Logged the API key in app logs | Auditors / log shippers / anyone with log access sees it | Redact secrets in your logger config; rotate |
Quick checklist before going live
[ ]
.envis in.gitignore(and was never accidentally committed)[ ] Production key is named
prodand used only in prod[ ] Dev key is named
devand only in dev/staging[ ] Webhook secrets stored in secret manager, not in code
[ ]
Authorizationheader is on every API call from your back-end[ ] Browser code never sees
dzpk_live_*(onlydzpub_live_*if you use public-key flow)[ ] Your webhook endpoint is on an unguessable URL and re-reads records via the API before acting (API v1 signatures aren't verifiable yet)
[ ] You have a key rotation reminder on your calendar
[ ] You have logging that does NOT capture full request bodies (could include API keys in client headers)