La redirection du navigateur n'est qu'un confort d'affichage. La vérité, c'est le webhook signé — ou une lecture serveur du paiement.
POST /webhooks/fedcheckout HTTP/1.1
FedCheckout-Event-Id: evt_9F3K…
FedCheckout-Event-Type: payment_intent.succeeded
FedCheckout-Timestamp: 1789964330
FedCheckout-Signature: 4b1f…
Content-Type: application/json
{
"id": "evt_9F3K…",
"type": "payment_intent.succeeded",
"mode": "live",
"created_at": "2026-09-21T04:38:50Z",
"data": {
"id": "fpay_pi_8K2M…",
"object": "payment_intent",
"status": "succeeded",
"amount": "500.000000",
"currency": "GDS",
"external_reference": "ORD-7781",
"payment_method": "gds_wallet"
}
}La signature est un HMAC-SHA256 calculé sur "{timestamp}.{corps brut}", avec le secret de cette destination. Le corps brut : pas un objet re-sérialisé. Deux JSON équivalents n'ont pas le même texte, donc pas la même signature.
FedCheckout-Event-Ididentifiant de l'événement, stable entre deux envoisFedCheckout-Event-Typele type, par exemple payment_intent.succeededFedCheckout-Timestampsecondes Unix, à inclure dans le calculFedCheckout-SignatureHMAC-SHA256 en hexadécimalRÈGLE 1
Compare à temps constant
timingSafeEqual, hash_equals, compare_digest — jamais ===.
RÈGLE 2
Refuse les vieux envois
Au-delà de cinq minutes d'écart entre le timestamp et l'heure de réception, rejette : c'est ce qui bloque le rejeu.
RÈGLE 3
Traite une seule fois
Le même Event-Id peut arriver deux fois. Enregistre-le et ignore les doublons.
payment_intent.createdpayment_intent.processingpayment_intent.succeededpayment_intent.failedpayment_intent.cancelledpayment_intent.expiredrefund.createdrefund.succeededrefund.failedRéponds 2xx dès que tu as enregistré l'événement — le traitement peut venir après. En cas d'échec, FedCheckout réessaie avec un recul croissant, puis range la livraison en lettre morte. Tu peux la renvoyer depuis la console, dans « Journaux d'API ».