POST /v1/signups
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.
- 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
{
"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 |
|---|---|---|---|
email | string | un parmi email/phone/external_user_id | Utilisé pour dédup. Stocké comme sha256(lowercase) uniquement. |
phone | string | Stocké comme sha256(value) uniquement. | |
external_user_id | string ≤ 190 | Votre ID interne pour l'utilisateur. Utile si pas d'email/phone. | |
source | string ≤ 64 | Libellé libre (slug page, campagne…). | |
country | string (2 chars) | ISO 3166-1 alpha-2. On uppercase. | |
ip | 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. | |
meta | object | Tout le reste. Stocké en JSON. | |
nonce | 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_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',
'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 | bad_request "Body must be valid JSON" | Body non-JSON |
| 400 | bad_request "Idempotency-Key header is required for write requests" | En-tête manquant |
| 400 | bad_request "Idempotency-Key must be <=64 chars, [A-Za-z0-9_-:.]" | Trop long, ou utilise des caractères hors de cet ensemble (les +, /, = du base64 sont rejetés) |
| 401 | unauthorized "Missing signature headers" | En-têtes X-DZ-* manquants |
| 401 | unauthorized "Timestamp out of window" | Dérive d'horloge de plus de 5 min |
| 401 | unauthorized "Invalid nonce format" | Le nonce ne fait pas exactement 32 caractères hex |
| 401 | unauthorized "Nonce reused" | Même nonce deux fois en 1 h. Après une heure l'appel est accepté, mais le doublon est jeté silencieusement |
| 401 | unauthorized "Signature mismatch" | Inputs HMAC erronés |
| 401 | unauthorized "Invalid or revoked public key" | Clé révoquée, mauvais key id, ou pas encore activée |
| 403 | forbidden "API is in pilot mode; key not enrolled" | Clé non inscrite au pilote |
| 429 | rate_limited | 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.