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

# Test mode

> Build and test your integration without spending a kobo.

Test-mode keys (`rb_test_*`) route every send to a **simulator** that:

* **Does not** contact real carriers.
* **Does not** debit your wallet.
* **Does** fire webhooks with realistic delivery receipts.
* **Does** return deterministic outcomes based on **magic numbers**.

You build your whole integration — including DLR handling and error branching — before a single real kobo changes hands.

## Creating a test key

In the dashboard: **API keys → Create → check "test mode"**. The returned key is prefixed `rb_test_*`.

<Note>
  Keep live and test keys in separate env vars. Convention: `ROBASE_API_KEY` for prod, `ROBASE_TEST_KEY` for dev/CI.
</Note>

## Magic numbers

Send to specific numbers to simulate outcomes:

| Number ending                          | Behavior                                       |
| -------------------------------------- | ---------------------------------------------- |
| `…00000000001` (e.g. `+2340000000001`) | Delivers in \~100 ms (`sms.delivered` webhook) |
| `…00000000002`                         | Rejected synchronously with `phone_invalid`    |
| `…00000000003`                         | Delivers after 30 s (slow-carrier simulation)  |
| `…00000000004`                         | Rejected with `dnd_blocked`                    |

Any other E.164 number defaults to the `…0001` behavior (fast delivery).

## Example: exercise every branch

```typescript theme={null}
import { Robase } from '@robase/node';

const pm = new Robase(process.env.ROBASE_TEST_KEY!);

// Happy path
await pm.sms.send({ to: '+2340000000001', from: 'TESTCO', body: 'ok' });

// Permanent failure — your code should NOT retry this.
try {
  await pm.sms.send({ to: '+2340000000002', from: 'TESTCO', body: 'invalid' });
} catch (e) { console.log(e.type); } // 'phone_invalid'

// DND block — your code should exclude this recipient from future sends.
try {
  await pm.sms.send({ to: '+2340000000004', from: 'TESTCO', body: 'dnd' });
} catch (e) { console.log(e.type); } // 'dnd_blocked'

// Slow delivery — your webhook handler must handle delayed DLRs.
await pm.sms.send({ to: '+2340000000003', from: 'TESTCO', body: 'slow' });
// `sms.delivered` fires ~30 s later.
```

## Webhooks in test mode

Test-mode sends fire webhooks **exactly the same way** live sends do. Register a test webhook at a tunnel URL (ngrok, cloudflared) while developing, then swap for your production URL when you ship.

The event payload includes `test_mode: true` in the `data` object so you can branch or log differently:

```json theme={null}
{
  "type": "sms.delivered",
  "data": {
    "sms_id": "...",
    "test_mode": true,
    ...
  }
}
```

## Test mode in CI

A common CI pattern:

```yaml theme={null}
# .github/workflows/ci.yml
env:
  ROBASE_API_KEY: ${{ secrets.ROBASE_TEST_KEY }}
```

Every integration test hits real Robase with a test key. You get genuine end-to-end confidence without cost, and you catch API contract regressions immediately.

## What test mode **doesn't** simulate

<Warning>
  **Carrier-specific rejections.** Some MNOs filter certain keywords (gambling, loan promos) that our simulator passes through. Test mode catches schema / wallet / compliance issues — it doesn't replace a smoke test on real numbers before launch.
</Warning>
