Docs
ENFR
Tableau de bord
Webhooks

Webhooks

Recevez les événements sur votre serveur et vérifiez qu’ils viennent de Monerail.

Les opérateurs et les réseaux de cartes répondent en quelques secondes ou minutes, jamais instantanément. Plutôt que d’interroger l’API en boucle, donnez une adresse HTTPS à Monerail : il y envoie un événement chaque fois que quelque chose se passe.

Créer un point de terminaison

Dans le tableau de bord sous Développeurs › Webhooks, choisissez un préréglage : Tous les événements, Paiements (encaissements, paiements, reversements) ou Cartes (cartes et titulaires). Pour une sélection plus fine, par exemple collection.* seulement, passez par l’API :

bash
curl https://api.monerail.com/v1/webhook_endpoints \
  -H "Authorization: Bearer $MONERAIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhooks/monerail", "events": ["collection.*", "payout.*"]}'

L’adresse doit être en HTTPS, avec un nom de domaine (les adresses IP sont refusées) et sans utilisateur:mot_de_passe@. Sinon, la requête échoue avec 400 url_invalid.

La réponse 201 contient le secret de signature du point de terminaison (whsec_…). Conservez-le aussitôt : il ne sera plus jamais affiché, pas même si vous rejouez la requête avec la même Idempotency-Key. Si vous le perdez, supprimez le point de terminaison et créez-en un nouveau. Les points de terminaison de test et de production sont distincts, chacun avec son secret ; ceux de production demandent un dossier d’entreprise validé. Votre entreprise peut en avoir 10, test et production confondus.

Ce que vous recevez

Un POST dont le corps JSON est l’événement :

JSON
{
  "id": "3ab70bfc-785d-45ce-99de-fd9ab67e5cd4",
  "object": "event",
  "type": "collection.succeeded",
  "livemode": false,
  "created_at": "2026-10-30T13:47:58.723Z",
  "data": {
    "object": {
      "id": "3b8f6c1e-2a47-4d1b-9e35-7c0a9d2f4e61",
      "object": "collection",
      "status": "succeeded",
      "amount": 5000,
      "fee": 125,
      "net": 4875,
      "currency": "XAF",
      "client_reference": "order_42"
    }
  }
}

data.object est l’objet complet, au même format que l’API, dans son état au moment de l’envoi. Événements webhook donne un exemple complet pour chaque famille.

En-tête Contenu
Monerail-Signature t=<horodatage>,v1=<signature>
Monerail-Event-Id L’identifiant de l’événement.
Monerail-Event-Type Le type de l’événement.
Monerail-Delivery-Attempt 1 pour la première tentative, puis 2, 3…
User-Agent Monerail-Webhooks/1.0

Vérifier la signature

N’importe qui peut envoyer une requête à votre adresse. Avant de faire confiance à un événement, vérifiez sa signature :

  1. Lisez t (temps Unix, en secondes) et v1 dans Monerail-Signature.
  2. Calculez le HMAC-SHA256 de t + "." + corps brut avec le secret du point de terminaison (la chaîne entière, whsec_ compris) comme clé, en hexadécimal minuscule.
  3. Comparez-le à v1 en temps constant, et refusez les événements dont t s’écarte de plus de 5 minutes de votre horloge.

Utilisez le corps brut, tel que reçu : analyser puis resérialiser le JSON change les octets et casse la signature. L’onglet curl envoie à votre serveur local un événement de test correctement signé.

# Send a test event to your local endpoint, signed exactly as Monerail signs it
SECRET="whsec_..."   # the secret of the endpoint you are testing
BODY='{"id":"3ab70bfc-785d-45ce-99de-fd9ab67e5cd4","object":"event","type":"collection.succeeded","livemode":false,"created_at":"2026-10-30T13:47:09.807Z","data":{"object":{"id":"3b8f6c1e-2a47-4d1b-9e35-7c0a9d2f4e61","object":"collection","status":"succeeded"}}}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

# replace the URL with your endpoint
curl http://localhost:3000/webhooks/monerail \
  -H "Content-Type: application/json" \
  -H "Monerail-Signature: t=$T,v1=$SIG" \
  -d "$BODY"

Envois et nouvelles tentatives

  • Répondez avec un code 2xx en moins de 10 secondes. Toute autre réponse, un délai dépassé ou une redirection comptent comme un échec (les redirections ne sont pas suivies).
  • Après un échec, Monerail réessaie au bout de 1 minute, 5 minutes, 30 minutes, 2 heures, 5 heures, 10 heures et 24 heures : 8 tentatives en tout, sur environ 42 heures. Après la dernière, l’envoi est marqué en échec.
  • Si votre domaine pointe vers une adresse privée, de bouclage, lien-local ou réservée, l’envoi échoue avec « destination not allowed » et est retenté comme tout échec.
  • Les événements partent en général en moins de 15 secondes, mais pas toujours dans l’ordre, et un même événement peut arriver plusieurs fois.
  • Pour renvoyer un événement, utilisez Renvoyer un événement (10 fois au plus par événement et par heure). Le tableau de bord n’a pas de bouton de renvoi.

Bonnes pratiques

  • Répondez d’abord, traitez ensuite. Placez l’événement dans une file et renvoyez 200 aussitôt.
  • Dédoublonnez avec l’id de l’événement : gardez les identifiants traités et ignorez les répétitions.
  • Fiez-vous au statut, pas à l’ordre. Basez votre logique sur data.object.status, ou relisez l’objet par l’API en cas de doute.
  • Rattrapez après une panne avec Lister les événements.
  • Rapprochez régulièrement avec Lister les transactions.

Types d’événements

Typedata.objectDescription
collection.succeededcollectionUn encaissement a réussi et le montant net a été crédité sur votre solde.
collection.failedcollectionUn encaissement a échoué (refus du client, fonds insuffisants, mauvais code…).
collection.expiredcollectionLe client n’a pas validé l’encaissement à temps.
payout.succeededpayoutUn paiement est arrivé chez le bénéficiaire.
payout.failedpayoutUn paiement a échoué. Le montant et les frais ont été rendus à votre solde.
settlement.succeededsettlementUn reversement est arrivé sur votre compte de reversement.
settlement.failedsettlementUn reversement a échoué. Le montant a été rendu à votre solde.
cardholder.createdcardholderUn titulaire a été créé.
cardholder.kyc_updatedcardholderLa vérification d’identité d’un titulaire a changé de statut. Lisez verification.status.
card.createdcardUne carte a été demandée.
card.issuedcardL’émetteur a émis la carte. Elle est active.
card.issue_failedcardL’émetteur n’a pas pu émettre la carte. La recharge initiale a été rendue au solde cartes.
card.frozencardUne carte a été bloquée.
card.unfrozencardUne carte a été débloquée.
card.terminatedcardUne carte a été résiliée. Son solde restant est revenu au solde cartes.
card.fundedtransactionUne carte a été rechargée depuis le solde cartes.
card.withdrawntransactionDe l’argent a été retiré d’une carte vers le solde cartes.
card.transactiontransactionUn paiement par carte a eu lieu.
card.refundedtransactionUn marchand a remboursé un paiement par carte.
card.reversedtransactionUn paiement par carte a été annulé avant règlement.
card.authorization_declinedauthorizationUne tentative de paiement par carte a été refusée. data.object vaut { object: "authorization", card, amount, currency, merchant, reason }, avec la carte complète.
cards_balance.creditedtransactionVotre solde cartes a été crédité.

Abonnez-vous à * pour tout recevoir, ou à une famille avec collection.*, payout.*, card.*… De nouveaux types peuvent apparaître : ignorez ceux que vous ne traitez pas.