WASync · Developers

Your first message

Send a WhatsApp text, read the thread back, and give the customer blue ticks.

Send a text

POST /messages
curl -X POST https://developers.wasync.app/api/v1/messages \
  -H "Authorization: Bearer $WASYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"connectionId":"conn_8f3a21","to":"40700000000","text":"Hi from my app 👋"}'
200 response
{
  "messageId": "cmqj3k2ab0001xyz",
  "waMessageId": "[email protected]_3EB0A1B2C3",
  "status": "sent"
}

to is international digits only, no + — e.g. 40700000000. text is at most 4096 characters. For media, supply media instead of text: { filename, mimetype, data (base64, ≤ 16 MB), caption? } — exactly one of the two. Voice notes use mimetype: audio/ogg and a voice-*.ogg filename. Uses whatsapp.send.

Set your HTTP timeout to at least 180 seconds

A qr send normally takes 1–7 s, up to about 20 s for long texts, and the self-heal path can take ~2.5 minutes. A cloud_api send is about 1 s. A slow send is not a failure — a client that gives up at 30 s and retries sends the customer the message twice. Always pass an idempotencyKey (a UUID per logical send) and retry with the same one.

Two ids — join on the right one

The response carries two identifiers and they are not interchangeable.

IdWhere it appearsUse it for
messageId (a cuid)messageId on POST, id on GET, message.id in both webhooksThe only joinable id. Store it on send, dedupe on it, match message.status events against it.
waMessageIdwaMessageId (REST), wa_id (webhooks), wa_message_id (media send)Support tickets and debugging. It can be null — never join or dedupe on it.

One degraded case is worth coding for: { "messageId": null, "waMessageId": "…", "status": …, "persisted": false } with HTTP 200 means the message was delivered but was not persisted. Do not retry it.

Read the thread back

GET /messages
curl "https://developers.wasync.app/api/v1/messages?connectionId=conn_8f3a21&limit=20" \
  -H "Authorization: Bearer $WASYNC_API_KEY"
200 response
{
  "messages": [
    {
      "id": "cmqj3k2ab0001xyz",
      "waMessageId": "[email protected]_3EB0A1B2C3",
      "connectionId": "conn_8f3a21",
      "direction": "outgoing",
      "text": "Hi from my app 👋",
      "mediaUrl": null,
      "mediaType": null,
      "status": "delivered",
      "errorMessage": null,
      "createdAt": "2026-07-22T10:00:00.000Z"
    }
  ],
  "nextCursor": "cmqj3k2ab0001xyz",
  "hasMore": true
}

Newest first, cursor-paginated: limit is 1–100 (default 20), and cursor takes the previous response's nextCursor. Uses whatsapp.read.

Give the customer blue ticks

POST /messages/read
curl -X POST https://developers.wasync.app/api/v1/messages/read \
  -H "Authorization: Bearer $WASYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"connectionId":"conn_8f3a21","phone":"40700000000"}'

Call this when an operator opens the conversation in your UI — otherwise nobody ever opens the chat in WhatsApp, the customer's messages stay on double grey ticks and the thread looks ignored. Body is { connectionId, phone?, messageId? } with at least one of phone / messageId (the cuid of an incoming message; messageId wins when both are given). It uses whatsapp.send, not whatsapp.read, because it transmits a receipt to the customer. Idempotent, no key, no rate limit. Returns { "ok": true, "marked": 3 }.

Next

  • Be told about inbound messages instead of polling: Webhooks.
  • Build it without code: n8n or MCP.
  • Generate a client from the contract: npx @openapitools/openapi-generator-cli generate -i https://developers.wasync.app/openapi.json -g typescript-fetch -o ./wasync-client

On this page