# How to send WhatsApp messages via API

> A practical API workflow for testing a WhatsApp send, handling accepted responses, and processing signed webhooks.

- Canonical: https://tyxter.com/blog/how-to-send-whatsapp-messages-via-api
- Published: 2026-09-21
- Updated: 2026-09-21
- Author: Tyxter
- Locale: en
- Tags: whatsapp, api, webhooks

---

To send a WhatsApp message through an API, first use a sender that is ready in the correct environment, then submit one logical message with a stable idempotency key and track the returned message identifiers. An accepted API response starts the delivery workflow; it does not prove that a recipient has received the message. For the wider integration path, start with the [integration page](/integration).

The useful goal for a first integration is not merely to make an HTTP request succeed. It is to preserve enough information to inspect the message later, receive its signed events, and retry safely when your own network path is uncertain. Tyxter's sandbox gives a deterministic way to rehearse that workflow before connecting a production WhatsApp account.

## What should you check before sending a message?

Use credentials that match the environment. A `tx_sandbox_` key belongs to sandbox and a production key belongs to production. In sandbox, call `GET /v1/sandbox/quickstart` before a send. Continue only when the response includes a non-null `sender.default_sender_id` and `capabilities.send_outbound_messages` is true. That response can also identify missing scopes and the suggested default template, so it is safer than inventing a sender identifier in application code.

For free-form text, WhatsApp requires a customer message inside the service window. The sandbox quickstart demonstrates this by simulating an inbound message first, then sending a reply. If your production flow is outside that situation, choose the documented template flow rather than treating every outbound message as interchangeable. The [quickstart](/docs/quickstart) covers the readiness call and the sandbox sequence in full.

The sandbox is a development environment, not a delivery test to a handset. It simulates inbound messages, failures, opt-outs, and delivery states without Meta credentials, a real number, or billing, and it never reaches real phones. This makes it appropriate for testing how your application reacts to API responses and webhooks without making a claim about a production recipient.

## How do you make a safe first send?

Create one idempotency key for one logical write. Keep that key when retrying the same request and payload; create a new key for a different inbound simulation or a different outbound message. The SDK example below opens a sandbox service window and sends a reply with separate keys because they are separate writes.

```ts
import { randomUUID } from 'node:crypto';
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;
const recipient = process.env.TYXTER_TEST_RECIPIENT;

if (!senderId || !recipient || !ready.capabilities.send_outbound_messages) {
  throw new Error('Sandbox sender and recipient are required');
}

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

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

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

Do not generate a replacement key just because the caller timed out. First decide whether the request represents the same logical message and has the same payload. If it does, retry with the original key. If it represents a new message, generate a new key. That boundary lets a retry be retried without turning a transient client failure into a duplicate action.

## What does an accepted response mean?

A successful send returns HTTP `202` with `status: accepted`. Keep both the `message_id` and `trace_id` from the result. They let you retrieve the message and connect API activity with later evidence. An accepted response says the API accepted the work; it is not a delivered or read receipt.

Read `GET /v1/messages/{id}`, or use the SDK message retrieval method, to observe the timeline. In the sandbox that timeline can progress through `sent`, `delivered`, or `read`. A production integration should also model failure as a possible outcome and read the documented error information rather than assuming that acceptance becomes delivery.

This distinction belongs in your product's UI and automation. Show “accepted” when that is the state you have, and reserve “delivered” for the corresponding observed message event or resource state. It avoids reporting a recipient outcome before the system has evidence for it. The [sandbox documentation](/docs/sandbox) explains which states and failure cases can be simulated while developing.

## How should an application process webhooks?

Use webhooks for message lifecycle events and validate their signature before trusting the payload. For a local sandbox receiver, the Tyxter CLI can listen for events and forward the normal signed envelope and headers to a localhost route. For a deployed receiver, register an HTTPS webhook endpoint with an idempotency key and explicit event subscriptions.

At the receiver, verify the exact raw request body with `@tyxter/sdk-js/webhook-verifier` before parsing or acknowledging it. Then deduplicate on `tyxter-webhook-id`. A receiver can be retried after a network failure, timeout, or non-2xx response, so an event handler that has already completed work must not repeat that work when it sees the same envelope again.

Your receiver should return a 2xx response after it has durably accepted the event and move slow processing to its own asynchronous path. Redirects are not followed by the webhook delivery service, and a late response is treated as a failed attempt. The [webhook guide](/docs/webhooks) documents the signing model, retry behavior, and the event envelope.

## Which IDs and states should you retain?

Persist the message identifier and trace identifier returned by the send, plus the webhook envelope identifier your receiver uses for deduplication. Store the state you actually observed and the raw request body only according to your own data-handling rules. Those records answer different questions: the message ID identifies one outbound message, the trace ID connects its path through the system, and the webhook envelope ID lets the receiver recognize a replay of that event.

For a customer reply flow, deduplicate the inbound event before generating an automated reply. If the reply call has to be retried, use the same idempotency key for that reply. The documented agent reply example uses `agent-reply-<event_id>` as that stable key shape. This pairing protects both ends of the loop: one inbound event is handled once, and one logical reply is submitted once even if your transport retries.

## What are common questions about API sends?

### Does `accepted` mean the person received the message?

No. It means the API accepted the send. Inspect the message or subscribed lifecycle events for later observed states.

### Can I test a message with a sandbox key on a real phone?

No. Sandbox sends are simulated and never reach real phones.

### Should every retry get a new idempotency key?

No. Reuse the key for a retry of the same logical write and payload. Generate a new key only for a new logical operation.

### Should I parse a webhook before checking its signature?

No. Verify the raw body first, then parse it and deduplicate the webhook envelope identifier.

Once your application can distinguish acceptance from delivery and handle retries safely, begin in the sandbox and carry that same discipline into the account integration.
