Webhooks
Les webhooks sont des charges utiles d'événements signées HMAC-SHA256 livrées à votre endpoint quand quelque chose se passe sur un paiement. Ils ré-essayent en cas d'échec et constituent la confirmation autoritative qu'un paiement a été capturé.
Événements
Événements de paiement
| Événement | Quand |
|---|---|
payment.completed | Vente capturée avec succès (état final du flux Sale) |
payment.captured | Autorisation passée en capturée (flux Auth → Capture séparé) |
payment.capture_failed | Tentative de capture sur un paiement autorisé échouée |
payment.failed | Paiement refusé ou échoué |
payment.refunded | Remboursement traité (total ou partiel) |
payment.refund_failed | Tentative de remboursement refusée ou en erreur — la capture d'origine reste réglée |
payment.voided | Paiement autorisé annulé avant capture |
payment.void_failed | Tentative d'annulation sur un paiement autorisé échouée |
Événements d'abonnement
| Événement | Quand |
|---|---|
subscription.created | Un nouvel abonnement a été créé (en trial ou active) |
subscription.active | Un abonnement est passé à l'état active (p. ex. après fin d'essai) |
subscription.trialing | Essai démarré — pas de débit encore |
subscription.charged | Cycle de renouvellement réussi |
subscription.charge_failed | Renouvellement refusé — le dunning démarre selon le calendrier du marchand |
subscription.cancelled | Abonnement annulé (par le marchand, le client, ou après dunning) |
subscription.updated | Moyen de paiement ou calendrier modifié |
Événements de session de paiement
| Événement | Quand |
|---|---|
checkout.session.expired | Session ouverte au-delà de expires_at balayée (cron toutes les 5 min) |
checkout.cancelled | Client a cliqué Annuler sur la page de checkout hébergée |
Charge utile
{
"event_id": "evt_abc123",
"event_type": "payment.completed",
"payload_redacted": {
"transaction_id": "txn_789xyz",
"receipt_number": "RCT-000042",
"amount": 2500,
"currency": "USD",
"status": "captured",
"subtotal": 2300,
"tax_amount": 200,
"tax_name": "VAT",
"payment_method_type": "card",
"card_brand": "Visa",
"card_last4": "0071",
"token_id": "tok_abc123",
"description": "Commande #1042",
"line_items": [
{ "name": "T-shirt", "quantity": 1, "unit_amount": 2300 }
],
"customer": { "name": "Jane Doe", "email": "[email protected]" },
"metadata": { "order_id": "1042", "source": "woocommerce" }
}
}payload_redacted est la copie sûre à journaliser — pas de PAN complet, pas de CVV, juste last4.
Disponibilité des champs par type d'événement
| Champ | payment.* | payment.refunded | subscription.* | checkout.* |
|---|---|---|---|---|
transaction_id | ✅ | ✅ | ✅ | — |
receipt_number | ✅ | ✅ | ✅ | — |
amount, currency | ✅ | ✅ (montant remboursé) | ✅ | ✅ |
subtotal, tax_amount, tax_name | ✅ | ✅ | ✅ | ✅ |
payment_method_type | ✅ | ✅ | ✅ | — |
card_brand, card_last4 | si carte | si carte | si carte | — |
token_id | si tokenisé | si on-token | toujours | — |
line_items, description | ✅ | ✅ | ✅ | ✅ |
customer | ✅ | ✅ | ✅ | ✅ |
metadata | ✅ — metadata de l'appelant sur la session originale | ✅ — même metadata que la session originale (PAS la metadata.merchant_order_id du remboursement) | ✅ | ✅ |
checkout_session_external_id | — | — | — | ✅ |
merchant_order_id (top-level sur remboursements) | — | ✅ | — | — |
metadata est renvoyé tel quel depuis l'appel POST /api/v1/checkout-sessions que vous avez fait — c'est ainsi que les plugins de boutique (WC / Give / Magento / Shopify) corrèlent les webhooks avec la commande d'origine. Définissez ce dont vous avez besoin (un order id, un SKU, un tag de campagne) à la création de session et vous le récupérerez sur chaque événement.
Événements de cycle d'abonnement
subscription.charged et subscription.cancelled portent la même forme que payment.* plus un bloc subscription :
"subscription": {
"external_id": "sub_abc123",
"plan_id": "plan_monthly_pro",
"cycle": 7,
"next_billing_date": "2026-06-12"
}Événements de session de paiement
checkout.session.expired et checkout.cancelled omettent le champ transaction_id — il n'y en a pas car aucun débit n'a été tenté. Utilisez-les pour effacer les lignes pending orphelines dans votre boutique :
{
"event_id": "evt_xyz",
"event_type": "checkout.session.expired",
"payload_redacted": {
"checkout_session_external_id": "cs_abc123",
"amount": 2500,
"currency": "USD",
"metadata": { "order_id": "1042", "source": "give" }
}
}Vérification de signature
Chaque livraison de webhook inclut quatre en-têtes :
| En-tête | Rôle |
|---|---|
X-GC-Signature | Signature HMAC-SHA256 encodée en hex |
X-GC-Timestamp | Timestamp Unix de la tentative de livraison |
X-GC-Event-ID | ID unique d'événement (evt_…) — aussi dans le corps, utile pour la recherche en logs |
X-GC-Event-Type | Le type d'événement (p. ex. payment.completed) |
Calculez la signature attendue côté serveur et comparez en temps constant :
expected = HMAC-SHA256("{timestamp}.{payload}", webhook_secret)Un exemple PHP complet est dans Exemples de code → Webhook handler.
Rejetez les événements anciens
Rejetez les événements dont X-GC-Timestamp s'écarte de plus de 5 minutes de l'horloge. Cela bloque les attaques par rejeu même si une signature fuit.
Rotation du secret
Chaque endpoint webhook a un secret courant et un précédent. Pour faire tourner :
- Tableau de bord → Webhooks → Endpoint → Rotate secret. Un nouveau secret est généré ; l'ancien devient le secret "précédent".
- Les nouvelles livraisons sont signées avec le secret courant. Les rejeux d'événements antérieurs à la rotation sont signés avec le secret précédent (la plateforme suit
secret_versionpar livraison). - Votre récepteur doit accepter l'une ou l'autre signature pendant une fenêtre de transition — essayez d'abord le courant, repli sur le précédent en cas de mismatch.
- Après que toutes les livraisons en vol qui vous intéressent ont été drainées (24h est un plafond sûr), révoquez le secret précédent depuis le tableau de bord.
Calendrier de relances
Les livraisons échouées sont ré-essayées en backoff exponentiel :
1m → 5m → 30m → 2h → 12h → 24hJusqu'à 6 tentatives par événement. Une fois épuisées, l'événement est marqué failed dans le tableau de bord et vous pouvez le rejouer manuellement.
Une livraison est considérée échouée si votre endpoint renvoie une réponse non-2xx ou dépasse le timeout (limite de 10 secondes).
Idempotence côté receveur
Rendez votre handler idempotent sur event_id (ou transaction_id pour les événements de paiement). Le même événement peut être livré plusieurs fois — par exemple, quand notre relance et votre serveur répondent en même temps.
