Sandbox
A deterministic, free environment that exposes the full product surface — so your integration works end-to-end before a single BRL is billed.
Every project has a sandbox environment alongside its production environment. The two share the same HTTP API, the same request and response shapes, the same webhook envelope. The only differences are:
- Sandbox keys (
tx_sandbox_...) can only operate on the sandbox environment. - Sandbox sends never reach a real phone — the adapter returns deterministic
sb_<uuid>provider ids. - Sandbox usage is free. Sandbox
UsageRecordrows havecost_brl = 0. - Templates submitted in sandbox auto-approve. No Meta App Review.
- Flows published in sandbox auto-publish. No Meta provider call.
Check sandbox readiness
Use this endpoint before the first send. It reports the current sandbox environment, whether a default sender exists, how many templates are approved and which one to default to, which scopes are missing, and the exact local listen event type: message.received.
The capabilities block answers the two questions that decide whether a send will work: send_outbound_messages (scope plus a sender) and send_template_messages (those two plus at least one approved template). They are separate because a free-form send inside an open service window can succeed while a template send still cannot.
curl https://api.tyxter.com/v1/sandbox/quickstart \
-H "authorization: Bearer $TYXTER_API_KEY"Simulate an inbound message
This is the canonical sandbox inbound endpoint and the sandbox's equivalent of a Meta webhook callback. It returns 202 Accepted, writes an inbound Message row, and fires message.received to your registered webhook endpoints with text normalized at data.content.text.body.
To open the WhatsApp 24-hour service window for a later free-form outbound send, set from to the customer phone you will send to next, and set to to the sandbox sender id used by that outbound request.
curl https://api.tyxter.com/v1/sandbox/inbound-messages \
-H "authorization: Bearer $TYXTER_API_KEY" \
-H "content-type: application/json" \
-H "tyxter-trace-id: diag_open_window" \
-d '{
"channel": "whatsapp",
"from": "+5511999999999",
"to": "sandbox_sender",
"type": "text",
"text": { "body": "Oi, qual o status do meu pedido?" }
}'Set type to "media" to rehearse an inbound attachment. The simulation stores real bytes: Tyxter creates a genuine inbound media asset — source inbound_provider, status consumed, a real byte_length — so message.received carries content.media, GET /v1/media/{asset_id}/download-url returns a working URL, the asset shows up in GET /v1/media?source=inbound_provider, and for kind: "audio" you can call POST /v1/messages/{message_id}/transcription and get a completed transcript back.
curl https://api.tyxter.com/v1/sandbox/inbound-messages \
-H "authorization: Bearer $TYXTER_API_KEY" \
-H "content-type: application/json" \
-d '{
"channel": "whatsapp",
"from": "+5511999999999",
"to": "sandbox_sender",
"type": "media",
"media": {
"kind": "audio",
"link": "https://example.com/voice-note.ogg",
"voice": true
}
}'Tyxter never fetches the reference you send. The media object requires one of id, link, asset_id, inline, or source because it is the same shape outbound sends use, but an inbound simulation ignores it and stores its own deterministic stand-in per kind instead — inbound bytes are something the provider hands Tyxter, never something the sender chooses. Only kind, mime_type, filename, and the audio-only voice signal are read. Two identical requests store byte-identical files, which is what makes an assertion on byte_lengthor on the sandbox transcript text stable across runs. Simulated inbound media counts against the environment's media storage quota exactly as an upload does, and needs object storage configured — without it the request returns 503 media_storage_not_configured before anything is written.
Opt-out keyword detection
The inbound worker scans the text body against the Brazilian opt-out keyword list (parar, sair, cancelar, stop). When a match fires:
- the contact transitions to
opted_out; - a
contact.opted_outwebhook fires; - subsequent outbound sends to that phone are rejected with
400 contact_opted_out.
You can exercise this path in sandbox by sending an inbound with body: "parar", then attempting an outbound send to the same number.
Simulate a paid payment
Sandbox payment requests can be forced to paid, failed, expired, or cancelled. Use this fixture to test payment completion branches and payment.paid webhooks without relying on a real provider checkout. The connectionless sandbox default is a deterministic hosted-link fixture; Abacate Pay options require a selected Abacate Pay sandbox connection.
curl https://api.tyxter.com/v1/sandbox/payments/pay_123/status \
-H "authorization: Bearer $TYXTER_API_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: fixture-pay-123-paid" \
-d '{ "status": "paid" }'Going to production
- Top up credits from the dashboard.
- Register a Meta connection from the dashboard.
- Submit your templates for approval. Production sends a real template rejection when Meta rejects.
- Swap your key. Same code,
tx_live_...instead oftx_sandbox_....
balance_brl <= -5 receives a 402 credit_balance_exhausted. Sandbox does not enforce this.