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 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

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 :

  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_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

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_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.

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