Skip to main content

Environment & .env setup

How to safely store DZBuild API credentials in your application — from .env files to KMS to GitHub Secrets.

Written by Support

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_BASE at https://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

DZBUILD_API_KEY

dzpk_live_3f9c1b7a4e02d5.<48 hex>

The full bearer token. Treat as a password.

DZBUILD_API_BASE

https://api.dzbuild.app/v1

Base URL. Use this host in prod, never dzbuild.com/api/v1 (it's only an alias).

DZBUILD_WEBHOOK_SECRET

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 all401 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

DZBUILD_PUBLIC_KEY_ID

dzpub_live_...

Safe to ship in client code (the id is public, the secret is not)

DZBUILD_SIGNING_SECRET

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:

  1. Your mobile app calls your back-end.

  2. 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 dev or staging — used in .env.local / dev environments

  • One 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:

  1. 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.

  2. 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-Key is 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 returns 401 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:

  1. 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.

  2. Update your secret store / env vars to the new key.

  3. Deploy. Verify traffic on the new key via GET /v1/usage.

  4. 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 .env to git

Key is now in git history forever; rotate immediately

git filter-repo won't fix forks/clones; assume compromised

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 dzpk_live_... in source

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

  • [ ] .env is in .gitignore (and was never accidentally committed)

  • [ ] Production key is named prod and used only in prod

  • [ ] Dev key is named dev and only in dev/staging

  • [ ] Webhook secrets stored in secret manager, not in code

  • [ ] Authorization header is on every API call from your back-end

  • [ ] Browser code never sees dzpk_live_* (only dzpub_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)

Did this answer your question?