إنتقل إلى المحتوى الرئيسي

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"
}
FieldTypeRequiredNotes
emailstringone of email/phone/external_user_idUsed for dedup. Stored as sha256(lowercase) only.
phonestringStored as sha256(value) only.
external_user_idstring ≤ 190Your own id for the user. Useful if you don't collect email/phone.
sourcestring ≤ 64Free-form label (page slug, campaign, etc.)
countrystring (2 chars)ISO 3166-1 alpha-2. We uppercase it.
ipstringStored as sha256(value) for fraud detection. Don't include unless meaningful.
metaobjectAnything else. Will be JSON-stored.
nonce32-hex stringSingle-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:

  1. (store_id, nonce) UNIQUE — protects against accidental replays of the same call.
  2. (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 billusage.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.

HTTPCode / MessageCause
400bad_request "Body must be valid JSON"Body wasn't JSON
401unauthorized "Missing signature headers"Forgot the X-DZ-* headers
401unauthorized "Timestamp out of window"Clock drift > 5 min
401unauthorized "Nonce reused"Same nonce twice within 1 h
401unauthorized "Signature mismatch"HMAC inputs wrong
401unauthorized "Invalid or revoked public key"Key revoked or wrong key id
402quota_exceededMonthly signup quota exhausted
429rate_limitedPer-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_bytes in PHP). Never recycle.
  • Send external_user_id even 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.counted webhook if you need confirmation that the row landed.