Skip to main content

Events

POST /v1/events — generic merchant-tracked events (page views, leads, custom analytics). Same HMAC scheme as signups, no email/phone dedup.

Written by Support

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 (email/phone), use /v1/signups instead — that gives you per-user dedup. /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 if a new key is still rejected after a short wait.

Revocation is not instant either: after a public key is revoked, calls can still be accepted for a short window. If you need a key stopped immediately, ask support.

Body

{
  "name":       "lead_submitted",
  "properties": { "plan": "pro", "country": "DZ", "form": "footer" },
  "nonce":      "32-hex-single-use"
}

Field

Type

Required

Notes

name

string ≤ 64 chars

Event name. snake_case recommended.

properties

object

JSON-serializable values. Stored as-is.

nonce

32-hex string

Single-use, forever. See Dedup — the 1-hour rejection window is only the outer guard; a nonce is never usable twice.

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 what you filter on later, so stick to a small, stable vocabulary — avoid generating names 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 202 and 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 (we don't accept batches in v1; that's a v1.1 feature for true high-volume telemetry).

Errors

Same as /v1/signups. See Errors. The three that catch people out on this endpoint:

HTTP

Code / Message

Cause

400

bad_request "Idempotency-Key header is required for write requests"

Header missing

400

bad_request "Idempotency-Key must be <=64 chars, [A-Za-z0-9_-:.]"

Too long, or uses characters outside that set (base64 +, /, = are rejected)

403

forbidden "API is in pilot mode; key not enrolled"

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.created webhooks; only orders created through POST /v1/orders do (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.

Did this answer your question?