A general-purpose event tracker. Use it for anything you want analytics on that isn't a signup:
Page views
Lead form submissions
Click-tracking on landing pages
Custom merchant-defined events
If your data has a meaningful user identity (an email address), use /v1/signups instead: it counts each email address once per store. Phone numbers are not deduplicated there. /v1/events is for higher-volume, identity-less data.
Auth
Public key + HMAC. Same scheme as /v1/signups. Your backend signs every call.
⚠️ Warning — Two things that block a brand-new key
Pilot gate. The API 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.Activation delay. A freshly issued public key is not usable the instant it is created. Until it is activated for these endpoints you get
401 unauthorized"Invalid or revoked public key". Contact support right after creating one to have it activated.
Revoking a public key does not stop it on its own: signed calls keep being accepted until support disables it. Tell support whenever you revoke a public key.
Body
{
"name": "lead_submitted",
"properties": { "plan": "pro", "country": "DZ", "form": "footer" },
"nonce": "32-hex-single-use"
}
Field | Type | Required | Notes |
| string ≤ 64 bytes | ✅ | Event name. |
| object | JSON-serializable values. Stored as-is. | |
| 32-hex string | ✅ | Use the same value as the |
POST requests must also carry an Idempotency-Key header (≤ 64 characters, charset [A-Za-z0-9_-:.]). Without it you get 400 bad_request and the event is never queued. The 32-hex nonce you already generate is a valid value — reuse it.
name is stored with each event, but no API endpoint or dashboard screen lists stored events: what you can read back, with a platform key, is the monthly count in GET /v1/usage (usage.event.total). Keep names to a small, stable vocabulary anyway, and avoid generating them dynamically (e.g., viewed_product_42 is bad; use name="viewed_product" with properties.product_id=42).
Response 202
{
"data": { "status": "queued", "kind": "event", "store_id": 13 },
"meta": { "request_id": "...", "api_version": "v1", "edge": true }
}
Same flow as signups: accepted in ~30 ms, stored within about 5 s.
Dedup
Dedup is on the nonce alone, per store. There's no email-based dedup.
A duplicate is dropped entirely: nothing is stored, no usage counter moves, and nothing in the API tells you it happened. The call still returned 202, because dedup happens after acceptance. So the duplicate is invisible to you — always generate a fresh nonce.
The nonce is guarded in two stages, with different lifetimes:
For 1 hour a reused nonce is rejected outright with
401 unauthorized"Nonce reused".Forever after that, a recycled nonce is accepted with
202and then silently discarded — you get no error, and no event.
Worked example: tracking page views (Node.js, server-side)
import crypto from 'node:crypto';export async function trackEvent(name, properties = {}) {
const KEY_ID = process.env.DZ_PUBLIC_KEY;
const SECRET = process.env.DZ_SIGNING_SECRET;
const nonce = crypto.randomBytes(16).toString('hex');
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({ name, properties, 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/events', {
method: 'POST',
headers: {
'Authorization': `DZ-Public ${KEY_ID}`,
'X-DZ-Timestamp': ts, 'X-DZ-Nonce': nonce, 'X-DZ-Signature': sig,
'Idempotency-Key': nonce, // required on every POST
'Content-Type': 'application/json',
},
body,
});
}// Inside Express middleware:
app.use((req, res, next) => {
trackEvent('page_view', { path: req.path, ua: req.get('user-agent') }).catch(() => {});
next();
});
Notice we don't await the call from the page-view middleware — fire-and-forget so the user's request isn't blocked. The API returns in ~30 ms anyway, but this protects against transient network issues.
What counts toward your quota
Every /v1/events call that actually lands increments usage.event.total AND usage.event.billable. There's no "free reads then billable writes" tier here — events are billable from request 1. Duplicates (same nonce) move neither counter, because they are never stored.
If event volume gets high, batch on your side: store events in your own queue, fire one /v1/events call per logical event in batches of 1 (the API does not accept batches). Every call counts toward the store's per-minute limit, shared with all of its keys.
Errors
Same as /v1/signups. See Errors. The three that catch people out on this endpoint:
HTTP | Code / Message | Cause |
400 |
| Header missing |
400 |
| Too long, or uses characters outside that set (base64 |
403 |
| Key not pilot-enrolled |
When NOT to use /v1/events
For storefront orders — the merchant's dashboard already records them, and duplicating them here just inflates your event count. Note that storefront orders do not fire API v1
order.createdwebhooks; only orders created throughPOST /v1/ordersdo (see the Event catalog).For server-internal events that don't relate to merchant data (CPU usage, cache hits). Use a real APM.
For massive volumes (>1M events / day). Build your own analytics pipeline and only export aggregates here.