Passer au contenu principal
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

IDLibellé FRStatut produitLatence (ms)Sévérité
successSuccèsSUCCESS800–1 800ok
pin_invalidPIN invalidePIN_INVALID600–1 400erreur
low_balanceSolde insuffisantINSUFFICIENT_FUNDS600–1 400erreur
timeoutDélai dépasséTIMEOUT25 000–35 000warn
blockedCompte bloquéACCOUNT_BLOCKED500–1 000erreur
cancelledAnnulé utilisateurUSER_CANCELLED3 000–8 000warn
unknown_msisdnNuméro inconnuUNKNOWN_MSISDN400–900erreur
limit_exceededPlafond atteintLIMIT_EXCEEDED500–1 000erreur
maintenanceService en maintenanceSERVICE_UNAVAILABLE200–600warn
duplicateRéférence en doubleDUPLICATE_REFERENCE200–500erreur
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 :
CasStatut renvoyéFinal ?
Numéro inconnu (aucun client de test)UNKNOWN_MSISDNoui
Client trouvé + bloquéACCOUNT_BLOCKEDoui
Client actif + solde < montantINSUFFICIENT_FUNDSoui
Client actif + solde ≥ montantPENDINGnon — 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