> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sandpay.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Scenarios

> The 10 deterministic sandbox scenarios

Every call to `POST /v1/payments` can specify a `scenario` field that **deterministically** controls the simulation outcome: final status, latency, and error message. This is the primary lever for testing your failure paths.

## Reference table

| ID               | Label                     | Status produced       | Latency (ms)  | Severity |
| ---------------- | ------------------------- | --------------------- | ------------- | -------- |
| `success`        | Success                   | `SUCCESS`             | 800–1 800     | ok       |
| `pin_invalid`    | Invalid PIN               | `PIN_INVALID`         | 600–1 400     | error    |
| `low_balance`    | Insufficient funds        | `INSUFFICIENT_FUNDS`  | 600–1 400     | error    |
| `timeout`        | Timeout                   | `TIMEOUT`             | 25 000–35 000 | warn     |
| `blocked`        | Blocked account           | `ACCOUNT_BLOCKED`     | 500–1 000     | error    |
| `cancelled`      | User cancelled            | `USER_CANCELLED`      | 3 000–8 000   | warn     |
| `unknown_msisdn` | Unknown number            | `UNKNOWN_MSISDN`      | 400–900       | error    |
| `limit_exceeded` | Limit exceeded            | `LIMIT_EXCEEDED`      | 500–1 000     | error    |
| `maintenance`    | Service under maintenance | `SERVICE_UNAVAILABLE` | 200–600       | warn     |
| `duplicate`      | Duplicate reference       | `DUPLICATE_REFERENCE` | 200–500       | error    |

Latency is drawn randomly within the stated range on every call — reproducing the natural variance of a real operator.

<Tip>
  Pass an explicit `scenario` to **force** a deterministic result (ideal for tests and CI). If you omit it, the outcome depends on your test client registry — see the section below.
</Tip>

## Without a scenario: the SIM registry (test clients)

If you **omit** the `scenario` field, SandPay behaves like a real operator and uses your **test clients** (`/clients` in the dashboard) as a SIM registry:

| Case                                     | Status returned      | Final?                               |
| ---------------------------------------- | -------------------- | ------------------------------------ |
| Unknown number (no matching test client) | `UNKNOWN_MSISDN`     | yes                                  |
| Client found + blocked                   | `ACCOUNT_BLOCKED`    | yes                                  |
| Active client + balance \< amount        | `INSUFFICIENT_FUNDS` | yes                                  |
| Active client + balance ≥ amount         | `PENDING`            | no — client confirms on their device |

<Tip>
  Create at least one test client in `/clients` before sending a payment without a scenario, otherwise all numbers return `UNKNOWN_MSISDN`. For a deterministic result, pass an explicit `scenario` instead — it always takes precedence.
</Tip>

<Note>
  Configurable per environment: the `unknownMsisdnPolicy` is `reject` by default (unknown number → `UNKNOWN_MSISDN`). In `passthrough` mode, an unknown number is forwarded to the operator adapter (legacy behaviour). The blocked and insufficient-funds cases always remain final.
</Note>

## Example — triggering `pin_invalid`

```bash theme={null}
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"
  }'
```

Response:

```json theme={null}
{
  "id": "TX_2L9P4R",
  "status": "PIN_INVALID",
  "latencyMs": 940,
  "scenario": "pin_invalid"
}
```

## Statuses and finality

The 11 canonical statuses are stable — your `switch` can enumerate them. `PENDING` is the only non-final status: it indicates a transaction is currently being processed (useful for async flows). All other statuses are final and trigger the `payment.completed` webhook.

## Out-of-scenario: `description`

The `description` field (optional) is purely informational — it is stored on the transaction and returned in the webhook payload, but does not influence the outcome.

## Raw operator response (`raw`)

Every payment resource (`POST /v1/payments`, `GET /v1/payments/{id}`, items in `GET /v1/payments`, and the `payment.completed` webhook) contains a `raw` field with the **native operator shape** corresponding to the scenario played.

This shape is synthesised with `_simulated: true` at the top level — it faithfully reproduces the native structure returned by each operator.

MTN example — `scenario: "success"`:

```json theme={null}
{
  "_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"
}
```

Moov example — `scenario: "low_balance"`:

```json theme={null}
{
  "_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"
}
```

Orange example — `scenario: "success"` (note: the `SUCCESSFULL` typo is faithfully reproduced from the real API):

```json theme={null}
{
  "_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"
}
```

## See also

* [Quickstart](/en/quickstart) — first API call
* [Webhooks](/en/webhooks) — receive the final status via push
* [Errors](/en/errors) — HTTP codes and retry strategy
