# `POST /v1/events`

Tracker d'événements généraliste. Utilisez-le pour tout ce que vous voulez analyser et qui n'est **pas** une inscription :

* Vues de page
* Soumissions de formulaires lead
* Click-tracking sur landing pages
* Événements custom définis par le marchand

Si vos données ont une **identité utilisateur** significative (une adresse email), utilisez plutôt `/v1/signups` : chaque adresse email n'y est comptée qu'une fois par boutique. Les numéros de téléphone n'y sont pas dédupliqués. `/v1/events` est pour le volume élevé sans identité.

## Auth[​](#auth "Lien direct vers Auth")

Clé publique + HMAC. Même schéma que [`/v1/signups`](https://dzbuild.com/fr/fr/api-docs/public-key/signups.md). Votre backend signe chaque appel.

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. Prévenez le support chaque fois que vous révoquez une clé publique.

## Corps[​](#corps "Lien direct vers Corps")

```
{

  "name":       "lead_submitted",

  "properties": { "plan": "pro", "country": "DZ", "form": "footer" },

  "nonce":      "32-hex-single-use"

}
```

| Champ        | Type               | Requis | Notes                                                                                                                                                                                                                                                                |
| ------------ | ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`       | string ≤ 64 octets | ✅     | Nom d'événement. `snake_case` recommandé. La limite compte des octets : une lettre arabe en vaut deux. Un nom absent, vide ou plus long n'est pas signalé : l'appel renvoie quand même `202` et l'événement est jeté.                                                |
| `properties` | object             |        | Valeurs sérialisables JSON. Stockées telles quelles.                                                                                                                                                                                                                 |
| `nonce`      | 32-hex string      | ✅     | Utilisez la même valeur que l'en-tête `X-DZ-Nonce` (un corps sans `nonce` reçoit la valeur de l'en-tête). Usage unique, pour toujours. Voir [Dédup](#d%C3%A9dup) : la fenêtre d'1 heure n'est que le garde-fou externe ; un nonce n'est jamais utilisable deux fois. |

Les requêtes `POST` doivent aussi porter un en-tête **`Idempotency-Key`** (≤ 64 caractères, charset `[A-Za-z0-9_-:.]`). Sans lui, vous obtenez `400 bad_request` et l'événement n'est jamais mis en file. Le nonce de 32 caractères hex que vous générez déjà est une valeur valide — réutilisez-le.

`name` est stocké avec chaque événement, mais aucun endpoint de l'API ni aucun écran du dashboard ne liste les événements stockés : ce que vous pouvez relire, avec une clé plateforme, c'est le compteur mensuel de `GET /v1/usage` (`usage.event.total`). Restez quand même sur un vocabulaire petit et stable, et évitez de générer les noms dynamiquement (ex. `viewed_product_42` est mauvais ; utilisez `name="viewed_product"` avec `properties.product_id=42`).

## Réponse 202[​](#réponse-202 "Lien direct vers Réponse 202")

```
{

  "data": { "status": "queued", "kind": "event", "store_id": 13 },

  "meta": { "request_id": "...", "api_version": "v1", "edge": true }

}
```

Même flow que les signups : accepté en \~30 ms, stocké sous environ 5 s.

## Dédup[​](#dédup "Lien direct vers Dédup")

La dédup porte sur le **nonce seul**, par boutique. Pas de dédup par email.

Un doublon est **purement et simplement jeté** : rien n'est stocké, aucun compteur de consommation ne bouge, et rien dans l'API ne vous le signale. L'appel a quand même renvoyé `202`, parce que la dédup arrive après l'acceptation. Le doublon vous est donc invisible — générez toujours un nonce frais.

Le nonce est gardé en deux temps, avec des durées de vie différentes :

* Pendant **1 heure**, un nonce réutilisé est rejeté d'emblée avec `401 unauthorized` `"Nonce reused"`.
* **Pour toujours** ensuite, un nonce recyclé est accepté avec `202` puis silencieusement écarté — pas d'erreur, et pas d'événement.

## Exemple : tracking de vues de page (Node.js, server-side)[​](#exemple--tracking-de-vues-de-page-nodejs-server-side "Lien direct vers Exemple : tracking de vues de page (Node.js, server-side)")

```
import crypto from 'node:crypto';



export async function trackEvent(name, properties = {}) {

  const KEY_ID = process.env.DZ_PUBLIC_KEY;

  const SECRET = process.env.DZ_SIGNING_SECRET;

  const nonce = crypto.randomBytes(16).toString('hex');

  const ts    = Math.floor(Date.now() / 1000).toString();

  const body  = JSON.stringify({ name, properties, nonce });

  const hash  = crypto.createHash('sha256').update(body).digest('hex');

  const sig   = crypto.createHmac('sha256', SECRET).update(`${KEY_ID}\n${nonce}\n${ts}\n${hash}`).digest('hex');



  await fetch('https://api.dzbuild.app/v1/events', {

    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,

  });

}



// Dans un middleware Express :

app.use((req, res, next) => {

  trackEvent('page_view', { path: req.path, ua: req.get('user-agent') }).catch(() => {});

  next();

});
```

Notez qu'on **n'attend pas** l'appel depuis le middleware page-view — fire-and-forget pour ne pas bloquer la requête utilisateur. L'API répond en \~30 ms de toute façon, mais ça protège contre les soucis réseau transitoires.

## Ce qui compte dans votre quota[​](#ce-qui-compte-dans-votre-quota "Lien direct vers Ce qui compte dans votre quota")

Chaque appel `/v1/events` qui aboutit réellement incrémente `usage.event.total` ET `usage.event.billable`. Pas de tier « lectures gratuites puis écritures facturables » ici — les events sont facturables dès la requête 1. Les doublons (même nonce) ne bougent ni l'un ni l'autre, puisqu'ils ne sont jamais stockés.

Si le volume monte, **batchez côté vous** : stockez dans votre propre queue et tirez un appel `/v1/events` par event logique en lots de 1 (l'API n'accepte pas de batch). Chaque appel compte dans la limite par minute de la boutique, partagée par toutes ses clés.

## Erreurs[​](#erreurs "Lien direct vers Erreurs")

Mêmes que `/v1/signups`. Voir [Erreurs](https://dzbuild.com/fr/fr/api-docs/errors.md). Les trois qui piègent le plus sur cet endpoint :

| HTTP | Code / Message                                                          | Cause                                                                                                |
| ---- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 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) |
| 403  | `forbidden` "API is in pilot mode; key not enrolled"                    | Clé non inscrite au pilote                                                                           |

## Quand **ne pas** utiliser `/v1/events`[​](#quand-ne-pas-utiliser-v1events "Lien direct vers quand-ne-pas-utiliser-v1events")

* Pour les **commandes vitrine** — le dashboard du marchand les enregistre déjà, et les dupliquer ici ne fait que gonfler votre compteur d'events. Notez que les commandes vitrine ne déclenchent **pas** de webhook `order.created` en API v1 ; seules les commandes créées via `POST /v1/orders` le font (voir le [Catalogue d'événements](https://dzbuild.com/fr/fr/api-docs/webhooks/events.md)).
* Pour les **événements server-internes** non liés aux données marchand (CPU, hits cache). Utilisez un vrai APM.
* Pour les **volumes massifs** (plus d'1M d'events/jour). Construisez votre pipeline analytique et exportez seulement les agrégats ici.
