Webhooks
Recevez des notifications en temps réel sur les événements liés à vos commandes et factures.
Temps réel
Recevez les événements instantanément lorsqu'ils se produisent.
Sécurisé
Signature HMAC-SHA256 pour vérifier l'authenticité des requêtes.
Retry automatique
Jusqu'à 10 tentatives automatiques en cas d'échec, avec backoff exponentiel.
Gestion des endpoints
/v1/webhook-endpointsLister les webhook endpoints configurés
{
"data": [
{
"id": "wh_a1b2c3d4...",
"url": "https://api.exemple.fr/webhooks/chorus-pay",
"enabled": true,
"description": "Production webhook",
"events": {
"order.created": true,
"order.message_sent": true,
"order.accepted": true,
"invoice.deposited": true,
"invoice.status_updated": true
},
"auth_configured": false,
"created_at": "2025-09-01T10:00:00Z",
"updated_at": "2025-10-20T14:00:00Z",
"last_triggered_at": "2025-10-22T10:30:00Z"
}
]
}/v1/webhook-endpointsCréer un nouveau webhook endpoint
{
"url": "https://api.exemple.fr/webhooks/chorus-pay",
"description": "Production webhook",
"events": {
"order.created": true,
"order.message_sent": true,
"order.accepted": true,
"order.cancelled": true,
"identity.verified": true,
"invoice.deposited": true,
"invoice.status_updated": true,
"invoice.failed": true
},
"auth": {
"username": "mon_utilisateur",
"password": "mon_mot_de_passe"
}
}{
"id": "wh_a1b2c3d4...",
"url": "https://api.exemple.fr/webhooks/chorus-pay",
"secret": "whsec_1234567890abcdef",
"enabled": true,
"description": "Production webhook",
"events": {
"order.created": true,
"order.message_sent": true,
"order.accepted": true,
"order.cancelled": true,
"invoice.deposited": true,
"invoice.status_updated": true,
"invoice.failed": true
},
"created_at": "2025-10-22T10:30:00Z"
}Important : Le secret est affiché une seule fois lors de la création. Stockez-le de manière sécurisée.
auth (optionnel) : Pour les URLs protégées par une authentification HTTP Basic (popup de connexion), fournissez username et password. Le mot de passe n'est jamais renvoyé dans les réponses API.
/v1/webhook-endpoints/:idModifier un webhook endpoint (URL, événements, activation)
{
"events": {
"order.created": true,
"order.accepted": true,
"invoice.deposited": true,
"invoice.failed": true
},
"enabled": true
}{
"id": "wh_a1b2c3d4...",
"url": "https://api.exemple.fr/webhooks/chorus-pay",
"enabled": true,
"description": "Production webhook",
"events": {
"order.created": true,
"order.accepted": true,
"invoice.deposited": true,
"invoice.failed": true
},
"created_at": "2025-09-01T10:00:00Z",
"updated_at": "2025-10-22T11:00:00Z",
"last_triggered_at": "2025-10-22T10:30:00Z"
}/v1/webhook-endpoints/:idModifier un webhook endpoint
{
"url": "https://api.votil.fr/webhooks/chorus-pay-v2",
"description": "Webhook staging",
"enabled": true,
"events": { "order.created": true },
"auth": {
"username": "user",
"password": "secret"
}
}Pour supprimer l'authentification : "auth": null
/v1/webhook-endpoints/:idSupprimer un webhook endpoint
{
"id": "wh_a1b2c3d4...",
"deleted": true
}/v1/webhook-endpoints/:id/testEnvoyer un événement de test pour vérifier la configuration
Envoie un événement de test à l'endpoint spécifié. L'endpoint doit être activé. La requête attend la livraison et retourne le résultat.
{
"event_id": "evt_a1b2c3d4...",
"webhook_event_id": "whevt_x7y8z9...",
"status": "sent",
"response_status_code": 200,
"sent_at": "2025-10-22T10:30:00Z"
}Événements
Tous les événements que vous pouvez recevoir via webhook. Chaque endpoint peut être configuré pour ne recevoir que les événements souhaités.
Format général
Tous les événements suivent la même structure
{
"id": "evt_a1b2c3d4e5f6...",
"type": "order.accepted",
"created_at": "2025-10-22T10:30:00Z",
"data": {
"object": {
// Données spécifiques à l'événement
}
}
}Un type = un fait
Le type dit ce qui s'est passé, le champ actor dit qui l'a fait (client, supplier, system, api) et les détails sont dans data.object (par exemple template et reminder pour un message, reason pour une annulation, invoice_status pour un statut). Une commande acceptée par le fournisseur à la place du client est donc un order.accepted avec actor: "supplier".
Commande
| Événement | Description |
|---|---|
order.created | Une commande a été créée. source dit d'où : supplier_ui, api, prestashop, shopify, dolibarr, quickinvoice. Créée par un module ou un connecteur boutique : connector (sa clé) et connector_name (son nom). |
order.identified | Le SIRET du client a été confirmé (par le client sur la page d'identification, ou par le fournisseur à sa place : actor). |
order.viewed | Le client a consulté une page de son parcours. step dit laquelle (identification, quote, purchase_order, verify-mailbox, verify-official-code, verify-chorus, verify-chorus-code, pending-validation ; link pour l'ancien parcours). |
order.message_sent | Un message est parti au client ou au fournisseur (recipient_role) : channel (email…), template (identification, quote, verification, invoice, cancellation, supplier_validation…), reminder {n, last} quand c'est une relance. Le premier message quote est le passage au statut Ouvert (devis envoyé). |
order.purchase_order_uploaded | Un devis signé ou un bon de commande a été déposé (par le client, le fournisseur ou l'API : actor) et analysé. Émis à la fin de l'analyse, avec les références extraites (engagement_number, service_code, numero_marche, amount_ttc) et analysis_success. |
order.accepted | Le devis est accepté et les informations du document confirmées (par le client, le fournisseur à sa place, ou l'API : actor). La vérification d'identité suit, puis le dépôt de la facture. |
order.cancelled | La commande est annulée. reason : client (annulation depuis l'email), supplier (depuis la fiche commande), api (annulée dans la boutique ou par l'API, connector et connector_name disent laquelle), inaction (sans réponse après les relances, stage). previous_status dit d'où elle a été annulée (action_required : après une facture rejetée). |
Identité
| Événement | Description |
|---|---|
identity.verified | L'identité du client est confirmée pour cette commande. verification_method : official_email_match, official_email_otp, platform_chorus_code, manual_supplier (par le fournisseur), chorus_code et email_otp (ancien parcours). invoice_deposited vaut true quand la facture était déjà sur Chorus Pro. |
Facture
| Événement | Description |
|---|---|
invoice.created | La facture est créée, dans Chorus Pay ou dans le logiciel de facturation du fournisseur : connector (clé du connecteur qui l'a créée) et connector_name (son nom) ; reference : son numéro. system (chorus, pennylane, dolibarr) : ancienne clé, encore posée par les connecteurs pas encore portés. Une nouvelle tentative après un rejet crée une nouvelle facture (actor supplier). |
invoice.deposited | La facture est sur Chorus Pro. identity_verified : l'identité du client était confirmée au dépôt (le marchand peut livrer). source : chorus_pay, ou chorus_import pour une facture déposée hors Chorus Pay et détectée. retry : après une nouvelle tentative. erp_reference, connector, connector_name : numéro et connecteur du logiciel de facturation quand il y en a un. |
invoice.failed | La création (step creation, chez connector / connector_name) ou le dépôt (step deposit, system chorus) de la facture a échoué. will_retry : true si une nouvelle tentative automatique suit, false si l'échec est définitif (action requise). Rien à voir avec un rejet par Chorus Pro (invoice.status_updated). |
invoice.status_updated | Le statut de la facture a changé sur Chorus Pro. chorus_status : le statut brut (MISE_A_DISPOSITION, MANDATEE, REJETEE…), invoice_status : le statut Chorus Pay dérivé (deposited, validated, paid, action_required, rejected) avec previous_invoice_status, rejection_reason quand Chorus en donne un. rejected ne désigne que REJETEE côté Chorus Pro. |
Webhook
| Événement | Description |
|---|---|
webhook.test | Événement de test (toujours envoyé, peu importe la configuration) |
Cycle de vie d'une commande
Voici le flux standard des événements pour une commande, de la création jusqu'au paiement.
order.createdLa commande est créée (boutique, API ou espace fournisseur)
order.identifiedLe SIRET de l'organisme est confirmé
order.message_sentPremier message template quote : la commande passe au statut Ouvert
order.purchase_order_uploadedLe devis signé ou le bon de commande est déposé et analysé
order.acceptedLes informations sont confirmées, la commande est acceptée
identity.verifiedL'identité du client est confirmée pour cette commande
invoice.createdLa facture est créée (Chorus Pay ou ERP)
invoice.depositedLa facture est déposée sur Chorus Pro
invoice.status_updatedChorus Pro fait avancer la facture jusqu'au paiement
Événements alternatifs
order.cancelledavec reason inaction si le client ne répond pas à tempsorder.cancelledavec reason client, supplier ou api (annulée dans la boutique ou par l'API)invoice.status_updatedavec invoice_status rejected si Chorus Pro rejette la factureinvoice.failedsi la création ou le dépôt échoue (will_retry dit si une nouvelle tentative suit)
Suivi Chorus Pro
L'événement invoice.status_updated est émis à chaque changement de statut Chorus Pro, vous permettant de suivre la progression de la facture en temps réel (mise à disposition, mandatée, comptabilisée, mise en paiement).
Exemples de payloads
Chaque événement contient un payload data.object avec les données pertinentes. Voici les payloads des événements les plus courants.
order.accepted
Le devis est accepté (par le client, ou par le fournisseur à sa place : actor)
{
"id": "evt_a1b2c3d4e5f6...",
"type": "order.accepted",
"actor": "client",
"created_at": "2026-09-17T10:30:00.123Z",
"data": {
"object": {
"order_id": "ord_abc123",
"quote_number": "01163",
"amount": 62,
"client_name": "Mairie de Saint-Cloud",
"client_siret": "21092064000016",
"accepted_at": "2025-11-09T10:30:00Z"
}
}
}identity.verified
L'identité du client est confirmée pour cette commande
{
"id": "evt_a1b2c3d4e5f6...",
"type": "identity.verified",
"actor": "client",
"created_at": "2026-09-17T10:30:00.123Z",
"data": {
"object": {
"order_id": "ord_abc123",
"siret": "21092064000016",
"email": "comptabilite@mairie.fr",
"verification_method": "official_email_otp",
"verified_at": "2025-11-12T08:15:00Z",
"invoice_deposited": false
}
}
}invoice.deposited
La facture est sur Chorus Pro ; identity_verified = le marchand peut livrer
{
"id": "evt_a1b2c3d4e5f6...",
"type": "invoice.deposited",
"actor": "system",
"created_at": "2026-09-17T10:30:00.123Z",
"data": {
"object": {
"invoice_id": "inv_abc123",
"order_id": "ord_abc123",
"amount": 62,
"chorus_reference": "CPP-2025-123456",
"invoice_number": "20260000000000000012",
"erp_reference": "FACT-2025-001",
"connector": "pennylane",
"connector_name": "Pennylane",
"identity_verified": true,
"source": "chorus_pay",
"retry": false,
"order": {
"id": "ord_abc123",
"metadata": {
"prestashop_order_id": "6"
}
}
}
}
}invoice.status_updated
Le statut Chorus Pro de la facture a changé
{
"id": "evt_a1b2c3d4e5f6...",
"type": "invoice.status_updated",
"actor": "system",
"created_at": "2026-09-17T10:30:00.123Z",
"data": {
"object": {
"invoice_id": "inv_abc123",
"chorus_reference": "CPP-2025-123456",
"chorus_status": "MANDATEE",
"previous_chorus_status": "SERVICE_FAIT",
"invoice_status": "validated",
"previous_invoice_status": "deposited",
"rejection_reason": null
}
}
}invoice.failed
Échec technique de création ou de dépôt (pas un rejet Chorus Pro)
{
"id": "evt_a1b2c3d4e5f6...",
"type": "invoice.failed",
"actor": "system",
"created_at": "2026-09-17T10:30:00.123Z",
"data": {
"object": {
"order_id": "ord_abc123",
"invoice_id": "inv_abc123",
"step": "deposit",
"system": "chorus",
"error": "Chorus Pro unavailable",
"will_retry": true
}
}
}Sécurité
Vérification de signature
Chaque requête webhook porte une signature HMAC-SHA256 horodatée dans l'en-tête X-Chorus-Pay-Signature-V2 (format t=<secondes unix>,v1=<hex>, signature de t + "." + corps brut). Vérifiez la signature sur le corps brut, tel que reçu, et refusez un t à plus de 5 minutes de votre horloge : une requête rejouée plus tard est ainsi rejetée. L'en-tête historique X-Chorus-Pay-Signature (HMAC du corps seul) est encore envoyé pour les intégrations existantes.
Headers envoyés
| Header | Description |
|---|---|
Content-Type | application/json |
X-Chorus-Pay-Signature-V2 | t=<unix>,v1=<hex> : HMAC-SHA256 de t.body, tolérance 5 minutes (recommandé) |
X-Chorus-Pay-Signature | HMAC-SHA256 du body seul (historique, sans protection contre le rejeu) |
User-Agent | Chorus-Pay-Webhooks/1.0 |
Implémentation (Node.js)
const crypto = require('crypto');
// Le corps BRUT est indispensable : la signature porte sur les octets reçus.
app.use('/webhooks/chorus-pay', express.raw({ type: 'application/json' }));
function verifyWebhookSignature(req) {
const header = req.headers['x-chorus-pay-signature-v2'] || '';
const secret = process.env.CHORUS_PAY_WEBHOOK_SECRET;
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${req.body.toString('utf8')}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(parts.v1 || ''), Buffer.from(expected));
}
app.post('/webhooks/chorus-pay', (req, res) => {
if (!verifyWebhookSignature(req)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
switch (event.type) {
case 'order.accepted':
// Traiter l'acceptation du devis
break;
case 'invoice.deposited':
// Facture déposée sur Chorus Pro
break;
case 'invoice.status_updated':
// Statut Chorus Pro mis à jour (data.invoice_status)
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).send('OK');
});Implémentation (Python)
import hmac
import hashlib
import json
import time
def verify_webhook_signature(request, secret):
header = request.headers.get('X-Chorus-Pay-Signature-V2', '')
parts = dict(kv.split('=', 1) for kv in header.split(',') if '=' in kv)
try:
t = int(parts['t'])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > 300:
return False
# request.data = corps brut, tel que reçu
expected = hmac.new(
secret.encode(),
f"{t}.".encode() + request.data,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(parts.get('v1', ''), expected)
@app.route('/webhooks/chorus-pay', methods=['POST'])
def webhook():
if not verify_webhook_signature(request, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.json
if event['type'] == 'order.accepted':
# Traiter l'acceptation du devis
pass
elif event['type'] == 'invoice.status_updated':
# Statut Chorus Pro mis à jour (data['invoice_status'])
pass
return 'OK', 200Gestion des retries
En cas d'échec de livraison (timeout de 30s, erreur 5xx, erreur réseau, etc.), nous réessayons automatiquement selon le schéma suivant :
| Tentative | Délai |
|---|---|
| 1 | Immédiate |
| 2 | Après 1 min |
| 3 | Après 2 min |
| 4 | Après 5 min |
| 5 | Après 30 min |
| 6 | Après 2 heures |
| 7 | Après 6 heures |
| 8 | Après 12 heures |
| 9 | Après 24 heures |
| 10 | Après 48 heures |
Après 10 tentatives infructueuses, l'événement est marqué comme échoué. Vous pouvez déclencher un retry manuel depuis l'interface ou via l'API.
Bonne pratique
Votre endpoint doit répondre avec un code HTTP 2xx (idéalement 200) dans les 30 secondes pour que nous considérions la livraison comme réussie. Traitez les webhooks de manière asynchrone pour éviter les timeouts.
/v1/webhook-events/:id/retryRéessayer manuellement un événement webhook échoué
Relance la livraison d'un événement webhook qui a échoué. L'événement ne doit pas être déjà livré avec succès ni avoir dépassé le nombre maximum de tentatives.
{
"id": "whevt_a1b2c3d4...",
"status": "retrying",
"retry_count": 3,
"next_retry_at": "2025-10-22T14:30:00Z",
"last_sent_at": "2025-10-22T12:00:00Z"
}Best Practices
1. Traitez les événements de manière idempotente
En cas de retry, vous pourriez recevoir le même événement plusieurs fois. Utilisez le champ id de l'événement pour détecter les doublons.
2. Répondez rapidement
Retournez un code 200 dès réception, puis traitez l'événement de manière asynchrone (queue, background job, etc.). Le timeout est de 30 secondes.
3. Utilisez HTTPS
Seules les URLs HTTPS sont acceptées pour les webhooks afin de garantir la sécurité des données en transit.
4. Vérifiez toujours la signature
Utilisez le header X-Chorus-Pay-Signature pour valider l'origine de chaque requête avant de la traiter.
5. Abonnez-vous aux événements pertinents
Ne vous abonnez qu'aux événements dont vous avez besoin. Pour la plupart des intégrations, les événements essentiels sont order.accepted, invoice.deposited et invoice.paid.
6. Loggez les événements
Conservez un log des événements reçus pour faciliter le debugging et l'audit. Stockez au minimum l'id, le type et le created_at.