Get started

Quickstart

Send your first WhatsApp message, receive a webhook back, and iterate — all in the sandbox, without touching a real number or being billed.

Sandbox sends are free. They never reach a real phone. Every sandbox message gets a deterministic provider id (sb_<uuid>) and a full status timeline so your integration can exercise the entire pipeline.

1. Get a sandbox key

Every Tyxter project is provisioned with a sandbox key on creation. From the dashboard, open Settings → API keys, copy the sandbox key, and keep it somewhere you can paste from.

Running locally from this repo? Export the seeded sandbox key before calling the API:

TYXTER_API_KEY=<local seeded sandbox key>

2. Check sandbox readiness

GET/v1/sandbox/quickstart
curl https://api.tyxter.com/v1/sandbox/quickstart \
  -H "authorization: Bearer $TYXTER_API_KEY"

This response tells you whether the key has the scopes needed to simulate inbound messages, listen locally for webhooks, and send outbound sandbox messages. It also makes missing sender setup explicit instead of inventing a default.

3. Send a message

POST/v1/messages
curl https://api.tyxter.com/v1/messages \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: qs-demo-1" \
  -d '{
    "to": "+5511999999999",
    "from": "sandbox_number",
    "type": "text",
    "text": { "body": "Hello from Tyxter." }
  }'

You get a 202 Accepted with the message id and trace id:

{
  "id": "msg_01HZ4...",
  "object": "message",
  "status": "accepted",
  "environment": "sandbox",
  "created_at": "2026-04-27T12:00:00.000Z",
  "trace_id": "trc_01HZ4..."
}
Every async operation carries a trace_id. It flows from the original request through every DB row, outbox event, queue job, worker log, webhook event, and billing ledger entry. Grep by trace id and you see the full story.

4. Check its status

GET/v1/messages/{'{id}'}
curl https://api.tyxter.com/v1/messages/msg_01HZ4... \
  -H "authorization: Bearer $TYXTER_API_KEY"

A sandbox message walks acceptedsentdelivered read within a few seconds, same as a real one. Expect a timeline like:

{
  "id": "msg_01HZ4...",
  "object": "message",
  "direction": "outbound",
  "status": "delivered",
  "events": [
    { "type": "message.accepted", "status": "accepted", "created_at": "..." },
    { "type": "message.sent",     "status": "sent",     "created_at": "..." },
    { "type": "message.delivered","status": "delivered","created_at": "..." }
  ]
}

5. Receive a webhook

Point a webhook endpoint at a URL you control. The webhook.site echo service is a great starting point — copy the URL and register it:

POST/v1/webhook-endpoints
curl https://api.tyxter.com/v1/webhook-endpoints \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "url": "https://webhook.site/your-uuid",
    "description": "Local testing",
    "subscribed_events": [
      "message.sent",
      "message.delivered",
      "message.failed"
    ]
  }'

Re-send step 2 — this time you will see the same events arrive at your endpoint, each signed with tyxter-webhook-signature. Read Webhooks for the signature format and retry semantics.

6. Simulate an inbound message

POST/v1/sandbox/inbound-messages
curl https://api.tyxter.com/v1/sandbox/inbound-messages \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "from": "+5511999999999",
    "to": "sandbox_number",
    "type": "text",
    "text": { "body": "ping" }
  }'

Your webhook endpoint receives a message.received event with the text at data.content.text.body. Reply with another POST /v1/messages to close the loop. That is the whole product in six steps.

Running the repo locally? pnpm sandbox:local-loop performs the quickstart check, inbound simulation, sandbox listen poll, and optional signed local forward in one command.

Next steps