Sommaire · Webhooks

Webhooks

À chaque changement d’état, Galsen Pay envoie une requête POST JSON, signée, à l’adresse configurée dans le tableau de bord (Développeurs › Webhooks). C’est le moyen fiable de savoir qu’un paiement a réussi.

Configurer

  • Une adresse https publique (pas de réseau privé, pas de localhost : elle est vérifiée à l’enregistrement et à chaque envoi).
  • L’enregistrement génère un secret de signature (whsec_…), affiché une seule fois. Rangez-le dans la configuration de votre serveur.
  • Vous pouvez renouveler le secret à tout moment : l’ancien cesse aussitôt de signer.

Événements

typeQuand
payment.createdUn paiement est créé.
payment.processingLe fournisseur traite le paiement.
payment.succeededPaiement réussi et confirmé : livrez.
payment.failedPaiement échoué.
refund.created · refund.succeeded · refund.failedCycle d’un remboursement.
payout.created · payout.succeeded · payout.failedCycle d’un versement.

Contenu d’un événement

{
  "id": "0b9e5c1e-7d1f-4a2e-9a77-3c1d2b4e5f60",
  "type": "payment.succeeded",
  "created": 1762700000,
  "livemode": false,
  "data": {
    "object": { "id": "…", "reference": "GSP-…", "status": "SUCCEEDED", "amount": 10000, "currency": "XOF", "paymentId": "…", … }
  }
}

data.object est l’objet complet au moment de l’événement : le paiement (voir Paiements), le remboursement ou le versement. livemode vaut false en mode test.

Vérifier la signature

v1 = HMAC-SHA256 du texte "<t>.<corps brut>" avec votre secret, en hexadécimal. Calculez-le sur les octets exactement reçus (pas un JSON relu puis réécrit), comparez en temps constant, et refusez un horodatage trop ancien (5 minutes) contre le rejeu.

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

const app = express()

// Le corps BRUT est indispensable : la signature porte sur les octets reçus.
app.post('/webhooks/galsen-pay', express.raw({ type: 'application/json', limit: '256kb' }), (req, res) => {
  const entete = req.get('X-GalsenPay-Signature') ?? ''
  if (!signatureValide(req.body, entete, process.env.GALSEN_WEBHOOK_SECRET)) return res.sendStatus(400)

  const evenement = JSON.parse(req.body.toString('utf8'))
  res.sendStatus(200) // répondre vite (moins de 5 s), traiter ensuite
  traiterPlusTard(evenement) // file d'attente, avec dédoublonnage sur evenement.id
})

function signatureValide(corpsBrut, entete, secret, toleranceSec = 300) {
  const parties = Object.fromEntries(entete.split(',').map((p) => p.split('=')))
  const t = Number(parties.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false
  const attendu = createHmac('sha256', secret).update(`${t}.${corpsBrut}`).digest('hex')
  const recu = String(parties.v1 ?? '')
  return recu.length === attendu.length && timingSafeEqual(Buffer.from(recu), Buffer.from(attendu))
}

Livraison

  • Répondez 2xx en moins de 5 secondes. Toute autre réponse, un délai dépassé ou une redirection (jamais suivie) compte comme un échec.
  • Nouvelles tentatives après 1 min, 5 min, 15 min, 1 h, 6 h et 24 h. Ensuite, l’événement est mis de côté et Galsen Pay peut le renvoyer à la demande.
  • Livraison au moins une fois : un même événement peut arriver deux fois. Ignorez un id déjà traité.
  • L’ordre d’arrivée n’est pas garanti. En cas de doute, relisez la ressource (GET /v1/payment-intents/{id}) : son état courant fait foi.

Sans webhook

Si votre serveur était indisponible, rien n’est perdu : les nouvelles tentatives continuent pendant plus de 24 heures, et l’API reste la source de vérité à tout moment.