neo-pays

Webhooks

La source de vérité de vos intégrations : chaque changement d'état vous est poussé, signé, avec relivraison automatique.

Créer un endpoint

POST /v1/webhooks
{
  "url": "https://votresite.com/webhook/neopays",
  "description": "production",
  "subscribed_events": ["payment.success", "refund.success"]
}

La réponse porte le secret de signature. subscribed_events vide = tous les événements ; un événement hors catalogue est refusé plutôt que stocké, parce qu'il attendrait une livraison que rien n'émet.

Cet appel garantit, il ne crée pas toujours. Une URL déjà enregistrée revient en 200 au lieu d'être dupliquée — un script de provisionnement rejoué laisse UN endpoint, pas deux recevant chaque événement en double. Le secret est rendu dans les deux cas, parce que la raison légitime de rappeler est de l'avoir perdu.

GET    /v1/webhooks              — vos endpoints et leurs compteurs
GET    /v1/webhooks/{id}         — un endpoint
PATCH  /v1/webhooks/{id}         — modification partielle
DELETE /v1/webhooks/{id}         — suppression définitive
GET    /v1/webhooks/{id}/secret  — relire le secret
POST   /v1/webhooks/test         — livraison de test vers tous vos endpoints actifs

Le PATCH ne change que ce que vous envoyez : omettre is_active ne désactive pas l'endpoint. Pour arrêter les livraisons sans perdre l'endpoint ni son secret, envoyez is_active: false plutôt que de supprimer.

Dix endpoints par compte au maximum. Le tableau de bord fait tout cela aussi, et lui seul sait rejouer une livraison échouée et faire une rotation de secret.

Les événements

Événement Quand
payment.initiated Encaissement créé
payment.pending En attente de confirmation du payeur
payment.success Encaissé — l'événement qui fait foi
payment.failed Échec
payout.success / payout.failed / payout.cancelled Cycle de vie d'un versement
payout.refunded Versement retourné par le rail après coup
refund.success / refund.failed Cycle de vie d'un remboursement

L'enveloppe est toujours la même :

{
  "event": "payment.success",
  "created": "2026-08-24T12:00:00Z",
  "data": {
    "id": "op_01J8Z9K2QW",
    "reference": "NP-...",
    "merchant_reference": "commande-1042",
    "amount": "5000",
    "currency": "XOF",
    "status": "success"
  }
}

Le corps du webhook n'a pas la même forme que la réponse de l'API. Il porte amount et currency là où l'API porte amount_minor et asset_id, et merchant_reference là où elle porte client_ref. C'est un contrat distinct, écrit avant l'autre et inchangé depuis : ne supposez pas qu'un objet de l'une se décode comme un objet de l'autre.

Vérifier la signature

Chaque livraison porte l'en-tête :

X-NeoPays-Signature: t=<horodatage unix>,v1=<hmac>

hmac = HMAC-SHA256(secret, "<t>.<corps brut>"), en hexadécimal. Vérifiez en trois temps :

const crypto = require('node:crypto')

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(kv => kv.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false
  const expected = crypto.createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

Le corps BRUT, pas le corps re-sérialisé. Calculez le HMAC sur les octets reçus, avant tout parsing JSON. Un re-JSON.stringify peut réordonner les clés et invalider la signature.

Pendant une rotation de secret, l'en-tête porte DEUX entrées v1=, le secret courant en premier. Vérifiez-les toutes, pas seulement la première : le jour où vous basculez votre configuration sur le nouveau secret avant la fin de la bascule plateforme, la première signature est celle de l'ancien secret, et un vérificateur qui s'arrête à la première rejette alors 100 % des livraisons.

Relivraison

Répondez 2xx en moins de quelques secondes (accusez réception, traitez ensuite). Toute autre réponse déclenche l'échelle de rejeu : 5 tentatives au total, après 60 s, 5 min, 30 min puis 2 h.

Un endpoint qui échoue 20 livraisons consécutives est suspendu une heure (les livraisons reprennent seules) ; un seul succès — y compris une livraison de test depuis le tableau de bord — le réhabilite immédiatement.

Idempotence côté récepteur

Les livraisons sont at-least-once : le même événement peut arriver deux fois. Dédupliquez sur le couple (event, data.id) ou journalisez les identifiants d'événements traités.