Documentation

API & intégration

Webhooks sortants

Mis à jour le 24 septembre 2026Plateforme v1 Markdown brut

Principe

Quand une organisation qui a installé votre app déclenche un événement
métier (commande passée, facture payée, devis accepté…), Capibara envoie un
POST JSON signé à l’URL que vous avez configurée. Vous n’interrogez
plus l’API en boucle : votre backend réagit en temps réel.

Configuration : portail développeur → votre app → section Backend & webhooks →
« Ajouter un webhook ». Vous choisissez l’URL (HTTPS obligatoire) et les
événements souscrits. Le secret est affiché une seule fois à la
création — stockez-le immédiatement (variable d’environnement, jamais dans
le code : le scan de review le détecterait).

5 webhooks maximum par app. Les adresses IP littérales et les hôtes
locaux/internes sont refusés.

Format de livraison

POST /votre/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Capibara-Webhooks/1.0
x-capibara-signature: sha256=3f5a…e2c1
x-capibara-timestamp: 1756900000
x-capibara-event: invoice.paid
x-capibara-delivery: 018f3c2e-7b1a-4c5d-9e8f-2a6b4c8d0e1f
{
  "id": "018f3c2e-7b1a-4c5d-9e8f-2a6b4c8d0e1f",
  "event": "invoice.paid",
  "occurredAt": "2026-07-02T14:32:05.120Z",
  "tenantId": "cm9x…",
  "data": { "invoiceId": "inv_…", "totalCents": 12050 }
}
  • id (= x-capibara-delivery) est stable à travers les retries d’une
    même livraison : c’est votre clé d’idempotence — si vous l’avez déjà
    traité, répondez 2xx sans retraiter.
  • tenantId identifie l’organisation émettrice (le même que dans l’API).
  • data porte les champs métier de l’événement — aucun identifiant interne
    de compte plateforme n’est transmis.
  • x-capibara-timestamp (secondes epoch) date l’envoi : avec
    createServerHandler({ maxAgeSec }), une livraison plus ancienne est
    refusée (401 stale). L’en-tête n’est pas couvert par la signature
    (contrat figé) : c’est un filet contre le rejeu d’une vieille capture, pas
    une preuve — l’idempotence par id reste la règle.

Richesse de l’enveloppe (par souscription)

Par défaut l’enveloppe porte l’événement tel quel (identifiants, montants).
Deux options, cochées à la création du webhook ou à tout moment sur sa ligne,
complètent data à la livraison (un réessai relit l’état à jour) :

Option Ajoute Pour Accès requis
Joindre les liens des documents data.document = { id, number, type, status, totalTtcCents, currency, pdfUrl, payUrl? } (PDF public signé, valable 7 jours ; payUrl seulement s’il reste un solde) sales.document.paid billing-fr.documents:read
data.charge = { id, code, label, amountCents, currency, paidAt } pos.charge.paid billing-fr.pos:read
Joindre l’identité du client data.customer = { name, email } (figés sur le document) ou { email } (saisi au paiement d’une charge) les deux l’accès du document et crm.contacts:read

Une option n’est appliquée que si l’organisation destinataire a accordé à
l’installation
l’accès indiqué (vérifié à chaque livraison) ; sinon
l’enveloppe part sans ce complément, sans erreur. Le lien pdfUrl expire
au bout de 7 jours : si vous devez conserver le document, téléchargez-le
à réception (ou relisez-le plus tard par l’API publique).

L’identité du client est une donnée personnelle : n’activez cette option que
si votre backend en a besoin, et traitez-la comme telle.

Vérifier la signature (obligatoire)

Chaque requête est signée HMAC-SHA256 sur le corps brut avec votre
secret : en-tête x-capibara-signature: sha256=<hex>. Rejetez (401) toute
requête dont la signature ne correspond pas — sinon n’importe qui pouvant
deviner votre URL peut vous injecter de faux événements.

Avec @capibara-dev/sdk :

import { verifyWebhookSignature, WEBHOOK_SIGNATURE_HEADER } from '@capibara-dev/sdk';

// ⚠️ TOUJOURS sur le corps BRUT reçu (string/octets), jamais sur un JSON
// re-sérialisé : la signature porte sur les octets exacts.
const ok = await verifyWebhookSignature(rawBody, req.headers[WEBHOOK_SIGNATURE_HEADER], process.env.CAPIBARA_WEBHOOK_SECRET!);
if (!ok) return res.status(401).end();

Sans SDK (Node.js) :

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody: string, header: string, secret: string): boolean {
  const m = /^sha256=([0-9a-f]{64})$/i.exec(header ?? '');
  if (!m) return false;
  const expected = Buffer.from(m[1], 'hex');
  const actual = createHmac('sha256', secret).update(rawBody, 'utf8').digest();
  return expected.length === actual.length && timingSafeEqual(actual, expected);
}

Accusé de réception, retries, désactivation

  • Répondez 2xx en moins de 10 secondes pour accuser réception. Faites le
    traitement lourd en asynchrone chez vous (queue) — répondez d’abord.
  • Toute autre réponse (ou timeout) déclenche des retries à backoff
    croissant
    : 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h.
  • Après 8 échecs consécutifs, la souscription est désactivée
    automatiquement
    et vous êtes prévenu par e-mail. Corrigez votre endpoint
    puis réactivez-la depuis la section Backend & webhooks (le compteur repart à zéro).
  • Le bouton Tester envoie un ping signé ("event": "ping") immédiat,
    sans pénaliser la souscription.

Événements disponibles

Un webhook ne livre jamais ce que votre app ne pourrait pas lire par l’API :
chaque événement exige que l’organisation ait accordé à l’installation
l’accès de lecture indiqué (les scopes feuilles de
votre manifeste). Sans cet accès, l’événement ne vous est pas envoyé par
cette organisation.

Événement Émis quand… Accès requis
contact.created Un contact est créé (CRM, formulaire du site, import…). crm.contacts:read
quote.created / quote.accepted Un devis est créé / accepté. billing-fr.documents:read
opportunity.won Une opportunité CRM est gagnée. crm.opportunities:read
invoice.created / invoice.sent / invoice.paid / invoice.voided Cycle de vie d’une facture. billing-fr.documents:read
order.placed / order.shipped Une commande boutique est passée / expédiée. shop.orders:read
product.created / product.updated Catalogue produit. shop.products:read
stock.low / stock.movement.recorded Stock. shop.products:read
booking.created Une réservation est prise (planning). planning.bookings:read
sales.document.paid Un document de vente v2 (facture, acompte) ou l’une de ses échéances est encaissé — le montant est dans l’événement ; enrichissable par les options ci-dessus. billing-fr.documents:read
pos.charge.paid Une charge TPE est payée (caisse, ou billing.charges.create de votre app). billing-fr.pos:read
approval.decided Une demande de validation posée par votre app (ctx.approvals.request) a été tranchée par un humain — jamais les validations des modules. aucun (la demande vient de votre app)

Non livrés par webhook — aucun accès d’app ne couvre encore ces
domaines, la souscription les refuse : goods.received (achats),
employee.created, leave.requested, leave.approved (RH),
document.attached (médias).

Les apps hébergées (events du manifeste) utilisent le même catalogue
fermé, ces cinq événements compris : une clé absente des deux listes
ci-dessus est refusée à la validation.

Vous ne recevez que les événements des organisations qui ont installé
votre app
(installation active) et accordé l’accès requis — jamais ceux
du reste de la plateforme. Ces conditions sont revérifiées à chaque
envoi
, réessais compris : une installation mise en pause ou désinstallée,
un accès retiré ou un compte développeur suspendu arrêtent aussi les
livraisons déjà en file.

Bonnes pratiques

  • Idempotence : déduplicable par id (les retries renvoient le même).
  • Ordre non garanti : datez vos traitements avec occurredAt, pas avec
    l’ordre d’arrivée.
  • Secret par webhook : régénérez-le au moindre doute (l’ancien est
    invalidé immédiatement) et limitez sa diffusion à l’endpoint de réception.
Webhooks sortants · Capibara for Developers