Recevoir les webhooks
Plutôt que d’interroger l’API en boucle, vous enregistrez une URL et nous vous appelons quand quelque chose change.
Un endpoint est enregistré pour un mode — live ou test — et abonné à une liste de noms d’événements. Un endpoint de test reçoit exactement la même forme de charge utile, signée de la même façon, qu’un endpoint live.
La forme d’un événement
Section intitulée « La forme d’un événement »Voici, octet pour octet à l’id près, le corps qu’un endpoint de test reçoit
pour invoiceSent — chaque livraison de test porte la même ressource
d’exemple, ce qui vous permet de comparer ce qui arrive à cette page :
{ "id": "evt_3f9c2b7d1e8a4c6f9b0d2e5a7c1f4b8d", "type": "invoiceSent", "createdAt": "2026-01-15T09:30:00.000Z", "mode": "test", "data": { "object": { "id": "inv_0000000000000001", "projectId": "prj_0000000000000001", "status": "sent", "refNumber": "F-2026-0042", "isFinalInvoice": true, "amount": 1850.5, "clientRef": { "id": "cli_0000000000000001", "name": "Acme AG", "email": "billing@example.com" }, "sentAt": "2026-01-15T09:30:00.000Z", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z" } }}| Champ | Signification |
|---|---|
id |
L’identifiant de l’événement, evt_ suivi de 32 caractères hexadécimaux. Votre clé de dédoublonnage — voir plus bas. |
type |
Le nom de l’événement — voir Noms des événements. |
createdAt |
Le moment où l’événement métier s’est produit, en ISO 8601, en UTC. Pas le moment de l’envoi de cette tentative : une nouvelle tentative porte la même valeur. |
mode |
live ou test, le mode de l’endpoint auquel l’événement a été livré. |
data.object |
La ressource concernée par l’événement. |
data.object est construit sur les mêmes schémas que les réponses de lecture
de l’API. Vous n’avez donc qu’une seule forme à analyser par ressource, que vous
l’ayez obtenue par un GET ou reçue par un webhook. Tous les horodatages, sur
l’enveloppe comme dans la ressource, sont des chaînes ISO 8601.
Lisez la ressource dans data.object, et non dans data : cet emboîtement est
ce qui nous permettra d’y ajouter plus tard un voisin — les valeurs précédentes
des champs modifiés, par exemple — sans déplacer ce que vous lisez déjà.
Noms des événements
Section intitulée « Noms des événements »Les noms d’événements sont en camelCase, et reprennent le vocabulaire des
notifications de l’application : invoiceSent, quoteAccepted,
jobRescheduled. Un endpoint peut être abonné à n’importe lequel de ceux-ci :
| Ressource | Événements |
|---|---|
| Projet | projectCreated, projectUpdated, projectShared, projectClientValidationRequested, projectAccepted, projectDeclined, projectCanceled, projectDone, projectRated, projectReminderCreated, projectReminderUpdated, projectReminderDeleted, projectReminderSent |
| Intervention | jobCreated, jobUpdated, jobScheduled, jobRescheduled, jobDone, jobNoShow, jobCanceled |
| Message | messageCreated |
| Devis | quoteCreated, quoteUpdated, quoteDeleted, quoteSent, quoteAccepted, quoteDeclined |
| Facture | invoiceCreated, invoiceUpdated, invoiceDeleted, invoiceSent, invoiceAccepted, invoiceDeclined, invoiceAutoMarkPaidFailed, invoiceReminderCreated, invoiceReminderUpdated, invoiceReminderDeleted, invoiceReminderSent, invoiceReminderFailed |
La même liste est l’énumération event de POST /test/webhooks/send dans la
description OpenAPI de l’API, qui fait foi : un nom qui n’y
figure pas est refusé par un 400 listant les noms valides.
Vérifier la signature
Section intitulée « Vérifier la signature »Chaque livraison porte un en-tête Tay-Signature :
Tay-Signature: t=1758186764,v1=5257a869e7b2cd0e2c...test l’horodatage Unix de la signature.v1est un HMAC-SHA256, en hexadécimal, de la chaîne${t}.${rawBody}, calculé avec le secret de signature de l’endpoint.
C’est le schéma de Stripe. Si vous avez déjà vérifié un webhook Stripe, vous avez déjà écrit ce code.
La vérification tient en quatre gestes :
- Lisez le corps brut, avant tout analyseur JSON.
- Extrayez
tetv1de l’en-tête. - Recalculez le HMAC sur
${t}.${rawBody}et comparez-le àv1en temps constant. - Refusez si
ts’écarte de l’heure courante de plus de 5 minutes.
import { createHmac, timingSafeEqual } from 'node:crypto';
/** La fenêtre de tolérance : cinq minutes, dans les deux sens. */const TOLERANCE_SECONDS = 300;
export function verify(rawBody, header, secret) { const parts = Object.fromEntries( header.split(',').map((part) => part.split('=')), );
const expected = createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`) .digest('hex');
const received = Buffer.from(parts.v1, 'hex'); const computed = Buffer.from(expected, 'hex');
// `timingSafeEqual` exige deux tampons de même longueur. if (received.length !== computed.length) return false; if (!timingSafeEqual(received, computed)) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)); return age <= TOLERANCE_SECONDS;}L’horodatage est inclus dans la chaîne signée précisément pour que vous puissiez détecter un rejeu : sans lui, une requête interceptée resterait valable indéfiniment.
Dédoublonnez par identifiant d’événement
Section intitulée « Dédoublonnez par identifiant d’événement »C’est le point le plus important de cette page.
Si votre endpoint ne répond pas un 2xx — parce qu’il est lent, indisponible,
ou qu’il a renvoyé une erreur — la livraison est réessayée un nombre borné
de fois, puis marquée en échec.
Une tentative rejouée porte le même id et le même createdAt que la
première. Elle n’est pas un nouvel événement : c’est le même, présenté à
nouveau. Seul le t de l’en-tête de signature change, parce qu’il horodate la
tentative, et non l’événement.
Vous devez donc :
- conserver les
iddéjà traités, dans votre base ; - à la réception, vérifier si l’
idy figure déjà ; - si oui, répondre
200et ne rien faire d’autre.
Sans cela, un endpoint momentanément lent facturera deux fois, enverra deux e-mails, ou créera deux lignes. Un réseau n’offre aucune garantie de livraison unique, et nous n’en offrons pas non plus : nous garantissons que l’identifiant est stable, ce qui vous permet de construire la garantie qui vous manque.
Traitez aussi l’ordre comme non garanti. Deux événements proches peuvent vous
parvenir dans le désordre ; fiez-vous à createdAt, et non à l’ordre d’arrivée.
Ce que votre endpoint doit faire
Section intitulée « Ce que votre endpoint doit faire »- Répondre
2xxrapidement. Accusez réception, mettez le travail en file, et traitez-le ensuite — un traitement long provoque un délai d’attente, donc une nouvelle tentative, donc un doublon. - Répondre
2xxmême pour untypeque vous ne connaissez pas. De nouveaux événements peuvent apparaître, et les refuser déclencherait des tentatives inutiles. - Ne jamais faire confiance à une charge utile dont la signature n’a pas été vérifiée.