Valsoria Pay Console →
Documentation

Démarrer

IntroductionDémarrageAuthentification

API

PaiementsPage de paiementDécaissementsRelevé de compteWebhooksRéférenceErreurs

Outils

Numéros de testSDK JavaScriptVersions

Décaissements

Envoyer de l'argent à quelqu'un — un fournisseur, un coursier, un affilié — depuis votre solde de transfert.

C'est la seule route de cette API qui fait sortir de l'argent vers une destination que vous choisissez. Elle obéit donc à deux règles que l'encaissement n'a pas.

1. Il vous faut une clé de décaissement

Une clé d'encaissement ne décaisse pas. Elle reçoit 403 insufficient_scope, et c'est délibéré : la clé qui vit dans votre site marchand — celle qui a le plus de chances de fuiter — n'a aucun pouvoir d'envoi.

Créez une clé dédiée depuis votre console, onglet Compte, en choisissant le service Décaissement. Gardez-la sur un serveur qui ne sert qu'à ça, et révoquez-la seule si vous la soupçonnez.

2. Les plafonds ne refusent pas, ils font attendre

Deux plafonds, réglables dans votre console (onglet Décaissements) :

PlafondDéfautCe qu'il borne
Par opération100 000 FUn seul envoi
Par 24 heures500 000 FLe total glissant, demandes en attente comprises

Au-delà, l'appel ne renvoie pas d'erreur : il rend 202 et la demande part en validation dans votre console. Refuser casserait une paie légitime le jour où elle dépasse ; laisser passer viderait le compte. Un responsable tranche, avec un code reçu par courriel — le même contrôle que pour un décaissement lancé à la main.

Le plafond sur 24 h compte aussi ce qui attend : sans cela, il suffirait d'ouvrir cent demandes sous le plafond.

Les plafonds bornent la perte, ils ne l'empêchent pas. Ils ne vérifient pas la destination : une clé compromise peut envoyer vers n'importe quel numéro, jusqu'au plafond. Gardez-les au plus près de votre usage réel.

Envoyer

POST /v1-payouts
Authorization: Bearer vp_sk_test_…
Idempotency-Key: SAL-08-2026-017

{
  "amount": 50000,
  "currency": "XOF",
  "beneficiary": { "msisdn": "0700000021", "operator": "orange_ci", "name": "Koffi Y." },
  "reference": "SAL-08-2026-017"
}

La destination fournie à l'appel est enregistrée dans votre carnet : c'est ce qui vous permet de relire ensuite, dans votre console, où votre argent est parti. Vous pouvez aussi viser un bénéficiaire existant avec beneficiary_id.

La réponse dit laquelle des deux choses s'est produite

// 201 — c'est parti
{ "id": "txn_…", "object": "payout", "amount": 50000,
  "fee_amount": 250, "total_debited": 50250, "status": "created" }
// 202 — retenu, rien n'est parti
{ "id": "por_…", "object": "payout_request", "status": "pending_approval",
  "reason": "plafond_operation", "limit": 100000 }

Ne traitez pas tout 2xx comme un succès. Un 202 noté « payé » dans votre système, c'est un salaire que vous croyez avoir versé. Testez object, ou utilisez le SDK :

const r = await valsoria.creerDecaissement({ amount: 50000, beneficiary: {…} });
if (LibrePay.enAttenteDeValidation(r)) {
  // r.reason dit quel plafond, r.limit sa valeur.
}

Suivre

GET /v1-payouts/txn_… pour un décaissement, GET /v1-payouts/por_… pour une demande retenue. Une ressource d'un autre marchand rend 404, jamais 403 : répondre 403 confirmerait son existence.

Les états : createdprocessingsucceeded ou failed. Et indeterminate — l'opérateur n'a pas confirmé. Nous ne rejouons jamais un envoi incertain, et vous ne devriez pas non plus : nous vérifions auprès de l'opérateur avant de conclure.

Envoyer en masse

Un lot n'est pas « n appels unitaires ». Trois différences, et chacune vous évite un ennui :

Créer, puis exécuter — en deux gestes

POST /v1-payout-batches
Idempotency-Key: PAIE-2026-08

{ "reference": "PAIE-2026-08", "lines": [
  { "amount": 30000, "beneficiary": { "msisdn": "0700000021", "operator": "orange_ci", "name": "Koffi Y." } },
  { "amount": 40000, "beneficiary_id": "…", "reference": "SAL-002" }
] }

Le lot naît brouillon : rien n'est parti. L'exécution est un appel distinct :

POST /v1-payout-batches/pob_…/execute

C'est délibéré : on relit ce qu'on s'apprête à envoyer avant de l'envoyer, ce qui n'a de sens que si les deux gestes sont séparés. POST …/cancel annule tant que rien n'est parti.

À la création, tout est valide ou rien n'est écrit

Une ligne invalide refuse le lot entier — et le message porte le rang :

{ "error": { "code": "invalid_beneficiary",
             "message": "Ligne 47 : beneficiary.msisdn attendu : 10 chiffres…" } }

Un lot partiellement retenu vous obligerait à comparer votre fichier ligne à ligne à ce qui a été gardé — exactement le travail qu'un lot évite. Au plus 500 lignes par lot.

termine_avec_erreurs n'est pas termine

L'exécution rend 200 dans les deux cas. Vérifiez status. Une ligne qui échoue porte son failure_code et son failure_message ; les autres sont parties.

const lot = await valsoria.executerLot('pob_…');
const aReprendre = LibrePay.lignesEnEchec(lot);  // rangs + motifs

Si le total dépasse votre plafond sur 24 h, le lot naît en_attente_validation et refuse de s'exécuter (409) tant qu'un responsable ne l'a pas validé depuis la console, avec un code reçu par courriel.

Le carnet de bénéficiaires, par API

GET /v1-beneficiaries liste votre carnet, POST /v1-beneficiaries y ajoute une destination. Les deux exigent la portée payout.

La liste est paginée : limit (50 par défaut, 100 au maximum), starting_after, et une réponse qui porte total et has_more — vous n'avez donc pas à compter vous-même pour savoir s'il reste des pages.

GET /v1-beneficiaries?limit=50
→ { "data": [...], "total": 128, "limit": 50, "has_more": true }

La création est idempotente sans clé d'idempotence : le carnet est unique par (marchand, mode, coordonnée). Rejouer le même appel rend la même ligne avec un 200 au lieu d'un 201 — pas un doublon.

Ce qui manque encore

Modifier ou archiver une destination par API. On lit et on ajoute ; on ne change ni ne retire depuis l'API. Un décaissement passé doit rester lisible avec la destination qu'il visait vraiment, et une coordonnée qu'on peut réécrire à distance est exactement ce qu'une clé volée réécrirait. Les deux gestes existent dans votre console.

La destination de vos RÈGLEMENTS ne se crée pas ici — ni par cette route, ni par aucune autre. C'est le compte sur lequel nous vous versons : il se désigne dans votre console, par le propriétaire du compte, avec un code de confirmation et un délai avant première utilisation. Voir l'accueil.

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