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) :
| Plafond | Défaut | Ce qu'il borne |
|---|---|---|
| Par opération | 100 000 F | Un seul envoi |
| Par 24 heures | 500 000 F | Le 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 (ValsoriaPay.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 : created → processing → succeeded 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 :
- le plafond porte sur le TOTAL — sinon cent lignes sous le plafond unitaire passeraient pour dix fois votre plafond journalier ;
- une ligne fautive n'annule pas les autres — un numéro invalide en ligne 47 ne prive pas de salaire les 46 précédentes ;
- l'idempotence porte sur le lot — rejouer la création rend le même lot.
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 = ValsoriaPay.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.
Ce qui manque encore
Le CRUD des bénéficiaires et la pagination des listes n'existent pas.
Version d'API 2026-08-25 ·
changements ·
spécification OpenAPI