POST /v1/signups
This is the headline metered unit of the DZBuild API. Every signup that comes through here counts against your tier's monthly quota and contributes to platform pricing for partner integrations.
It's designed for one specific use case: you have an external website / app that accepts user signups, and you want each one of those to count for your DZBuild merchant account. Examples:
- A WordPress site you run for marketing → user fills your signup form → you call
/v1/signups. - A mobile app where users register → app's backend calls
/v1/signups. - A landing page on a different domain → backend calls
/v1/signups.
It is not designed for storefront orders (those create customers via the storefront's own flow) or for one-off lead-magnet signups (use /v1/events for those).
Auth
Public key + HMAC. Your backend signs every call. See Authentication for the full HMAC scheme.
Body
{
"email": "user@example.com",
"phone": "+213555000000",
"external_user_id": "u_42",
"source": "landing-page-1",
"country": "DZ",
"ip": "203.0.113.42",
"meta": { "campaign": "spring-2026" },
"nonce": "32-hex-single-use"
}
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | one of email/phone/external_user_id | Used for dedup. Stored as sha256(lowercase) only. |
phone | string | Stored as sha256(value) only. | |
external_user_id | string ≤ 190 | Your own id for the user. Useful if you don't collect email/phone. | |
source | string ≤ 64 | Free-form label (page slug, campaign, etc.) | |
country | string (2 chars) | ISO 3166-1 alpha-2. We uppercase it. | |
ip | string | Stored as sha256(value) for fraud detection. Don't include unless meaningful. | |
meta | object | Anything else. Will be JSON-stored. | |
nonce | 32-hex string | ✅ | Single-use per key, valid for 1 h |
You must include at least one of email, phone, or external_user_id so we have a stable identifier for dedup.
Response 202 Accepted
{
"data": { "status": "queued", "kind": "signup", "store_id": 13 },
"meta": { "request_id": "...", "api_version": "v1", "edge": true }
}
The 202 means "we accepted it at the edge and queued it for persistence". The actual database write happens within 5 seconds via our async queue. You don't wait for it. If you need immediate confirmation, register a webhook for signup.counted.
Deduplication rules
Two layers, both at the database level:
(store_id, nonce)UNIQUE — protects against accidental replays of the same call.(store_id, email_hash)UNIQUE — one count per merchant per email, for life.
If a duplicate hits either constraint, the row is inserted with status = 'duplicate' and does not bill — usage.signup.billable stays flat. You can audit duplicates via GET /v1/usage/history.
This means: trying to inflate your numbers by re-submitting the same email won't work. It's also a feature — your idempotency story is automatic.
Worked example: Node.js
import crypto from 'node:crypto';
const KEY_ID = process.env.DZ_PUBLIC_KEY;
const SECRET = process.env.DZ_SIGNING_SECRET;
export async function trackSignup({ email, phone, external_user_id, source, country, meta }) {
const nonce = crypto.randomBytes(16).toString('hex');
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({ email, phone, external_user_id, source, country, meta, nonce });
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const payload = `${KEY_ID}\n${nonce}\n${ts}\n${bodyHash}`;
const sig = crypto.createHmac('sha256', SECRET).update(payload).digest('hex');
const r = 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,
'Content-Type': 'application/json',
},
body,
});
if (!r.ok) {
const err = await r.json();
throw new Error(`signup failed: ${err.error?.code} ${err.error?.message}`);
}
return r.json();
}
Worked example: PHP (e.g. inside a WordPress hook)
<?php
add_action('user_register', function($user_id) {
$user = get_userdata($user_id);
dz_track_signup([
'email' => $user->user_email,
'external_user_id' => "wp_{$user_id}",
'source' => 'wordpress-' . get_bloginfo('name'),
]);
});
function dz_track_signup(array $payload): void {
$keyId = getenv('DZ_PUBLIC_KEY');
$secret = getenv('DZ_SIGNING_SECRET');
$nonce = bin2hex(random_bytes(16));
$ts = (string) time();
$payload['nonce'] = $nonce;
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
$hash = hash('sha256', $body);
$sig = hash_hmac('sha256', "$keyId\n$nonce\n$ts\n$hash", $secret);
$ch = curl_init('https://api.dzbuild.app/v1/signups');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
"Authorization: DZ-Public $keyId",
"X-DZ-Timestamp: $ts",
"X-DZ-Nonce: $nonce",
"X-DZ-Signature: $sig",
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
}
Worked example: Python (Django signal)
import hashlib, hmac, json, os, secrets, time, requests
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.db.models.signals import post_save
KEY_ID = os.environ['DZ_PUBLIC_KEY']
SECRET = os.environ['DZ_SIGNING_SECRET']
@receiver(post_save, sender=User)
def track_dz_signup(sender, instance, created, **kw):
if not created: return
payload = {
'email': instance.email, 'external_user_id': str(instance.pk),
'source': 'django-app', 'nonce': secrets.token_hex(16),
}
body = json.dumps(payload)
ts = str(int(time.time()))
h = hashlib.sha256(body.encode()).hexdigest()
sig = hmac.new(SECRET.encode(),
f"{KEY_ID}\n{payload['nonce']}\n{ts}\n{h}".encode(),
hashlib.sha256).hexdigest()
requests.post('https://api.dzbuild.app/v1/signups', data=body, timeout=5,
headers={
'Authorization': f'DZ-Public {KEY_ID}',
'X-DZ-Timestamp': ts, 'X-DZ-Nonce': payload['nonce'],
'X-DZ-Signature': sig, 'Content-Type': 'application/json',
})
Errors
Returned at the edge in ~10 ms — quick feedback for malformed requests.
| HTTP | Code / Message | Cause |
|---|---|---|
| 400 | bad_request "Body must be valid JSON" | Body wasn't JSON |
| 401 | unauthorized "Missing signature headers" | Forgot the X-DZ-* headers |
| 401 | unauthorized "Timestamp out of window" | Clock drift > 5 min |
| 401 | unauthorized "Nonce reused" | Same nonce twice within 1 h |
| 401 | unauthorized "Signature mismatch" | HMAC inputs wrong |
| 401 | unauthorized "Invalid or revoked public key" | Key revoked or wrong key id |
| 402 | quota_exceeded | Monthly signup quota exhausted |
| 429 | rate_limited | Per-minute burst exceeded |
Best practices
- Sign on your backend, not in the browser. Don't ship the signing secret to your users.
- Generate fresh nonces with a CSPRNG (
crypto.randomBytes,random.SystemRandom,random_bytesin PHP). Never recycle. - Send
external_user_ideven when you have email — it survives email changes. - Don't catch and retry on 401 errors automatically — they're permanent. Inspect once, fix the bug.
- Do retry on 5xx with exponential backoff using a fresh nonce each time.
- Subscribe to
signup.countedwebhook if you need confirmation that the row landed.