Chaque appel à POST /v1/payments peut spécifier un champ scenario qui pilote de façon déterministe le résultat de la simulation : statut final, latence, message d’erreur. C’est le levier principal pour tester vos chemins d’échec.
Tableau de référence
| ID | Libellé FR | Statut produit | Latence (ms) | Sévérité |
|---|
success | Succès | SUCCESS | 800–1 800 | ok |
pin_invalid | PIN invalide | PIN_INVALID | 600–1 400 | erreur |
low_balance | Solde insuffisant | INSUFFICIENT_FUNDS | 600–1 400 | erreur |
timeout | Délai dépassé | TIMEOUT | 25 000–35 000 | warn |
blocked | Compte bloqué | ACCOUNT_BLOCKED | 500–1 000 | erreur |
cancelled | Annulé utilisateur | USER_CANCELLED | 3 000–8 000 | warn |
unknown_msisdn | Numéro inconnu | UNKNOWN_MSISDN | 400–900 | erreur |
limit_exceeded | Plafond atteint | LIMIT_EXCEEDED | 500–1 000 | erreur |
maintenance | Service en maintenance | SERVICE_UNAVAILABLE | 200–600 | warn |
duplicate | Référence en double | DUPLICATE_REFERENCE | 200–500 | erreur |
La latence est tirée aléatoirement dans la fourchette indiquée à chaque appel — pour reproduire les variations naturelles d’un opérateur réel.
Passez un scenario explicite pour forcer un résultat déterministe (idéal pour les tests / la CI). Si vous l’omettez, le résultat dépend de votre registre de clients de test — voir la section ci-dessous.
Sans scénario : le registre SIM (clients de test)
Si vous omettez le champ scenario, SandPay se comporte comme un opérateur réel et utilise vos clients de test (/clients dans le tableau de bord) comme un registre SIM :
| Cas | Statut renvoyé | Final ? |
|---|
| Numéro inconnu (aucun client de test) | UNKNOWN_MSISDN | oui |
| Client trouvé + bloqué | ACCOUNT_BLOCKED | oui |
| Client actif + solde < montant | INSUFFICIENT_FUNDS | oui |
| Client actif + solde ≥ montant | PENDING | non — le client confirme sur son appareil |
Créez au moins un client de test dans /clients avant d’envoyer un paiement sans scénario, sinon tous les numéros renvoient UNKNOWN_MSISDN. Pour un résultat déterministe, passez plutôt un scenario explicite — il l’emporte toujours.
Comportement configurable par environnement : la politique unknownMsisdnPolicy vaut reject par défaut (numéro inconnu → UNKNOWN_MSISDN). En mode passthrough, un numéro inconnu est transmis à l’adaptateur opérateur (ancien comportement). Les cas bloqué / solde insuffisant restent toujours définitifs.
Exemple — déclencher pin_invalid
curl -X POST https://api.sandpay.dev/v1/payments \
-H "Authorization: Bearer $SANDPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "FCFA",
"operator": "mtn",
"country": "CI",
"msisdn": "+22507123456",
"reference": "ORDER-PIN-01",
"scenario": "pin_invalid"
}'
Réponse :
{
"id": "TX_2L9P4R",
"status": "PIN_INVALID",
"latencyMs": 940,
"scenario": "pin_invalid"
}
Statuts et finalité
Les 11 statuts canoniques sont stables — votre switch peut les compter. PENDING est le seul statut non-final : il indique qu’une transaction est en cours de traitement (utile sur les flots asynchrones). Tous les autres statuts sont définitifs et déclenchent le webhook payment.completed.
Hors-scénario : description
Le champ description (optionnel) est purement informatif — il est conservé dans la transaction et renvoyé dans le payload du webhook, mais n’influence pas le résultat.
Raw operator response (raw)
Chaque réponse de paiement (POST /v1/payments, GET /v1/payments/{id}, items de GET /v1/payments, et webhook payment.completed) contient un champ raw avec la shape native de l’opérateur correspondante au scénario joué.
Cette shape est synthétisée avec _simulated: true au top-level — elle reproduit fidèlement la structure native que renvoie chaque opérateur.
Exemple MTN — scenario: "success" :
{
"_simulated": true,
"amount": "25000",
"currency": "FCFA",
"externalId": "ORDER-2026-A1",
"payer": { "partyIdType": "MSISDN", "partyId": "22507123456" },
"payerMessage": "Premium upgrade",
"payeeNote": "ORDER-2026-A1",
"status": "SUCCESSFUL",
"financialTransactionId": "SIM_A1B2C3D4"
}
Exemple Moov — scenario: "low_balance" :
{
"_simulated": true,
"responseCode": "51",
"responseMessage": "Insufficient funds",
"transactionId": "MV-SIM_A1B2C3D4",
"externalReference": "ORDER-2026-A1",
"amount": "25000",
"currency": "FCFA",
"phoneNumber": "22507123456",
"timestamp": "2026-05-23T10:14:02.412Z"
}
Exemple Orange — scenario: "success" (notez la faute d’orthographe SUCCESSFULL reproduite fidèlement de l’API réelle) :
{
"_simulated": true,
"status": "SUCCESSFULL",
"subscriber_msisdn": "22507123456",
"amount": 25000,
"txnid": "SIM_A1B2C3D4",
"pay_token": "ORANGE_SIM_A1B2C3D4",
"confirmtxnstatus": "200",
"confirmtxnmessage": "Transaction successfully processed",
"order_id": "ORDER-2026-A1"
}
Voir aussi