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": "..." }
| Field | Notes |
|---|---|
instanceId | Must 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). |
recipient | International 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). |
type | text, image, document |
content | text: { text } (max 4096 chars). image: { mediaId, caption? }. document: { mediaId, fileName?, caption? } |
clientReference | Your reference (max 200), returned in responses and webhooks, filterable. |
metadata | Up to 20 key/values, returned as-is. |
expiresInSeconds | Give 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.
| Kind | Types | Default max size |
|---|---|---|
| image | JPEG, PNG, WebP | 5 MB |
| document | PDF, DOC/DOCX, XLS/XLSX, PPT/PPTX, TXT, CSV, ZIP | 50 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
| Status | Certainty | Meaning |
|---|---|---|
QUEUED | confirmed | Stored; waiting for its turn or for the instance to connect. |
PROCESSING | confirmed | A worker is sending it now. |
SENT | inferred → confirmed | Transmitted to WhatsApp. Becomes confirmed when WhatsApp's server acknowledges it. |
DELIVERED | confirmed | The recipient's device received it. |
READ | confirmed | The recipient opened it (only if they have read receipts enabled). |
FAILED | confirmed | Will not be sent. See failure.code. |
UNKNOWN | unknown | A 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. |
RECEIVED | confirmed | Inbound 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
| Code | When |
|---|---|
INVALID_RECIPIENT | WhatsApp rejected the recipient before sending |
RECIPIENT_NOT_ON_WHATSAPP | The number has no WhatsApp account |
MEDIA_UNAVAILABLE | The referenced media was deleted or could not be read |
EXPIRED | Not sent before expiresAt (e.g. the instance stayed disconnected) |
INSTANCE_DELETED | The instance was deleted while the message was queued |
PROVIDER_REJECTED | WhatsApp reported an error for the message |
OUTCOME_UNKNOWN | Used 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.