Aller au contenu

Versements

Un versement envoie de l’argent du solde de versement du marchand vers un portefeuille mobile ou un compte bancaire. Une clé prépare les versements et les lit ; seule une personne approuve ou rejette un versement, dans le portail marchand. Dans le bac à sable, tous les participants, toutes les destinations et tous les frais sont synthétiques, et aucun versement ne déplace d’argent réel.

Trois portées de clé ouvrent les routes de versement /v1 : payouts:read lit les versements, les lots et leurs fichiers, payouts:write fait les vérifications de nom, les versements et les lots et alimente le solde du bac à sable, et balance:read lit le solde de versement. Une clé n’approuve ni ne rejette jamais un versement.

Seule la délégation d’un Propriétaire ou d’un Admin donne à une clé l’une de ces portées. La clé d’amorçage du bac à sable et les clés émises par l’équipe de Futa n’en ont aucune, et keys:manage seule n’en donne jamais : POST /v1/keys demandant une portée de versement avec une telle clé répond 403 SCOPE_ESCALATION.

GET /v1/balance avec balance:read lit le solde dans les deux devises, jamais additionnées ni converties : available et reserved en unités mineures entières, l’alert_threshold de solde bas de chaque devise et below_alert, funded (faux tant qu’aucune alimentation ne l’a atteint) et l’unique funding_reference du marchand, PF-…. Les encaissements et les règlements ne l’alimentent jamais. Un versement ou un lot réserve son argent sur available quand il est soumis, et l’y rend quand il échoue, est rejeté ou expire.

{
"schema_version": 1,
"merchant_id": "sandbox-merchant",
"funding_reference": "PF-3XW8-K2NB",
"balances": [
{
"currency": "CDF",
"available": 0,
"reserved": 0,
"alert_threshold": null,
"alert_version": 0,
"below_alert": false,
"funded": false
},
{
"currency": "USD",
"available": 100000,
"reserved": 18000,
"alert_threshold": 20000,
"alert_version": 1,
"below_alert": false,
"funded": true
}
],
"as_of": "2026-10-28T09:15:00Z",
"sandbox": true
}

Dans le bac à sable, POST /v1/sandbox/payout-top-ups avec payouts:write, une clé de test et une Idempotency-Key alimente le solde en argent synthétique : le simulateur de la banque sponsor écrit un avis de crédit portant la référence de financement du marchand, et le solde montre l’argent une fois cet avis comptabilisé, jamais avant ; relisez donc GET /v1/balance. Aucun compte de financement réel, aucun virement ni aucun retrait n’existe dans le bac à sable.

{
"amount": 100000,
"currency": "USD"
}
{
"schema_version": 1,
"synthetic": true,
"notice_id": "SIM-SN-000000000001",
"funding_reference": "PF-3XW8-K2NB",
"amount": 100000,
"currency": "USD",
"credited_at": "2026-10-28T09:14:02.481233Z"
}

La même clé et le même corps n’alimentent qu’une fois. 503 TOP_UP_UNAVAILABLE laisse le résultat inconnu : renvoyez la même requête avec la même clé. Un bac à sable qui ne donne à l’API marchande aucune clé de simulateur répond ainsi à chaque alimentation, sans rien demander.

Le seuil d’alerte de solde bas se règle dans le portail marchand. Son alerte s’y affiche, et sa notification « Solde de versement bas » est enregistrée : rien n’est encore envoyé par email ni par SMS.

Chaque versement indique une vérification de nom faite pour lui dans les 24 heures. POST /v1/name-enquiries avec payouts:write et une Idempotency-Key demande au participant qui détient la destination, avec le nom que vous attendez, ou null :

{
"kind": "WALLET",
"participant": "simulator",
"destination": "+243810000001",
"expected_name": "SYNTHETIC HOLDER"
}

La réponse donne status : FOUND, UNKNOWN_ACCOUNT, ou UNAVAILABLE avec le motif NOT_CONTRACTED. Un nom trouvé n’est montré que masqué, son premier mot entier et chaque autre mot réduit à sa première lettre, avec un verdict par rapport au nom attendu : MATCH, DIFFERENT, UNAVAILABLE, ou null quand aucun n’était attendu. Un nom qui diffère de celui attendu, ou que le participant n’a pas pu donner, n’est jamais montré, même masqué. La destination n’est jamais répétée. Une vérification est gardée 24 heures, jusqu’à expires_at, et une demande faite dans ce délai répond par cette même vérification, avec cached vrai.

{
"schema_version": 1,
"synthetic": true,
"id": "00000000-0000-4000-8000-000000000011",
"kind": "WALLET",
"participant": "simulator",
"destination_masked": "+243•••••0001",
"status": "FOUND",
"holder_name_masked": "SYNTHETIC H***",
"reason": null,
"expected_name_given": true,
"verdict": "MATCH",
"checked_at": "2026-10-28T09:20:11.204117Z",
"expires_at": "2026-10-29T09:20:11.204117Z",
"cached": false
}

Les vérifications de nom ont un budget, SYNTHETIC, sur une heure glissante : au plus 5 pour une destination, 60 par personne ou par clé et 300 pour le marchand. Un renvoi exact sous sa clé ne compte qu’une fois, et les lignes d’un lot, dont la validation du lot vérifie les noms, ne comptent dans aucun d’eux. Au-delà d’une limite, 429 NAME_ENQUIRY_RATE_LIMITED répond avec Retry-After, les secondes jusqu’à ce que chaque limite atteinte ait de nouveau de la place.

POST /v1/payouts avec payouts:write et une Idempotency-Key prépare le versement et le soumet :

{
"kind": "WALLET",
"participant": "simulator",
"destination": "+243810000001",
"amount": 18000,
"currency": "USD",
"message": "Prime trimestrielle",
"merchant_reference": "PAIE-2026-10-017",
"name_enquiry_id": "00000000-0000-4000-8000-000000000011",
"acknowledge_unavailable_name": false
}
  • Les montants sont en unités mineures entières de leur devise, à partir de 1 : des cents en USD et des centimes en CDF, où un montant est toujours en francs entiers, donc un multiple de 100. 18050 vaut 180,50 dollars en USD ; 1500000 vaut 15 000 francs en CDF, et 15000 seulement 150 francs.
  • Les destinations sont un numéro de portefeuille, +243 et neuf chiffres, pour le kind WALLET, ou une référence de compte bancaire, SIM-BANK- et quatre chiffres dans le bac à sable, pour le kind BANK.
  • message est le motif du versement, au plus 100 caractères, ou null ; merchant_reference est la vôtre, de 1 à 64 caractères parmi les lettres, les chiffres, _, ., / et -, ou null.
  • acknowledge_unavailable_name vaut true seulement quand la vérification n’a pas pu donner le nom du titulaire : sans lui, un tel versement répond 409 NAME_ACKNOWLEDGEMENT_REQUIRED, et avec lui après une vérification qui a donné un nom, 400 INVALID_REQUEST.

Le montant et les frais du versement sont réservés sur le solde disponible quand il est enregistré, sinon il est refusé avec 409 INSUFFICIENT_PAYOUT_BALANCE et le shortfall, en unités mineures de sa devise :

{
"error": {
"code": "INSUFFICIENT_PAYOUT_BALANCE",
"message_en": "The available payout balance does not cover this amount and its fees: the shortfall is stated. Nothing was reserved.",
"message_fr": "Le solde de versement disponible ne couvre pas ce montant et ses frais : il manque le montant indiqué. Rien n'a été réservé.",
"shortfall": {
"amount": 4500,
"currency": "USD"
}
}
}

Chaque refus propre à une route de versement est ce corps, shortfall à null sauf pour ce code ; les refus de clé, de portée, de taille, de type de média et de quota sont le corps Error, et un 400 INVALID_REQUEST peut être l’un ou l’autre : le corps Error quand la requête est refusée avant que la route de versement ne la lise, ce corps quand la route ou le switch la refuse. Les autres refus d’un versement sont 404 NAME_ENQUIRY_NOT_FOUND, 409 NAME_ENQUIRY_STALE (la vérification date de plus de 24 heures), 409 PAYOUT_DESTINATION_UNKNOWN (le participant ne connaît pas le compte), 409 PAYOUT_ROUTE_UNAVAILABLE (le participant ne reçoit pas de versements pour le moment), 409 MERCHANT_LIMIT_EXCEEDED (le plafond par versement, ou le plafond par jour, un jour allant de minuit à minuit à l’heure de Kinshasa, Africa/Kinshasa ; tous deux SYNTHETIC dans le bac à sable), 409 PAYOUTS_HELD (Futa suspend les versements du marchand) et 409 IDEMPOTENCY_CONFLICT. La page des erreurs les liste tous par statut.

Au-delà du seuil d’approbation du marchand pour sa devise, un versement attend une deuxième personne, PENDING_APPROVAL, son argent réservé, pendant sept jours au plus ; rien n’est envoyé avant son approbation. Le seuil commence à zéro, un zéro que personne n’a fixé, si bien que chaque versement attend. Seul le Propriétaire fixe un seuil : le baisser s’applique aussitôt, tandis que le relever ou le supprimer, y compris le premier seuil au-dessus de zéro, est une proposition qu’un autre Propriétaire ou Admin approuve, sans exception pour un marchand qui n’a qu’un Propriétaire, et la proposition expire au bout de sept jours (SYNTHETIC).

Approuver et rejeter sont le fait d’une personne, dans le portail marchand, jamais de l’API : un Propriétaire ou un Admin qui n’a pas préparé le versement et n’est pas détenteur de la clé qui l’a préparé, avec une connexion récente et un motif. Les détenteurs d’une clé sont les personnes qui l’ont créée ou renouvelée, et, pour une clé créée avec une autre clé, les détenteurs de celle-ci à leur tour. Quand key_holders.known vaut false, un détenteur de cette clé n’est pas une personne connue et personne ne peut approuver le versement ; un rejet ou l’expiration rend quand même son argent. Une clé que plus de 50 personnes ont détenue ne prépare ni versement ni lot : ses commandes de préparation sont refusées 400 INVALID_REQUEST.

C’est le principe des quatre yeux, pas une panne : un versement au-dessus du seuil demande toujours une deuxième personne, et un versement égal ou inférieur au seuil n’en demande aucune. Un marchand qui n’a qu’un Propriétaire et aucun Admin, dont le Propriétaire prépare les versements ou détient la clé qui les prépare, ne peut faire approuver aucun de ces versements, ni relever le seuil seul, et chacun expire au bout de sept jours, son argent rendu. Invitez un Admin : l’un des deux crée alors la clé ou prépare le versement et l’autre l’approuve, et l’Admin approuve la proposition de seuil du Propriétaire. Un membre Finance peut aussi préparer des versements dans le portail, que le Propriétaire approuve.

Un versement envoyé plus de 24 heures après sa vérification de nom, comme peut l’être un versement approuvé plus tard, voit d’abord son nom vérifié de nouveau, comme une ligne de lot : un verdict différent l’arrête FAILED avec NAME_CHANGED, rien n’étant envoyé et son argent rendu, et tant que le participant ne répond pas, ou ne peut plus donner le nom, il reste QUEUED et la vérification est redemandée : à chaque étape, puis, après trois vérifications de suite sans réponse, au plus une fois par minute. Quinze minutes après la première vérification sans réponse, le versement est escaladé aux opérations, son argent toujours réservé jusqu’à ce qu’une réponse arrive ou que les opérations agissent (ces trois nombres sont SYNTHETIC).

201 répond par le versement tel qu’enregistré, QUEUED ou PENDING_APPROVAL. Lisez-le avec GET /v1/payouts/{id} et payouts:read, qui fait foi ; GET /v1/payouts liste les versements du plus récent au plus ancien, 100 par page.

État Signification
PENDING_APPROVAL Attend une deuxième personne dans le portail, son argent réservé.
QUEUED Enregistré sans attendre d’approbation, ou approuvé : à envoyer.
PROCESSING Envoyé au participant, pas encore définitif.
PAID La destination est créditée ; paid_at dit quand.
FAILED Il a échoué, avec son reason : le participant l’a refusé (UNKNOWN_ACCOUNT, ACCOUNT_SUSPENDED, CREDIT_REFUSED), ou la vérification de nom de Futa avant l’envoi a trouvé le nom changé (NAME_CHANGED) et rien n’a été envoyé. Son argent est de nouveau disponible.
UNCERTAIN La réponse du participant n’est pas encore connue : son argent reste réservé, et il n’est jamais renvoyé sous une autre clé.
REJECTED Rejeté par la personne chargée de l’approuver. Son argent est de nouveau disponible.
EXPIRED Non approuvé dans les sept jours. Son argent est de nouveau disponible.
{
"schema_version": 1,
"id": "00000000-0000-4000-8000-000000000021",
"reference": "PO-7G2K-9Q4M",
"merchant_id": "sandbox-merchant",
"kind": "WALLET",
"participant": "simulator",
"destination_masked": "+243•••••0001",
"name": {
"enquiry_id": "00000000-0000-4000-8000-000000000011",
"status": "FOUND",
"holder_name_masked": "SYNTHETIC H***",
"verdict": "MATCH",
"checked_at": "2026-10-28T09:20:11.204117Z",
"acknowledged_unavailable": false
},
"amount": 18000,
"fee": 0,
"total": 18000,
"reserved": 18000,
"currency": "USD",
"fee_policy": "synthetic-zero-fee-v1",
"message": "Prime trimestrielle",
"merchant_reference": "PAIE-2026-10-017",
"state": "PENDING_APPROVAL",
"reason": null,
"prepared_by": "key:00000000-0000-4000-8000-000000000004",
"key_holders": {
"known": true,
"holders": ["user:00000000-0000-4000-8000-000000000003"]
},
"threshold_version": 0,
"approval": {
"expires_at": "2026-11-04T09:21:30.112094Z",
"approved_by": null,
"rejected_by": null,
"decided_at": null,
"reason": null
},
"escalated": false,
"version": 1,
"created_at": "2026-10-28T09:21:30.112094Z",
"updated_at": "2026-10-28T09:21:30.112094Z",
"paid_at": null,
"sandbox": true
}

Un lot paie de nombreuses destinations dans une seule devise. Par l’API, une clé le prépare ; une personne l’approuve dans le portail.

  1. Le créer. POST /v1/payout-batches avec payouts:write et une Idempotency-Key indique sa devise et combien de lignes arriveront, au plus 100 000. Il répond par le lot, UPLOADED, avec sa référence PB-…. Un marchand garde au plus 20 lots ouverts, SYNTHETIC : au-delà, 409 PAYOUT_BATCH_TOO_LARGE. GET /v1/payout-batches/{id} lit un lot, et GET /v1/payout-batches les liste du plus récent au plus ancien, 100 par page.

    {
    "currency": "USD",
    "expected_lines": 3
    }
  2. Envoyer ses lignes par tranches. POST /v1/payout-batches/{id}/lines prend au plus 1 000 lignes et 512 Kio par appel, chaque ligne numérotée dans le lot, dans sa devise, son montant en unités mineures entières exactes. Le numéro de la tranche, de 1 au nombre de lignes annoncées, l’identifie : la même tranche renvoyée ne change rien, quelle que soit sa clé, et une tranche refusée peut être renvoyée avec des lignes corrigées sous son numéro. Voici les trois lignes de l’exemple de fichier plus bas :

    {
    "chunk": 1,
    "lines": [
    {
    "number": 1,
    "kind": "WALLET",
    "participant": "simulator",
    "destination": "+243810000001",
    "expected_name": "SYNTHETIC HOLDER",
    "amount": 18000,
    "currency": "USD",
    "message": "Prime trimestrielle",
    "merchant_reference": "LIGNE-1"
    },
    {
    "number": 2,
    "kind": "WALLET",
    "participant": "simulator",
    "destination": "+243810000002",
    "expected_name": null,
    "amount": 25050,
    "currency": "USD",
    "message": "Salaire, octobre",
    "merchant_reference": "LIGNE-2"
    },
    {
    "number": 3,
    "kind": "WALLET",
    "participant": "simulator",
    "destination": "+243810000003",
    "expected_name": null,
    "amount": 1200,
    "currency": "USD",
    "message": null,
    "merchant_reference": null
    }
    ]
    }
  3. Suivre sa validation. Une fois chaque ligne annoncée arrivée, le lot est VALIDATING, puis VALIDATED : chaque ligne est vérifiée, son nom compris, et est VALID, REVIEW (à revoir) ou EXCLUDED. Une ligne dont tous les problèmes sont parmi NAME_DIFFERENT, NAME_UNAVAILABLE, DUPLICATE_PAYOUT, DUPLICATE_REFERENCE et ROUTE_PAUSED est à revoir, en attente de la décision du préparateur ; tout autre problème, INVALID_DESTINATION, ROUTE_UNAVAILABLE, INVALID_AMOUNT, MERCHANT_LIMIT_EXCEEDED, UNKNOWN_ACCOUNT, INVALID_EXPECTED_NAME, INVALID_MESSAGE ou INVALID_REFERENCE, exclut la ligne jusqu’à ce qu’elle soit corrigée. GET /v1/payout-batches/{id}/validation donne sa progression, ses décomptes, ce que ses lignes valides réserveraient et ses lignes à problème, et GET /v1/payout-batches/{id}/lines ses lignes, masquées, 1 000 par page, éventuellement d’un seul state. GET /v1/payout-batches/{id}/report donne le rapport de validation sous forme de fichier à partir de VALIDATED, ou une fois qu’un lot jamais validé est LAPSED.

  4. Décider des lignes à revoir. Tant que le lot est VALIDATED, POST /v1/payout-batches/{id}/lines/{line}/decision prend ACCEPT, KEEP, REMOVE ou CORRECT à la version du lot. ACCEPT et KEEP ne s’appliquent qu’à une ligne à revoir, et sont traités de la même façon : l’un comme l’autre rend la ligne VALID, et la décision est enregistrée telle qu’envoyée. Une ligne ROUTE_PAUSED, dont la route du participant était suspendue lors de sa vérification, n’accepte ni l’un ni l’autre (409 PAYOUT_ROUTE_UNAVAILABLE) : corrigez-la une fois la route rouverte, ou retirez-la. Une correction porte les nouvelles valeurs de la ligne sous son propre numéro et est vérifiée de nouveau.

    {
    "expected_version": 4,
    "decision": "REMOVE",
    "correction": null
    }
  5. Le soumettre. POST /v1/payout-batches/{id}/submit à la version du lot l’envoie en approbation. Le total de ses lignes valides, montants et frais, est réservé sur le solde disponible, sinon il est refusé avec 409 INSUFFICIENT_PAYOUT_BALANCE et le manque ; le lot est alors PENDING_APPROVAL.

    {
    "expected_version": 5
    }
  6. Une personne l’approuve ou le rejette, dans le portail. Chaque lot attend une deuxième personne, quel que soit son total, pendant sept jours au plus, et rien n’est envoyé avant son approbation. Le principe des quatre yeux s’applique : un Propriétaire ou un Admin l’approuve ou le rejette, avec une connexion récente et un motif, et jamais quelqu’un qui l’a préparé (qui l’a créé, a envoyé une tranche, a décidé d’une ligne ou l’a soumis) ni le détenteur d’une clé parmi eux. Un lot garde au plus 100 préparateurs et 50 détenteurs de clé : une commande qui en ajouterait un de plus est refusée 409 PAYOUT_BATCH_INVALID. Approuvé, il est APPROVED, PROCESSING, puis COMPLETED une fois chaque ligne définitive ; sinon REJECTED ou EXPIRED, son argent de nouveau disponible. Un lot jamais soumis qui reste sept jours inchangé est LAPSED, SYNTHETIC, et n’a jamais retenu d’argent.

  7. Lire son résultat. GET /v1/payout-batches/{id}/result donne le fichier de résultat une fois le lot COMPLETED, REJECTED, EXPIRED ou LAPSED. Le rapport et le résultat sont écrits de façon asynchrone : 202 avec Retry-After pendant l’écriture du fichier, puis le fichier ; 409 RESULT_NOT_READY avant que le lot atteigne l’état qu’il décrit.

  8. Relancer ses lignes échouées. POST /v1/payout-batches/{id}/retry à la version du lot (« Relancer ») crée un nouveau lot, retry_of le premier, des lignes que le participant a refusées définitivement, chacune liée à sa ligne d’origine et portant sa référence. La relance ne réserve rien : ses lignes sont vérifiées de nouveau, puis soumises, réservées et approuvées de nouveau comme une nouvelle soumission. Une ligne incertaine n’est jamais relancée ; sans ligne échouée à relancer, 409 PAYOUT_LINE_NOT_RETRYABLE.

futa-payout-batch-v1 est le modèle CSV propre à Futa, intitulé « modèle Futa » partout où il est proposé : ce n’est ni le format d’une banque, ni celui d’un régulateur, ni celui d’une norme. Une personne le téléverse dans le portail marchand, qui propose aussi le modèle ; le téléversement est une action du portail, et une clé envoie les mêmes lignes en JSON par POST /v1/payout-batches/{id}/lines. Le modèle est figé en version 1 : tout changement est un nouveau modèle.

  • Encodage. UTF-8, avec ou sans marque d’ordre des octets.
  • Séparateur. Une virgule ; un fichier séparé par des points-virgules n’a pas d’en-tête valide.
  • Enregistrements. Un par ligne, terminé par un saut de ligne ou par un retour chariot et un saut de ligne ; la dernière ligne peut se terminer sans.
  • Guillemets. Une cellule peut être entourée de guillemets doubles, ce qui lui permet de contenir une virgule ; un guillemet double à l’intérieur s’écrit deux fois.
  • En-tête. Exactement type,participant,numero,nom_attendu,montant,devise,motif,reference, dans cet ordre et en minuscules.
  • Limites. Au moins 1 ligne et au plus 100 000 ; au plus 16 Mio (16 777 216 octets), marque d’ordre des octets comprise.
  • Une seule devise. Chaque ligne est dans la devise du fichier.
  • Cellules telles qu’écrites. Aucune cellule n’est rognée ni réinterprétée.
  • Formules. Une cellule contenant un caractère de contrôle, ou dont le premier caractère autre qu’une espace est =, +, - ou @, est refusée (UNSAFE_CELL) : un montant négatif en est une.
Colonne Contient Devient dans la ligne
type wallet ou bank, en minuscules kind : WALLET ou BANK
participant Le code du participant : simulator ou synthetic-bank dans le bac à sable participant
numero Un numéro de portefeuille en chiffres, sans le signe plus (243810000001) ; une référence de compte bancaire (SIM-BANK-0001) destination : +243810000001, ou la référence telle qu’écrite
nom_attendu Facultatif : le nom du titulaire que vous attendez expected_name, ou null si la cellule est vide
montant Le montant dans l’unité de la devise avec un point décimal : au plus deux décimales en USD (180, 180.5, 180.50), aucune en CDF (15000) amount en unités mineures : 180.50 USD vaut 18050, 15000 CDF vaut 1500000
devise USD ou CDF, en majuscules currency
motif Facultatif : le message du versement message, ou null si la cellule est vide
reference Facultatif : votre propre référence merchant_reference, ou null si la cellule est vide

Un tableur enregistre un long nombre comme 243810000001 sous la forme 2.4381E+11 si sa colonne n’est pas au format texte : mettez numero au format texte avant d’enregistrer. Un numéro de portefeuille sous sa forme nationale, 0810000001, est signalé sur sa ligne (INVALID_DESTINATION).

type,participant,numero,nom_attendu,montant,devise,motif,reference
wallet,simulator,243810000001,SYNTHETIC HOLDER,180.00,USD,Prime trimestrielle,LIGNE-1
wallet,simulator,243810000002,,250.5,USD,"Salaire, octobre",LIGNE-2
wallet,simulator,243810000003,,12,USD,,

Un fichier qui enfreint le modèle est refusé en entier avant qu’aucune ligne soit envoyée : 422 PAYOUT_FILE_INVALID avec ses 100 premiers problèmes, chacun avec sa ligne et sa colonne quand il les a, et more_problems quand il y en a davantage. Un fichier de plus de 16 Mio est refusé 413 REQUEST_TOO_LARGE avant d’être lu en entier. Les valeurs que le modèle lit, comme un participant inconnu, un montant nul ou un nom différent, sont jugées sur leur ligne par la validation, sans jamais refuser le fichier entier. Tant que le lot qu’un fichier a créé est ouvert, le même fichier renvoyé par la même personne répond par ce lot, quelle que soit sa clé. Une fois le lot terminé, le même fichier renvoyé sous une nouvelle Idempotency-Key crée un nouveau lot, validé et approuvé de nouveau ; sous la clé de son premier envoi, il répond encore par le lot qu’il a créé jusqu’à ce que les 720 heures de cette clé soient passées, puis il est refusé 409 IDEMPOTENCY_CONFLICT.

Le rapport de validation et le résultat qu’écrit Futa sont des fichiers UTF-8 avec une marque d’ordre des octets, leurs lignes terminées par un retour chariot et un saut de ligne, chaque cellule entre guillemets, et une cellule qu’un tableur lirait comme une formule écrite après une apostrophe. Les destinations sont masquées et le nom d’un titulaire n’apparaît que masqué, quand il correspondait ou qu’aucun n’était attendu.

  • Le rapport, un enregistrement par ligne que la validation a signalée ou dont le préparateur a décidé : ligne,etat,problemes,decision,numero_masque,nom_masque,montant,devise.
  • Le résultat, un enregistrement par ligne une fois le lot terminé : ligne,reference_futa,type,participant,numero_masque,montant,devise,motif,reference,etat,raison,ligne_relancee, où reference_futa est le PB-…/NNNNNN de la ligne et ligne_relancee désigne la ligne d’origine de la ligne d’une relance.
  • Un lot caduc (LAPSED) a lui aussi son rapport et son résultat écrits, chacun ne listant que les lignes vérifiées avant sa caducité.

Chaque commande de versement prend une Idempotency-Key de 8 à 128 lettres, chiffres, _ et -, propre au marchand et à la clé qui l’envoie :

  • Pendant 720 heures, la même clé avec le même corps répond par le premier résultat de la commande, quoi qu’il se soit passé depuis ; la même clé avec un autre corps répond 409 IDEMPOTENCY_CONFLICT.
  • Après 720 heures, une commande renvoyée sous sa clé est refusée 409 IDEMPOTENCY_CONFLICT et jamais envoyée : la clé est gardée dix ans, si bien qu’elle ne fait jamais un second versement.
  • Une réponse perdue, un délai dépassé, 503 SWITCH_UNAVAILABLE ou 503 TOP_UP_UNAVAILABLE, laisse le résultat inconnu : renvoyez la même requête sous la même clé avec le même corps.
  • Un résultat incertain n’est jamais renvoyé sous une nouvelle clé : lisez plutôt le versement ou le lot.

Une tranche de lignes est connue par son lot et son numéro plutôt que par sa clé, une alimentation n’alimente qu’une fois sous sa clé, et les clés commençant par payout-batch- ou payout-file-, que Futa crée pour ses propres commandes, sont refusées 400 INVALID_REQUEST. Une décision, une soumission et une relance couvrent chacune une ligne ou un lot à la version envoyée : réutiliser leur clé pour une autre répond 409 IDEMPOTENCY_CONFLICT.

Les versements et les lots envoient ces événements au point de réception du marchand, signés comme tout événement :

Événement Envoyé par défaut Quand
payout.pending_approval Non Un versement attend une deuxième personne.
payout.completed Oui Un versement est payé (PAID).
payout.failed Oui Un versement a échoué, avec son motif.
payout.rejected Non Un versement a été rejeté par la personne chargée de l’approuver.
payout.expired Non Un versement n’a pas été approuvé dans les sept jours.
payout_batch.validated Non Les lignes d’un lot sont vérifiées, prêtes pour les décisions.
payout_batch.completed Oui Chaque ligne d’un lot est définitive.

Un événement de versement suit payout-event.schema.json et un événement de lot payout-batch-event.schema.json ; payout-event.vectors.json et payout-batch-event.vectors.json contiennent des événements enregistrés. QUEUED, PROCESSING et UNCERTAIN n’envoient aucun événement, et les lignes d’un lot n’en envoient aucun qui leur soit propre. Un événement ne porte que key_holders.known : qui détient quelle clé reste derrière les lectures authentifiées. Dédupliquez sur l’id de l’événement, n’appliquez l’événement d’un versement que si son data.version dépasse le dernier appliqué pour ce versement, et lisez GET /v1/payouts/{id} ou GET /v1/payout-batches/{id} en cas de doute : un événement est un avis, pas l’enregistrement.

{
"id": "00000000-0000-4000-8000-000000000031",
"type": "payout.completed",
"schema_version": 1,
"occurred_at": "2026-10-28T10:02:41.512903Z",
"data": {
"schema_version": 1,
"id": "00000000-0000-4000-8000-000000000021",
"reference": "PO-7G2K-9Q4M",
"merchant_id": "sandbox-merchant",
"kind": "WALLET",
"participant": "simulator",
"destination_masked": "+243•••••0001",
"name": {
"enquiry_id": "00000000-0000-4000-8000-000000000011",
"status": "FOUND",
"holder_name_masked": "SYNTHETIC H***",
"verdict": "MATCH",
"checked_at": "2026-10-28T09:20:11.204117Z",
"acknowledged_unavailable": false
},
"amount": 18000,
"fee": 0,
"total": 18000,
"reserved": 0,
"currency": "USD",
"fee_policy": "synthetic-zero-fee-v1",
"message": "Prime trimestrielle",
"merchant_reference": "PAIE-2026-10-017",
"state": "PAID",
"reason": null,
"prepared_by": "key:00000000-0000-4000-8000-000000000004",
"key_holders": {
"known": true
},
"threshold_version": 0,
"approval": {
"expires_at": "2026-11-04T09:21:30.112094Z",
"approved_by": "user:00000000-0000-4000-8000-000000000001",
"rejected_by": null,
"decided_at": "2026-10-28T10:01:12.004211Z",
"reason": "Prime validée"
},
"escalated": false,
"version": 4,
"created_at": "2026-10-28T09:21:30.112094Z",
"updated_at": "2026-10-28T10:02:41.512903Z",
"paid_at": "2026-10-28T10:02:41.512903Z",
"sandbox": true
}
}

Un nouveau point de réception reçoit par défaut payout.completed, payout.failed et payout_batch.completed, en plus des événements de paiement et de remboursement que liste le guide des webhooks ; choisissez les autres par sa configuration. Un point de réception enregistré avant que les événements de versement soient livrés garde les événements qu’il avait : ajoutez-lui les événements de versement pour les recevoir.

Les versements du bac à sable n’atteignent que les participants synthétiques du simulateur : simulator, le portefeuille de test (« Portefeuille simulé »), et synthetic-bank, la banque synthétique (« Banque simulée »). Six numéros de portefeuille et six références bancaires choisissent comment le participant répond à un crédit et à une vérification de nom ; la référence bancaire qui a le même dernier chiffre répond comme son numéro de portefeuille.

Destination Crédit Vérification de nom
+243810000001, SIM-BANK-0001 Crédité aussitôt FOUND, SYNTHETIC H***
+243810000002, SIM-BANK-0002 Refusé : UNKNOWN_ACCOUNT UNKNOWN_ACCOUNT
+243810000003, SIM-BANK-0003 Refusé : ACCOUNT_SUSPENDED FOUND, un autre nom : SYNTHETIC O*** H***, ou DIFFERENT par rapport à SYNTHETIC HOLDER
+243810000004, SIM-BANK-0004 Aucune réponse tant que le contrôle de test du simulateur ne la règle pas, ce qu’aucune clé marchande ne peut faire : le versement reste UNCERTAIN FOUND, SYNTHETIC H***
+243810000005, SIM-BANK-0005 Accepté, puis crédité 2 secondes plus tard FOUND, SYNTHETIC H***
+243810000006, SIM-BANK-0006 Indisponible au premier envoi, crédité quand il est renvoyé sous le même crédit UNAVAILABLE, NOT_CONTRACTED : prenez-en acte

Un crédit refusé fait échouer son versement avec ce motif. Toute autre destination bien formée est un compte que le simulateur ne détient pas : sa vérification de nom répond UNKNOWN_ACCOUNT et son crédit est refusé de même. Un versement unique n’est jamais préparé sur une vérification qui n’a trouvé aucun compte. Les deux noms sont synthétiques et le disent.

Tous les frais sont le zéro synthétique, fee_policy synthetic-zero-fee-v1, et chaque plafond et chaque seuil est SYNTHETIC : les plafonds par versement et par jour du marchand, le seuil d’approbation qui commence à zéro, les sept jours d’une approbation et de la caducité d’un lot, les 20 lots ouverts et le budget de vérifications de nom. Aucun n’est une valeur de production.

  • XLSX. Un classeur est refusé avec son motif (XLSX_NOT_ACCEPTED), et il n’y a pas de « Modèle XLSX » : envoyez du CSV.
  • Modèles enregistrés. « Enregistrer comme modèle » et « Utiliser un modèle enregistré » ne sont pas construits.
  • SMS au bénéficiaire. Le message d’un versement est enregistré et transmis au participant ; aucun SMS n’est envoyé.
  • Alertes de solde bas par email ou SMS. L’alerte s’affiche dans le portail marchand et sa notification est enregistrée ; rien n’est envoyé.