Aller au contenu principal

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.

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"
}
ChampTypeRequisNotes
emailstringun parmi email/phone/external_user_idUtilisé pour dédup. Stocké comme sha256(lowercase) uniquement.
phonestringStocké comme sha256(value) uniquement.
external_user_idstring ≤ 190Votre ID interne pour l'utilisateur. Utile si pas d'email/phone.
sourcestring ≤ 64Libellé libre (slug page, campagne…).
countrystring (2 chars)ISO 3166-1 alpha-2. On uppercase.
ipstringStocké 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.
metaobjectTout le reste. Stocké en JSON.
nonce32-hex stringUsage 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 :

  1. 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é avec 202 puis écarté silencieusement, car le nonce est mémorisé définitivement.)
  2. 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.

HTTPCode / MessageCause
400bad_request "Body must be valid JSON"Body non-JSON
400bad_request "Idempotency-Key header is required for write requests"En-tête manquant
400bad_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)
401unauthorized "Missing signature headers"En-têtes X-DZ-* manquants
401unauthorized "Timestamp out of window"Dérive d'horloge de plus de 5 min
401unauthorized "Invalid nonce format"Le nonce ne fait pas exactement 32 caractères hex
401unauthorized "Nonce reused"Même nonce deux fois en 1 h. Après une heure l'appel est accepté, mais le doublon est jeté silencieusement
401unauthorized "Signature mismatch"Inputs HMAC erronés
401unauthorized "Invalid or revoked public key"Clé révoquée, mauvais key id, ou pas encore activée
403forbidden "API is in pilot mode; key not enrolled"Clé non inscrite au pilote
429rate_limitedBurst 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_bytes en PHP). Jamais de recyclage.
  • Envoyez external_user_id mê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.counted si besoin de confirmation que la ligne a atterri.