C'est l'unité métrée principale de l'API DZBuild. Chaque inscription qui passe par ici est comptée dans le chiffre signups_per_month de votre tier et contribue au pricing plateforme pour les intégrations partenaires. Le comptage est réel ; l'application de la limite, non — elle est remontée par GET /v1/usage, mais rien ne bloque actuellement un appel pour dépassement.
Conçu pour un cas précis : vous avez un site/app externe qui accepte des inscriptions, et vous voulez qu'elles comptent pour votre compte marchand DZBuild. Exemples :
Un site WordPress que vous gérez pour le marketing → l'utilisateur remplit le formulaire → vous appelez
/v1/signups.Une app mobile où les utilisateurs s'inscrivent → backend appelle
/v1/signups.Une landing page sur un autre domaine → backend appelle
/v1/signups.
Pas conçu pour les commandes vitrine (qui créent des clients via le flow vitrine) ni pour des inscriptions ponctuelles type lead-magnet (utilisez /v1/events).
Auth
Clé publique + HMAC. Votre backend signe chaque appel. Voir Authentification pour le schéma HMAC complet.
⚠️ Attention — Deux choses qui bloquent une clé toute neuve
Barrière pilote. L'API est réservée au pilote en production. Une clé que DZBuild n'a pas inscrite renvoie
403 forbidden"API is in pilot mode; key not enrolled"à chaque appel.Délai d'activation. Une clé publique fraîchement émise n'est pas utilisable à l'instant où elle est créée — tant qu'elle n'est pas activée pour ces endpoints, vous obtenez
401 unauthorized"Invalid or revoked public key". La révocation n'est pas instantanée non plus : s'il vous faut arrêter une clé immédiatement, demandez au support.
Corps
{
"email": "[email protected]",
"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"
}
Champ | Type | Requis | Notes |
| string | un parmi email/phone/external_user_id | Utilisé pour dédup. Stocké comme |
| string | Stocké comme | |
| string ≤ 190 | Votre ID interne pour l'utilisateur. Utile si pas d'email/phone. | |
| string ≤ 64 | Libellé libre (slug page, campagne…). | |
| string (2 chars) | ISO 3166-1 alpha-2. On uppercase. | |
| string | Stocké haché, jamais en clair. Il ne vous rapporte rien que vous puissiez relire et fait quand même sortir des données personnelles de votre système — ne l'envoyez pas. | |
| object | Tout le reste. Stocké en JSON. | |
| 32-hex string | ✅ | Usage unique, pour toujours. Voir Règles de déduplication — la fenêtre d'1 heure n'est que le garde-fou externe ; un nonce n'est jamais utilisable deux fois. |
nonce est le seul champ réellement obligatoire, mais un body sans aucun identifiant reste compté comme facturable.
Envoyez email. Sans lui, la dédup à vie par email décrite plus bas ne peut pas s'appliquer : une re-soumission accidentelle du même utilisateur compte alors deux fois.
Chaque POST doit aussi porter un en-tête Idempotency-Key (≤ 64 caractères, charset [A-Za-z0-9_-:.]). Sans lui, vous obtenez 400 bad_request et rien n'est mis en file. Le nonce de 32 caractères hex que vous générez déjà est une valeur valide — réutilisez-le.
Réponse 202 Accepted
{
"data": { "status": "queued", "kind": "signup", "store_id": 13 },
"meta": { "request_id": "...", "api_version": "v1", "edge": true }
}
Le 202 signifie « accepté et mis en file pour persistance ». Le stockage se termine sous environ 5 secondes. Vous n'attendez pas. Si vous avez besoin de confirmation immédiate, enregistrez un webhook signup.counted.
Règles de déduplication
Deux règles, toutes deux appliquées par boutique :
Un comptage par nonce, à vie — protège des rejeux accidentels du même appel. (Un nonce réutilisé est rejeté d'emblée pendant 1 heure avec
401 "Nonce reused"; ensuite il est accepté avec202puis écarté silencieusement, car le nonce est mémorisé définitivement.)Un comptage par adresse email, à vie — Ne s'applique que si vous envoyez
email.
Quand une inscription touche l'une des règles, rien n'est stocké. Ce que vous voyez à la place, c'est usage.signup.total qui augmente de 1 pendant que usage.signup.billable reste plat. Les doublons ne facturent donc pas, mais ils ne sont pas non plus enregistrés individuellement : il n'y a rien pour retrouver le doublon.
Vous pouvez déduire les doublons depuis GET /v1/usage/history en calculant count − billable_count pour endpoint_group = "signup". Cet endpoint renvoie des agrégats horaires sous data.rows (period_hour, endpoint_group, count, billable_count), porte par défaut sur les 7 derniers jours, et rejette toute fenêtre de plus de 90 jours avec 400 bad_request "range too large (max 90 days)".
Avantage pratique : votre histoire d'idempotence est automatique, tant que vous envoyez email.
Exemple : 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,
'Idempotency-Key': nonce, // obligatoire sur chaque POST
'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();
}
Exemple : PHP (hook WordPress)
<?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",
"Idempotency-Key: $nonce", // obligatoire sur chaque POST
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
}
Exemple : Python (signal Django)
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_saveKEY_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',
'Idempotency-Key': payload['nonce'], # obligatoire sur chaque POST
})
Erreurs
Les erreurs de validation reviennent en ~10 ms — feedback rapide pour les requêtes mal formées.
HTTP | Code / Message | Cause |
400 |
| Body non-JSON |
400 |
| En-tête manquant |
400 |
| Trop long, ou utilise des caractères hors de cet ensemble (les |
401 |
| En-têtes X-DZ-* manquants |
401 |
| Dérive d'horloge de plus de 5 min |
401 |
| Le nonce ne fait pas exactement 32 caractères hex |
401 |
| Même nonce deux fois en 1 h. Après une heure l'appel est accepté, mais le doublon est jeté silencieusement |
401 |
| Inputs HMAC erronés |
401 |
| Clé révoquée, mauvais key id, ou pas encore activée |
403 |
| Clé non inscrite au pilote |
429 |
| Burst par minute dépassé. Les plafonds découlent du plan de la boutique : free 60, pro 120, unlimited 300, enterprise 600 req/min |
Il n'y a pas de 402 sur cet endpoint. signups_per_month est compté mais jamais appliqué : le dépasser ne fait échouer aucun appel.
Bonnes pratiques
Signez côté backend, pas dans le navigateur. N'envoyez pas le secret aux utilisateurs.
Générez des nonces frais avec un CSPRNG (
crypto.randomBytes,random.SystemRandom,random_bytesen PHP). Jamais de recyclage.Envoyez
external_user_idmême si vous avez l'email — il survit aux changements d'email.Ne retry pas automatiquement sur 401 — c'est permanent. Inspectez une fois, fixez le bug.
Retry sur 5xx avec exponential backoff et un nonce frais à chaque fois.
Abonnez-vous au webhook
signup.countedsi besoin de confirmation que la ligne a atterri.