LibrePay Console →
Documentation

Démarrer

IntroductionDémarrageAuthentification

API

PaiementsPage de paiementDécaissementsIBAN de collecteRelevé de compteWebhooksRéférenceErreurs

Outils

Numéros de testSDK JavaScriptVersions

Versions et changements

Comment l'API évolue

Nous ne cassons pas une intégration qui fonctionne. Concrètement :

Tout changement qui romprait ces règles serait servi sous une nouvelle version d'API, et l'ancienne continuerait de répondre.

La version

Elle est datée : 2026-08-25. Vous la retrouvez dans chaque événement de webhook, champ api_version — utile quand un événement dort en file pendant une montée de version.

Journal

2026-09-27

Vos webhooks se paramètrent entièrement, par mode, depuis la console et par l'API. Nouvelle route /v1-webhook-endpoints : déclarer, lire, modifier (adresse, description, événements), désactiver et réactiver, changer le secret, envoyer un événement de test. Le catalogue des événements est servi par GET /v1-webhook-endpoints/event-types. Une clé ne gère que les événements de son service.

Dans la console, un endpoint se crée désormais dans le mode affiché : les endpoints de production se déclarent en mode production. Un endpoint reçoit tous les événements, ou ceux que vous choisissez.

Nouveau type d'événement : webhook.test, émis uniquement quand vous le demandez.

2026-09-26

Le domaine de la page de paiement peut dépendre de votre compte. Pour certains comptes, url (session de checkout) désigne une page servie sur un autre domaine que LibrePay. Transmettez-la telle quelle, sans la filtrer ni la reconstruire ; les liens déjà émis restent valables. Voir le checkout.

Une demande de décaissement retenue se conclut, et le dit. Au-delà d'un plafond, POST /v1/payouts répond 202 avec une demande (por_…). Elle porte désormais payout_id une fois approuvée (le champ disparaissait jusqu'ici), refusal_reason et decided_at ; elle expire d'elle-même après 48 h ; et trois événements la concluent : payout_request.approved, .refused, .expired. Voir les décaissements.

Annuler une session de checkout. POST /v1-checkout/{id}/cancel referme une session encore ouverte : son lien cesse d'être payable avant expires_at. Rejouable, cloisonnée par mode, refusée (409 checkout_not_cancellable) dès qu'un paiement est en route ou conclu. Voir la page de paiement. Ajout au SDK : annulerSessionCheckout(id).

2026-09-24

Le service IBAN existe en sandbox. Une clé de test recevait 503 : aucune banque n'était configurée hors production, et l'on ne pouvait éprouver ni l'ouverture d'un IBAN ni l'arrivée d'un virement avant d'ouvrir de vrais comptes. Une banque simulée ouvre désormais des comptes de test (numéros en 999…, scénarios …0003 incertain et …0008 refusé), et POST /v1-ibans/{id}/simulate-transfer fait arriver un virement par le même chemin qu'un virement réel, jusqu'à topup.succeeded.

Le relevé dit aussi votre solde de transfert. GET /v1/statements rend désormais transfer_balance et transfer_balance_at. Depuis que le balayage automatique verse la collecte vers le transfert toutes les cinq minutes, closing_balance seul retombait près de zéro alors que l'argent était chez nous : un rapprochement comptable concluait à un écart de la totalité des fonds. Champ ajouté, rien ne change pour qui l'ignore.

Les webhooks de décaissement portent enfin de quoi s'en servir. La charge d'un payout.pending, payout.succeeded ou payout.failed contenait { id, amount } et rien d'autre. Elle contient désormais la même chose qu'un payment.* : object, currency, status, operator, fee_amount, net_amount, merchant_reference, created_at.

⚠ data.id CHANGE DE VALEUR, ET C'EST UNE EXCEPTION ASSUMÉE À LA RÈGLE ÉNONCÉE PLUS HAUT. Il portait notre identifiant interne — un UUID que l'API publique ne rend nulle part. POST /v1/payouts répond txn_…, GET /v1/payouts/{id} attend txn_… : l'événement était donc le seul endroit où circulait une valeur inutilisable, et aucun code d'intégration ne pouvait en dépendre sans être déjà cassé. Nous l'alignons sur l'identifiant public plutôt que de laisser vivre une incohérence que nous aurions à expliquer à chaque intégration.

Mesuré avant correction, en sandbox : 1 480 payout.succeeded, pas un seul rapprochable par son destinataire.

⚠ Un fee_amount sur un payout.failed n'est pas une dépense. L'échec rend le montant commission comprise, et la charge porte reverted: true. Si reverted est vrai, l'opération vous a coûté zéro, quel que soit fee_amount.

2026-09-22

Le virement bancaire, par l'opérateur bda_ci. Un bénéficiaire de type bancaire — IBAN et banque — devient une destination de décaissement comme une autre. Même route, même idempotence, mêmes états.

POST /v1-beneficiaries
{ "type": "bancaire", "libelle": "Fournisseur Kouassi",
  "titulaire": "KOUASSI SARL",
  "iban": "CI93 CI201 01001 127586883703 85",
  "banque": "BANQUE D ABIDJAN", "operator": "bda_ci" }

⚠ operator désigne le CANAL, pas la banque destinataire. bda_ci est la voie par laquelle l'argent part ; la banque qui reçoit se lit dans l'IBAN, et peut être n'importe quel établissement de la zone UEMOA. Ne cherchez pas un opérateur par banque : il n'y en a pas, et il n'y en aura pas.

⚠ Un virement bancaire ne se rembourse pas. La banque n'expose aucune opération inverse : rendre l'argent est un nouveau virement, à demander comme tel. POST /v1-refunds sur un décaissement bda_ci est refusé.

⚠ Les délais ne sont pas ceux du mobile money. Un virement passe par la compensation interbancaire : comptez jusqu'à 24 h (RTGS) ou 72 h (SICA). La transaction reste pending pendant ce temps — ce n'est pas une anomalie, et la re-vérification est automatique.

failure_code possible en plus : invalid_recipient quand l'IBAN est refusé par la banque réceptrice.

L'IBAN de collecte devient accessible par API. GET /v1-ibans liste vos comptes, POST /v1-ibans en fait ouvrir un. Voir IBAN de collecte.

⚠ Une portée de clé nouvelle : iban, distincte de collect et payout. Faire ouvrir un compte bancaire est irréversible : une clé de site qui encaisse ne doit pas pouvoir multiplier les comptes. La clé se crée depuis votre console, et seulement une fois le service ouvert de notre côté.

⚠ POST /v1-ibans n'accepte pas Idempotency-Key, et c'est délibéré : une clé d'idempotence promet que rejouer rend la même réponse. Si la banque ne répond pas, nous ne pouvons pas lui demander si le compte a été créé — il n'y a donc rien à rejouer. Un 502 iban_emission_incertaine veut dire ne rejouez pas : relisez GET /v1-ibans plus tard.

Nouvel événement : iban.created. Un IBAN vient d'être ouvert à votre nom.

Nouvel événement : topup.succeeded. Un virement bancaire reçu sur votre IBAN de collecte crédite votre solde de transfert, et vous êtes prévenu. C'est le seul événement que vous n'avez pas provoqué : personne ne demande un virement entrant, on le constate.

⚠ Si votre point de terminaison n'a aucun filtre (enabled_events vide), il le reçoit déjà — c'est le comportement « vide = tous ». Prévoyez une branche par défaut, comme pour toute valeur nouvelle.

2026-09-21 (suite)

Nouveau failure_code : amount_too_small. Le montant est sous le minimum de l'opérateur — c'est l'inverse de limit_exceeded. Proposez plus, pas moins.

Il existe parce qu'un refus parfaitement explicite d'Orange — « le montant de la transaction est inférieur au montant minimum défini pour ce service » — repartait jusqu'ici en unknown. Le ranger sous limit_exceeded aurait été pire : ce code annonce un plafond, et vous auriez baissé le montant.

La liste complète des failure_code est désormais documentée dans le schéma Paiement de la spec. Elle s'allongera — prévoyez une branche par défaut.

2026-09-21

Wave est raccordé, et il ne fonctionne pas comme les autres. Orange Money, MTN MoMo et Moov Money poussent un dialogue sur le téléphone du payeur. Wave héberge sa propre page : tant que votre client ne l'a pas ouverte, il ne se passe rien, et le paiement expire.

Nouveau champ redirect_url sur le paiement. C'est l'adresse où envoyer votre client. null sur les opérateurs qui poussent — il n'y a nulle part où aller — et le seul chemin vers l'argent sur Wave.

Il est null dans la réponse de création : à cet instant l'opérateur n'a pas encore été appelé. Récupérez-le par GET /v1/payments/{id}, ou par le nouvel événement ci-dessous.

Nouvel événement payment.action_required. Émis uniquement lorsque votre client doit aller quelque part ; il porte redirect_url et ne conclut rien.

{
  "type": "payment.action_required",
  "data": {
    "id": "txn_6INMt2lIL1Pv_y6RgqZvag",
    "amount": 10000,
    "operator": "wave_ci",
    "redirect_url": "https://pay.wave.com/c/cos-xxxx"
  }
}

⚠ Correction : payment.pending n'a jamais existé. Notre page Webhooks le listait ; aucun code ne l'émet, et aucune intégration n'en a jamais reçu. Si vous l'attendiez, vous attendiez pour rien. Nous avons corrigé la page plutôt que d'inventer l'événement.

La page de paiement LibrePay gère la redirection elle-même : si vous l'utilisez, vous n'avez rien à changer.

2026-09-18

Nouvelle route publique : GET /v1-operators. Elle rend les opérateurs réellement servis aujourd'hui, par service, avec les montants acceptés et le tarif public de chacun. Sans clé — c'est l'information que notre page Tarifs affiche déjà à tout visiteur.

{
  "object": "operator_list",
  "currency": "XOF",
  "services": {
    "collect": [
      { "operator": "orange_ci", "name": "Orange Money Côte d'Ivoire",
        "min_amount": 100, "max_amount": 2000000,
        "pricing": { "percent_bp": 140, "fixed": 0, "min_fee": 25,
                     "max_fee": null, "paid_by": "merchant" } }
    ]
  }
}

Ce qu'elle vous évite. Recopier une liste d'opérateurs dans votre écran de paiement. Une liste recopiée ne vieillit pas bruyamment : proposer un opérateur qu'aucun canal ne joint fait échouer le paiement après que le payeur a saisi son numéro — au moment le plus coûteux — et en omettre un qui fonctionne fait perdre la vente sans que personne ne le sache.

Trois précisions qui comptent :

Les anciens en-têtes Valsoria-* ne sont plus envoyés. La fenêtre de compatibilité ouverte le 2026-09-07 est fermée.

En-têteÉtat
LibrePay-Request-Idle seul envoyé
LibrePay-Signaturele seul envoyé
LibrePay-Event-Idle seul envoyé
Valsoria-Request-Id, Valsoria-Signature, Valsoria-Event-Idretirés

Le User-Agent de nos webhooks devient LibrePay-Webhooks/1.0. Le SDK n'exporte plus les alias ValsoriaPay / ValsoriaPayError : employez LibrePay et LibrePayError.

Pourquoi maintenant, et pourquoi l'annonce et le retrait le même jour. La règle que nous nous sommes donnée est qu'une suppression ne se fait pas en silence. Elle ne dit pas qu'un préavis doit courir même quand il n'y a personne à prévenir. Avant de fermer, nous avons compté en production, le 2026-09-17 au soir : aucune clé API n'y a jamais existé, aucun endpoint de webhook n'y est déclaré, aucun webhook n'y a jamais été livré. La fenêtre ne protégeait donc aucune intégration — et c'était le dernier moment où la refermer était gratuit.

Le jour où il y aura des intégrations, le préavis courra pour de bon : c'est précisément pour ne pas avoir à en négocier un plus tard que nous fermons celle-ci tant qu'elle est vide.

2026-09-07

Les en-têtes prennent le nom LibrePay — et les anciens continuent d'arriver.

La plateforme s'appelle LibrePay depuis le 2026-09-03 ; les en-têtes portaient encore l'ancienne marque. Nous envoyons désormais les deux, avec la même valeur :

Nouveau nomAncien nom, toujours envoyé
LibrePay-Request-IdValsoria-Request-Id
LibrePay-SignatureValsoria-Signature
LibrePay-Event-IdValsoria-Event-Id

Rien ne casse. Lisez le nouveau nom avec l'ancien en repli (req.header('LibrePay-Signature') ?? req.header('Valsoria-Signature')), et vous serez prêts le jour où les anciens partiront — ce qui n'arrivera pas sans une annonce préalable ici.

Le SDK suit : le paquet devient @librepay/pay et la classe LibrePay. ValsoriaPay et ValsoriaPayError restent exportés comme alias.

Ce qui a été corrigé dans cette documentation le même jour, parce qu'elle affirmait des choses fausses :

2026-08-26

2026-08-25

2026-08-21

2026-08-20

Version d'API 2026-08-25 · changements · spécification OpenAPI