Webhooks

Recevez des notifications en temps réel sur les événements liés à vos pay links 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

GET/v1/webhook-endpoints

Lister les webhook endpoints configurés

Response (200 OK)
{
  "data": [
    {
      "id": "wh_a1b2c3d4...",
      "url": "https://api.exemple.fr/webhooks/chorus-pay",
      "enabled": true,
      "description": "Production webhook",
      "events": {
        "pay_link.created": true,
        "pay_link.sent": true,
        "pay_link.accepted": true,
        "invoice.deposited": true,
        "invoice.paid": 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"
    }
  ]
}
POST/v1/webhook-endpoints

Créer un nouveau webhook endpoint

Request
{
  "url": "https://api.exemple.fr/webhooks/chorus-pay",
  "description": "Production webhook",
  "events": {
    "pay_link.created": true,
    "pay_link.sent": true,
    "pay_link.accepted": true,
    "pay_link.cancelled": true,
    "invoice.deposited": true,
    "invoice.paid": true,
    "invoice.rejected": true
    "pay_link.rejected": true,
    "pay_link.expired": true
  },
  "auth": {
    "username": "mon_utilisateur",
    "password": "mon_mot_de_passe"
  }
}
Response (201 Created)
{
  "id": "wh_a1b2c3d4...",
  "url": "https://api.exemple.fr/webhooks/chorus-pay",
  "secret": "whsec_1234567890abcdef",
  "enabled": true,
  "description": "Production webhook",
  "events": {
    "pay_link.created": true,
    "pay_link.sent": true,
    "pay_link.accepted": true,
    "pay_link.cancelled": true,
    "invoice.deposited": true,
    "invoice.paid": true,
    "invoice.rejected": 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.

PATCH/v1/webhook-endpoints/:id

Modifier un webhook endpoint (URL, événements, activation)

Request
{
  "events": {
    "pay_link.created": true,
    "pay_link.accepted": true,
    "invoice.paid": true,
    "invoice.creation.failed": true
  },
  "enabled": true
}
Response (200 OK)
{
  "id": "wh_a1b2c3d4...",
  "url": "https://api.exemple.fr/webhooks/chorus-pay",
  "enabled": true,
  "description": "Production webhook",
  "events": {
    "pay_link.created": true,
    "pay_link.accepted": true,
    "invoice.paid": true,
    "invoice.creation.failed": true
  },
  "created_at": "2025-09-01T10:00:00Z",
  "updated_at": "2025-10-22T11:00:00Z",
  "last_triggered_at": "2025-10-22T10:30:00Z"
}
PATCH/v1/webhook-endpoints/:id

Modifier un webhook endpoint

Request (champs optionnels)
{
  "url": "https://api.votil.fr/webhooks/chorus-pay-v2",
  "description": "Webhook staging",
  "enabled": true,
  "events": { "pay_link.created": true },
  "auth": {
    "username": "user",
    "password": "secret"
  }
}

Pour supprimer l'authentification : "auth": null

DELETE/v1/webhook-endpoints/:id

Supprimer un webhook endpoint

Response (200 OK)
{
  "id": "wh_a1b2c3d4...",
  "deleted": true
}
POST/v1/webhook-endpoints/:id/test

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

Response (200 OK)
{
  "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": "pay_link.accepted",
  "created_at": "2025-10-22T10:30:00Z",
  "data": {
    "object": {
      // Données spécifiques à l'événement
    }
  }
}

Pay Link

ÉvénementDescription
pay_link.createdUn nouveau pay link a été créé
pay_link.sentLe devis a été envoyé par email au client
pay_link.viewedLe client a consulté la page de paiement
pay_link.acceptedLe client a accepté le devis et téléversé les documents d'engagement
pay_link.purchase_order_uploadedUn bon de commande a été téléversé via l'API
pay_link.purchase_order_uploaded_by_clientUn bon de commande a été téléversé par le client
pay_link.purchase_order_analysedLe bon de commande a été analysé par IA
pay_link.status_updatedLe statut du pay link a changé
pay_link.chorus_status_updatedLe statut Chorus Pro du pay link a été mis à jour
pay_link.scheduled_date_updatedLa date programmée de facturation a été modifiée
pay_link.marked_as_paidLe pay link a été marqué comme payé manuellement
pay_link.cancelledLe pay link a été annulé par le fournisseur
pay_link.recycledLe pay link a été recyclé (action requise → accepté)
pay_link.rejectedLe pay link a été rejeté avec un motif
pay_link.expiredLe pay link a expiré sans être accepté

Facture

ÉvénementDescription
invoice.createdUne facture a été créée dans le système (ERP ou interne)
invoice.depositedLa facture a été déposée sur Chorus Pro
invoice.validatedLa facture a été validée sur Chorus Pro
invoice.paidLa facture a été payée par l'organisme public
invoice.rejectedLa facture a été rejetée dans Chorus Pro
invoice.creation.failedLa création de la facture a échoué définitivement après plusieurs tentatives

Webhook

ÉvénementDescription
webhook.testÉvénement de test (toujours envoyé, peu importe la configuration)

Cycle de vie d'un pay link

Voici le flux standard des événements pour un pay link, de la création jusqu'au paiement.

1
Créationpay_link.created

Le fournisseur crée un pay link

2
Envoipay_link.sent

Le devis est envoyé par email au client

3
Consultationpay_link.viewed

Le client ouvre le lien de paiement

4
Acceptationpay_link.accepted

Le client accepte et upload le bon de commande

5
Facturationinvoice.created

La facture est créée dans l'ERP

6
Dépôtinvoice.deposited

La facture est déposée sur Chorus Pro

7
Validationinvoice.validated

Chorus Pro valide la facture

8
Paiementinvoice.paid

L'organisme public effectue le paiement

Événements alternatifs

  • pay_link.expired si le client ne répond pas à temps
  • pay_link.cancelled si le fournisseur annule
  • invoice.rejected si Chorus Pro rejette la facture
  • invoice.creation.failed si la facturation échoue

Suivi Chorus Pro

L'événement pay_link.chorus_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.

pay_link.accepted

Envoyé lorsque le client accepte le devis

{
  "id": "evt_a1b2c3d4e5f6...",
  "type": "pay_link.accepted",
  "created_at": "2025-10-22T10:30:00Z",
  "data": {
    "object": {
      "pay_link_id": "pl_abc123",
      "quote_number": "01163",
      "amount": 62.00,
      "client_name": "Mairie de Saint-Cloud",
      "client_siret": "21092064000016",
      "accepted_at": "2025-10-22T10:30:00Z",
      "validation_mode": "instant"
    }
  }
}

invoice.deposited

Envoyé lorsque la facture est déposée sur Chorus Pro

{
  "id": "evt_x7y8z9...",
  "type": "invoice.deposited",
  "created_at": "2025-10-22T10:31:00Z",
  "data": {
    "object": {
      "invoice_id": "inv_abc123",
      "pay_link_id": "pl_abc123",
      "amount": 62.00,
      "chorus_reference": "CPP-2025-123456",
      "deposited_at": "2025-10-22T10:31:00Z"
    }
  }
}

invoice.paid

Envoyé lorsque le paiement est effectué

{
  "id": "evt_p1q2r3...",
  "type": "invoice.paid",
  "created_at": "2025-11-15T14:20:00Z",
  "data": {
    "object": {
      "invoice_id": "inv_abc123",
      "amount": 62.00,
      "previous_status": "validated",
      "new_status": "paid",
      "paid_at": "2025-11-15T14:20:00Z"
    }
  }
}

pay_link.status_updated

Envoyé à chaque changement de statut du pay link

{
  "id": "evt_s4t5u6...",
  "type": "pay_link.status_updated",
  "created_at": "2025-11-15T14:20:00Z",
  "data": {
    "object": {
      "pay_link_id": "pl_abc123",
      "quote_number": "01163",
      "invoice_id": "inv_abc123",
      "chorus_reference": "CPP-2025-123456",
      "previous_status": "invoiced",
      "new_status": "paid",
      "invoice_status": "paid",
      "chorus_status": "MISE_EN_PAIEMENT"
    }
  }
}

invoice.creation.failed

Envoyé lorsque la création de la facture échoue définitivement

{
  "id": "evt_f1g2h3...",
  "type": "invoice.creation.failed",
  "created_at": "2025-10-22T12:00:00Z",
  "data": {
    "object": {
      "pay_link_id": "pl_abc123",
      "queue_id": "iq_xyz789",
      "error": "Chorus Pro API unavailable",
      "retry_count": 5
    }
  }
}

Sécurité

Vérification de signature

Chaque requête webhook inclut une signature HMAC-SHA256 dans l'en-tête X-Chorus-Pay-Signature. Vérifiez toujours cette signature pour vous assurer que la requête provient bien de Chorus Pay.

Headers envoyés

HeaderDescription
Content-Typeapplication/json
X-Chorus-Pay-SignatureSignature HMAC-SHA256 du body
User-AgentChorus-Pay-Webhooks/1.0

Implémentation (Node.js)

const crypto = require('crypto');

function verifyWebhookSignature(req) {
  const signature = req.headers['x-chorus-pay-signature'];
  const secret = process.env.WEBHOOK_SECRET;
  
  const hmac = crypto.createHmac('sha256', secret);
  const expectedSignature = hmac
    .update(JSON.stringify(req.body))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhooks/chorus-pay', (req, res) => {
  if (!verifyWebhookSignature(req)) {
    return res.status(401).send('Invalid signature');
  }
  
  const event = req.body;
  
  switch (event.type) {
    case 'pay_link.accepted':
      // Traiter l'acceptation du devis
      break;
    case 'invoice.deposited':
      // Facture déposée sur Chorus Pro
      break;
    case 'invoice.paid':
      // Paiement reçu
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
  
  res.status(200).send('OK');
});

Implémentation (Python)

import hmac
import hashlib
import json

def verify_webhook_signature(request, secret):
    signature = request.headers.get('X-Chorus-Pay-Signature')
    
    expected_signature = hmac.new(
        secret.encode(),
        request.data,
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, expected_signature)

@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'] == 'pay_link.accepted':
        # Traiter l'acceptation du devis
        pass
    elif event['type'] == 'invoice.paid':
        # Paiement reçu
        pass
    
    return 'OK', 200

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

TentativeDélai
1Immédiate
2Après 1 min
3Après 2 min
4Après 5 min
5Après 30 min
6Après 2 heures
7Après 6 heures
8Après 12 heures
9Après 24 heures
10Aprè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.

POST/v1/webhook-events/:id/retry

Ré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.

Response (200 OK)
{
  "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 pay_link.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.