Skip to content

Sending messages & media

Send

POST /v1/messages
Authorization: Bearer wt_live_...
Content-Type: application/json
Idempotency-Key: notification-987
{
  "instanceId": "wa_123",
  "recipient": "923001234567",
  "type": "text",
  "content": { "text": "Your child was marked absent today." },
  "clientReference": "attendance-987",
  "metadata": { "studentId": "s-55" },
  "expiresInSeconds": 3600
}

Response 202 Accepted:

{ "id": "msg_123", "status": "QUEUED", "instanceId": "wa_123", "clientReference": "attendance-987", "createdAt": "..." }
FieldNotes
instanceIdMust belong to the key's project and be set to connect (desiredState: CONNECTED). Messages to LOGGED_OUT or RESTRICTED instances are rejected (409 INSTANCE_STATE_CONFLICT).
recipientInternational format with or without + (923001234567, +92 300 1234567, 0092...). Numbers without a country code are accepted only if the project has a default country. Invalid numbers: 400 INVALID_RECIPIENT. Only individual chats are supported (no groups).
typetext, image, document
contenttext: { text } (max 4096 chars). image: { mediaId, caption? }. document: { mediaId, fileName?, caption? }
clientReferenceYour reference (max 200), returned in responses and webhooks, filterable.
metadataUp to 20 key/values, returned as-is.
expiresInSecondsGive up after this long (60 s – 7 days, default 24 h): status FAILED, code EXPIRED.

Idempotency-Key is required for sends; see Idempotency. Retrying with the same key returns the original response and never creates a second message.

202 QUEUED means durably accepted: the message is stored and will be sent asynchronously. It does not mean delivered.

Media

Upload first, then reference the returned id:

curl -X POST $WT/v1/media -H "Authorization: Bearer $KEY" -F "file=@report.pdf;type=application/pdf"
# -> { "id": "med_...", "kind": "document", "mimeType": "application/pdf", "sizeBytes": 48211, ... }

Or import from a public URL (POST /v1/media/import with { "url": "https://...", "fileName": "report.pdf" }). Imports follow no redirects and reject private/internal addresses.

KindTypesDefault max size
imageJPEG, PNG, WebP5 MB
documentPDF, DOC/DOCX, XLS/XLSX, PPT/PPTX, TXT, CSV, ZIP50 MB

The declared type must match the file's actual content (magic bytes), otherwise 415 UNSUPPORTED_MEDIA_TYPE. Media expires after the plan's media retention; sending with expired media fails (409 MEDIA_NOT_READY). GET /v1/media/{id}/download returns a pre-signed URL valid for 5 minutes (also for stored inbound media).

{ "instanceId": "wa_123", "recipient": "923001234567", "type": "document",
  "content": { "mediaId": "med_...", "fileName": "Report card.pdf", "caption": "Term 1 results" } }

Message lifecycle

QUEUED ──► PROCESSING ──► SENT ──► DELIVERED ──► READ
   │            │
   │            ├──► FAILED   (definitely not sent: invalid recipient, media missing, expired...)
   │            └──► UNKNOWN  (sent attempt with unconfirmed outcome; never resent automatically)
   └──► FAILED (EXPIRED / INSTANCE_DELETED while waiting)

Inbound messages: RECEIVED
StatusCertaintyMeaning
QUEUEDconfirmedStored; waiting for its turn or for the instance to connect.
PROCESSINGconfirmedA worker is sending it now.
SENTinferred → confirmedTransmitted to WhatsApp. Becomes confirmed when WhatsApp's server acknowledges it.
DELIVEREDconfirmedThe recipient's device received it.
READconfirmedThe recipient opened it (only if they have read receipts enabled).
FAILEDconfirmedWill not be sent. See failure.code.
UNKNOWNunknownA send was attempted but its outcome could not be confirmed (for example the worker stopped mid-send). It may or may not have arrived. A later receipt upgrades it automatically.
RECEIVEDconfirmedInbound message.

GET /v1/messages/{id}/events returns the full history with each step's certainty.

Delivery and read receipts are only sent by WhatsApp when the recipient's privacy settings allow it: a message can stay SENT forever even though it was read.

Failure codes

CodeWhen
INVALID_RECIPIENTWhatsApp rejected the recipient before sending
RECIPIENT_NOT_ON_WHATSAPPThe number has no WhatsApp account
MEDIA_UNAVAILABLEThe referenced media was deleted or could not be read
EXPIREDNot sent before expiresAt (e.g. the instance stayed disconnected)
INSTANCE_DELETEDThe instance was deleted while the message was queued
PROVIDER_REJECTEDWhatsApp reported an error for the message
OUTCOME_UNKNOWNUsed with status UNKNOWN

INSTANCE_NOT_CONNECTED, INSTANCE_LOGGED_OUT and CANCELLED are reserved and not currently emitted: sends to a disconnected instance stay queued and retry until they expire.

What to do with UNKNOWN

WhatsTIG does not resend UNKNOWN messages, because doing so could deliver duplicates. Decide per use case: for an attendance alert you may accept the risk of a duplicate and send again with a new idempotency key; for a payment reminder you may prefer to wait for a receipt or check with the recipient.

Ordering and pacing

Messages for one instance are sent one at a time, oldest first, paced by the instance's sending limit. Different instances send independently, so a large backlog on one account never delays another. Send order is not a delivery order guarantee.

Reading messages

GET /v1/messages (newest first) with filters instanceId, direction (outbound/inbound), status, clientReference, createdAfter, createdBefore; see Pagination. Message content is returned only while it is retained (project setting and plan retention).

Receiving messages

Incoming 1:1 messages are delivered as message.received webhooks and listed with direction=inbound. Group chats, status updates and broadcast lists are ignored. sender is the E.164 number when WhatsApp exposes it; it is null when the sender's number is hidden behind a WhatsApp privacy identifier (LID). The sender's WhatsApp profile name, when provided, is in metadata.pushName. See Webhooks.