Failure Simulation
Happy paths are easy to test. The hard part is making sure your integration does the right thing when a send fails — and failures are, by nature, hard to trigger on demand. Failure Simulation makes them reproducible: pick a failure type in the app, and the API returns exactly that error for every send, so you can prove your retry and error-handling code works before it ever meets a real one.
Like Dry Run, no real SMS is sent and no load is spent.
Turning it on
Failure Simulation is a host-wide mode set in the app, and it persists across restarts. While it’s
active, every POST /api/sms/send returns the failure you selected. The simulation field on
GET /health and GET /api/diagnostics reports which mode is active.
The failures you can simulate
| Mode | HTTP | Error code | Simulates |
|---|---|---|---|
| Always Fail | 502 | send_failed | A generic send failure |
| No Signal | 502 | no_service | No signal / no service |
| Rate Limited | 429 | rate_limited | The rate limit being exceeded |
| Unauthorized | 401 | unauthorized | A rejected API key |
Every simulated response carries "success": false and "smsSent": false, echoes the active
mode and simulation, and its message is literally prefixed Simulated: so a simulated failure
is never mistaken for a real one.
A Rate Limited simulation includes the same Retry-After header and retryAfterSeconds field
(60) a genuine rate-limit response would — so your back-off logic gets the real signal to test
against:
{
"success": false,
"smsSent": false,
"mode": "real",
"simulation": "rate-limited",
"error": { "code": "rate_limited", "message": "Simulated: rate limit exceeded." },
"retryAfterSeconds": 60
}
Not a security bypass
The Unauthorized simulation is applied only after a real, successful key check — so it lets
you test how your client reacts to a 401, without ever weakening authentication. A request with a
genuinely bad key is still rejected the normal way, before simulation is even considered.