All docs

Testing & Sandbox

Use tf_test_* keys + magic numbers to exercise every code path without burning real quota.

Sandbox keys

API keys prefixed tf_test_ route through a synthetic delivery pipeline. No real SMS or iMessage is sent. Sandbox messages live in the real messages table with is_sandbox = 1 so GET /v1/messages/:id works for them, but they are excluded from billing and quota counters.

Generate a sandbox key from the dashboard → API Keys → “Create test key”, or via the API:

curl -X POST https://api.senderz.com/admin/api-keys \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -d '{"tenant_id":"<tenant_id>","name":"sandbox","prefix":"tf_test"}'

Magic recipient numbers

Sending to these E.164 numbers from any sandbox key triggers deterministic outcomes:

RecipientOutcome
+15555550001delivered (default success path)
+15555550002failed, reason: carrier_error
+15555550003failed, reason: opted_out
+15555550004failed, reason: rate_limited
+15555550005delivered after a 5-second simulated delay (webhook timing test)
any other E.164delivered

Example — exercise the failure path:

curl -X POST https://api.senderz.com/v1/messages \
  -H "Authorization: Bearer tf_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"+15555550002","body":"will fail"}'

Response:

{
  "message_id": "sb_01HXY3M0KSANDBOXMSGID",
  "status": "failed",
  "reason": "carrier_error",
  "from": "+15555550100",
  "channel": "imessage",
  "estimated_delivery_ms": 0,
  "sandbox": true
}

The sb_ prefix on message_id distinguishes sandbox rows from production.

Webhook dispatch in sandbox

By default sandbox responses are synchronous and no webhook fires. Your production webhook handler stays clean.

Opt in by passing ?dispatch_webhook=true:

curl -X POST 'https://api.senderz.com/v1/messages?dispatch_webhook=true' \
  -H "Authorization: Bearer tf_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"+15555550005","body":"webhook timing test"}'

When opted in, we POST the standard webhook payload to your registered URL with an extra header X-Senderz-Sandbox: 1 so your handler can branch.

Sandbox quota

Sandbox messages do not count against your plan’s new-contact cap or rate limit. Abuse protection: 1000 sandbox messages per hour per key. Exceeding returns 429 SANDBOX_RATE_LIMITED.

What sandbox does NOT cover

  • BlueBubbles connectivity or device-side delivery
  • Real opt-out registry interactions (use a tenant with no opt-outs to keep it simple)
  • Voice — voice has no sandbox; use the tf_test_* key with the regular voice endpoints and Telnyx test mode instead