Passer au contenu principal

Inscriptions

POST /v1/signups — comptez une inscription d'utilisateur final dans votre quota mensuel. L'unité métrée principale. Signée HMAC, acceptée de façon asynchrone, dédupliquée par nonce + email.

Écrit par Support

C'est l'unité métrée principale de l'API DZBuild. Chaque inscription qui passe par ici est comptée, et GET /v1/usage rapporte le total du mois à côté de votre limite signups_per_month. Sur le plan Enterprise, cette limite vaut -1 par défaut, ce qui signifie aucune limite (voir Quotas), et rien ne bloque un appel qui la dépasse.

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". Contactez le support juste après en avoir créé une pour la faire activer. Révoquer une clé publique ne l'arrête pas à lui seul : les appels signés continuent d'être acceptés jusqu'à ce que le support la désactive, donc prévenez le support chaque fois que vous en révoquez une.

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

email

string

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. Il est stocké mais ne sert pas à la dédup, et aucun endpoint ne le renvoie.

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 dont la plateforme a besoin, et si le body n'en contient pas, la valeur de l'en-tête X-DZ-Nonce est utilisée. Si vous l'envoyez dans le body, utilisez la même valeur que l'en-tête. 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 d'une confirmation, enregistrez un webhook signup.counted. Il n'est pas instantané : il part après le stockage de l'inscription, seulement pour les inscriptions comptées (pas les doublons), et il porte key_id, source et country, pas les identifiants de l'utilisateur.

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)". Appelez-le avec une clé plateforme : les clés publiques n'ont pas le scope usage:read.

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, avant toute mise en file. Elles ne couvrent que les en-têtes, la signature et la syntaxe JSON. Les champs du body sont vérifiés après le 202 : un body que la plateforme ne peut pas stocker, par exemple un nonce de body qui ne fait pas 32 caractères hex, est jeté sans erreur.

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 "Invalid timestamp"

X-DZ-Timestamp n'est pas en secondes entières (1 à 12 chiffres). Une valeur en millisecondes comme Date.now() échoue ici

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

403

forbidden "API access requires an active Enterprise plan"

La boutique de la clé n'est pas sur un plan Enterprise actif

403

forbidden "Missing scope: signups:write"

Vous avez appelé avec une clé plateforme (Bearer) ; seules les clés publiques portent ce scope (sur /v1/events, le message nomme events:write)

429

rate_limited

Burst par minute dépassé : 600 requêtes par minute par boutique, partagées par toutes les clés de la boutique. Attendez les secondes indiquées par Retry-After, puis réessayez avec un nonce frais

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.

  • external_user_id ne sert pas à la dédup. Parmi les champs utilisateur, seul email y sert : le même utilisateur renvoyé avec une autre adresse email compte une nouvelle fois.

  • Ne retry pas automatiquement sur 401 — c'est permanent. Inspectez une fois, fixez le bug.

  • Retry sur 429 et 5xx avec exponential backoff, avec un nonce frais et une nouvelle signature à chaque fois. Dès que la signature est validée, le nonce est consommé, même si l'appel est ensuite refusé : renvoyer la même requête signée renvoie 401 "Nonce reused".

  • Abonnez-vous au webhook signup.counted si besoin de confirmation que la ligne a atterri.

Avez-vous trouvé la réponse à votre question ?