Get started

Quickstart

Send a sandbox WhatsApp message, inspect its trace_id, and receive a signed webhook without Meta credentials, a real sending number, or billing.

Sandbox guarantee. Messages are free, deterministic, and never reach a real phone. Once your sandbox key and sender exist, the core path takes about five minutes.

1. Before you begin

Have these three things ready:

Create a key in Settings → API keys. Starting from a fresh account or handing setup to a coding agent? Follow New customer setup first.

2. Install a client

The official @tyxter/sdk-jspackage is the recommended TypeScript and JavaScript path. It ships request and response types, stable API errors, idempotency options, trace headers, and webhook signature verification. Use your repository's existing package manager rather than creating a second lockfile.

npm install @tyxter/sdk-js

Package details and release history are available on npm. The remaining examples default to TypeScript but keep equivalent cURL and Python paths beside each operation.

Configure environment variables

Store the real key in your project's existing ignored environment file. Never commit it.

TYXTER_API_KEY=tx_sandbox_...
TYXTER_TEST_RECIPIENT=+5511999999999

3. Check sandbox readiness

GET/v1/sandbox/quickstart
import { Tyxter } from '@tyxter/sdk-js';

const apiKey = process.env.TYXTER_API_KEY;
if (!apiKey?.startsWith('tx_sandbox_')) throw new Error('Sandbox API key required');

const tyxter = new Tyxter({ apiKey });
const ready = await tyxter.sandbox.quickstart();
const senderId = ready.sender.default_sender_id;

if (!senderId || !ready.capabilities.send_outbound_messages) {
  throw new Error(
    ready.missing_scopes.length > 0
      ? `Missing scopes: ${ready.missing_scopes.join(', ')}`
      : 'Sandbox sender is not configured',
  );
}

console.info({ sender_id: senderId, capabilities: ready.capabilities });

Continue only when sender.default_sender_id is non-null and capabilities.send_outbound_messages is true. The response also names missing_scopes, webhook capabilities, approved templates, and the suggested default template; do not invent missing sender ids.

4. Open the service window and send

WhatsApp free-form text requires a customer message inside the 24-hour service window. The first request simulates that inbound message; the second sends your reply through the same public API used in production.

POST/v1/sandbox/inbound-messages
POST/v1/messages
import { randomUUID } from 'node:crypto';
import { Tyxter } from '@tyxter/sdk-js';

const apiKey = process.env.TYXTER_API_KEY;
const recipient = process.env.TYXTER_TEST_RECIPIENT;
if (!apiKey?.startsWith('tx_sandbox_')) throw new Error('Sandbox API key required');
if (!recipient) throw new Error('TYXTER_TEST_RECIPIENT is required');

const tyxter = new Tyxter({ apiKey });
const ready = await tyxter.sandbox.quickstart();
const senderId = ready.sender.default_sender_id;
if (!senderId || !ready.capabilities.send_outbound_messages) {
  throw new Error('Sandbox sender is not ready');
}

await tyxter.sandbox.inboundMessages.create(
  {
    channel: 'whatsapp',
    from: recipient,
    to: senderId,
    type: 'text',
    text: { body: 'Opening the Tyxter sandbox service window.' },
  },
  { idempotencyKey: randomUUID() },
);

const message = await tyxter.whatsapp.sendText(
  { from: senderId, to: recipient, body: 'Hello from Tyxter.' },
  { idempotencyKey: randomUUID() },
);

console.info({
  message_id: message.id,
  trace_id: message.trace_id,
  status: message.status,
});

Generate one idempotency key for each logical write and reuse that key only when retrying the same payload. A successful send returns 202 Accepted:

{
  "id": "msg_01HZ4...",
  "object": "message",
  "status": "accepted",
  "status_reason": null,
  "channel": "whatsapp",
  "environment": "sandbox",
  "template_id": null,
  "template_version_id": null,
  "template_version": null,
  "created_at": "2026-04-27T12:00:00.000Z",
  "trace_id": "trc_01HZ4..."
}
First milestone reached. Keep the message_id and trace_id. The trace id follows the request through the message row, outbox event, queue job, worker logs, webhook event, and billing ledger.

5. Inspect the message timeline

GET/v1/messages/{id}

Replace the example id with the message id returned by the send.

import { Tyxter } from '@tyxter/sdk-js';

const tyxter = new Tyxter({ apiKey: process.env.TYXTER_API_KEY! });
const detail = await tyxter.messages.retrieve('msg_01HZ4...');

console.info(detail.status, detail.events);

Sandbox messages normally move from accepted to sent, delivered, and read within a few seconds. The detail response includes the complete event timeline:

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

6. Receive signed webhooks

Choose the receiver path that matches where your app is running. Use Tyxter CLI for a local app, or register an HTTPS endpoint for deployed staging and production receivers.

Local receiver: use Tyxter CLI

No public port or tunnel required. Tyxter CLI polls GET /v1/webhook-events/listen and forwards sandbox events to your local route with the normal signed webhook envelope and headers.
docker run --rm \
  -e TYXTER_API_URL=https://api.tyxter.com \
  -e TYXTER_API_KEY=$TYXTER_API_KEY \
  -e TYXTER_WEBHOOK_FORWARD_URL=http://host.docker.internal:3000/webhooks/tyxter \
  -v tyxter-cli-data:/data \
  ghcr.io/tyxter-dev/tyxter-cli:latest listen

Change the port and path in TYXTER_WEBHOOK_FORWARD_URL to match your app. The named Docker volume preserves the listener cursor and local signing secret between runs, so your receiver can verify the forwarded raw body exactly as it would in production. This sandbox flow does not require a normal dashboard webhook endpoint.

See the official tyxter-dev/tyxter-cli repository and Webhooks for doctor, listener diagnostics, and local signing-secret steps.

Deployed receiver: register an HTTPS endpoint

Create an endpoint at a public HTTPS URL you control. For a first visual check, you can use a temporary webhook.site URL. Store the returned signing_secret; Tyxter reveals it only on creation.

POST/v1/webhook-endpoints
import { randomUUID } from 'node:crypto';
import { Tyxter } from '@tyxter/sdk-js';

const tyxter = new Tyxter({ apiKey: process.env.TYXTER_API_KEY! });
const endpoint = await tyxter.webhookEndpoints.create(
  {
    url: 'https://webhook.site/your-uuid',
    description: 'Sandbox quickstart',
    subscribed_events: [
      'message.received',
      'message.sent',
      'message.delivered',
      'message.read',
      'message.failed',
    ],
  },
  { idempotencyKey: randomUUID() },
);

console.info({
  webhook_endpoint_id: endpoint.id,
  signing_secret: endpoint.signing_secret,
});

Now simulate another inbound message. The CLI listener forwards it to your local route; a registered endpoint receives it because it explicitly subscribes to message.received. In both cases, the event text is at data.content.text.body and the request carries the three Tyxter signature headers.

POST/v1/sandbox/inbound-messages
import { randomUUID } from 'node:crypto';
import { Tyxter } from '@tyxter/sdk-js';

const tyxter = new Tyxter({ apiKey: process.env.TYXTER_API_KEY! });
const ready = await tyxter.sandbox.quickstart();

await tyxter.sandbox.inboundMessages.create(
  {
    channel: 'whatsapp',
    from: process.env.TYXTER_TEST_RECIPIENT!,
    to: ready.sender.default_sender_id!,
    type: 'text',
    text: { body: 'ping' },
  },
  { idempotencyKey: randomUUID() },
);

Verify the exact raw body with @tyxter/sdk-js/webhook-verifier, deduplicate by tyxter-webhook-id, and acknowledge quickly.

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

Next steps