Get started

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:

Because the contracts are identical, you build once against sandbox and promote to production by swapping the key.

Check sandbox readiness

GET/v1/sandbox/quickstart

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

POST/v1/sandbox/inbound-messages

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:

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

POST/v1/sandbox/payments/{payment_id}/status

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

  1. Top up credits from the dashboard.
  2. Register a Meta connection from the dashboard.
  3. Submit your templates for approval. Production sends a real template rejection when Meta rejects.
  4. Swap your key. Same code, tx_live_... instead of tx_sandbox_....
A production key whose org has a balance_brl <= -5 receives a 402 credit_balance_exhausted. Sandbox does not enforce this.