API reference
Messages API
Send text, a file or a saved template from a connected number, then follow the message until it is read.
Send a message
POST/v1/messagesScope messages:send
A request needs connection_id, to, and one of: text, a template, or media. The connection must be in the CONNECTED state.
| Field | Type | Description |
|---|---|---|
connection_id | string, required | The WhatsApp connection ID to send from |
to | string, required | Recipient phone number in E.164 international format |
type | string | One of: TEXT, IMAGE, VIDEO, AUDIO, DOCUMENT, LOCATION, CONTACT. Default: TEXT. |
text | string | Message text, or the caption when sending media |
content | string | Alias of text |
media_url | string | Public URL of the file to send. Use with a media type. |
media_id | string | ID of a file already uploaded to the workspace, as an alternative to media_url |
template | object | A saved template to render, found by id or by name |
template.id | string | |
template.name | string | |
template.variables | object |
curl https://api.getoxtro.com/v1/messages \
-H "Authorization: Bearer $OXTRO_API_KEY" \
-H "Idempotency-Key: order-4821-confirmation" \
-H "Content-Type: application/json" \
-d '{
"connection_id": "conn_123",
"to": "+213555123456",
"text": "Your order ORD-4821 has been confirmed."
}'{
"id": "msg_123",
"status": "queued",
"queued_at": "2026-10-10T09:41:07.000Z"
}Send an Idempotency-Key header with every send, so that a retry after a timeout does not become a second WhatsApp message.
Send a template
Templates are your own reusable messages with {{variables}}, written in the Oxtro app. They are not Meta-approved templates and need no review. Reference one by name or id and pass a value for every variable. A missing or blank variable is rejected with MISSING_TEMPLATE_VARIABLE and nothing is sent.
curl https://api.getoxtro.com/v1/messages \
-H "Authorization: Bearer $OXTRO_API_KEY" \
-H "Idempotency-Key: order-4821-confirmation" \
-H "Content-Type: application/json" \
-d '{
"connection_id": "conn_123",
"to": "+213555123456",
"template": {
"name": "order_confirmed",
"variables": {
"customer_name": "Ahmed",
"order_number": "ORD-4821"
}
}
}'Send media
Set type to IMAGE, VIDEO, AUDIO or DOCUMENT and give a public media_url. text becomes the caption.
curl https://api.getoxtro.com/v1/messages \
-H "Authorization: Bearer $OXTRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"connection_id": "conn_123",
"to": "+213555123456",
"type": "DOCUMENT",
"media_url": "https://example.com/invoices/ORD-4821.pdf",
"text": "Invoice for order ORD-4821"
}'Errors
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_REQUEST | A field is missing or malformed, or the request has no text, template or media. |
| 400 | INVALID_PHONE | to is not a valid phone number. |
| 400 | MISSING_TEMPLATE_VARIABLE | The template has a variable you did not provide. |
| 400 | CONNECTION_OFFLINE | The connection is not in the CONNECTED state. Scan the QR code again. |
| 403 | QUOTA_EXCEEDED | The monthly message limit of your plan is reached. |
| 404 | NOT_FOUND | The connection, template or media does not exist in this workspace. |
| 409 | IDEMPOTENCY_CONFLICT | The Idempotency-Key was already used with a different body. |
See Errors for the error format and the codes shared by every endpoint.
Get a message
GET/v1/messages/{id}Scope messages:read
curl https://api.getoxtro.com/v1/messages/msg_123 \
-H "Authorization: Bearer $OXTRO_API_KEY"{
"id": "msg_123",
"workspace_id": "ws_123",
"connection_id": "conn_123",
"direction": "OUTBOUND",
"type": "TEXT",
"content": "Your order ORD-4821 has been confirmed.",
"status": "DELIVERED",
"error_message": null,
"queued_at": "2026-10-10T09:41:07.000Z",
"sent_at": "2026-10-10T09:41:08.000Z",
"delivered_at": "2026-10-10T09:41:10.000Z",
"read_at": null,
"failed_at": null,
"created_at": "2026-10-10T09:41:07.000Z"
}| Field | Type | Description |
|---|---|---|
id | string, required | |
workspace_id | string, required | |
connection_id | string, required | |
direction | string, required | One of: OUTBOUND, INBOUND. |
type | string, required | One of: TEXT, IMAGE, VIDEO, AUDIO, DOCUMENT, LOCATION, CONTACT. |
content | string | null | |
status | string, required | One of: QUEUED, PROCESSING, SENT, DELIVERED, READ, FAILED, CANCELLED. |
error_message | string | null | |
queued_at | ISO 8601 string | null | |
sent_at | ISO 8601 string | null | |
delivered_at | ISO 8601 string | null | |
read_at | ISO 8601 string | null | |
failed_at | ISO 8601 string | null | |
created_at | ISO 8601 string, required |
Statuses
| Status | Meaning |
|---|---|
QUEUED | Accepted and waiting in the send queue. |
PROCESSING | Being sent. |
SENT | Handed to WhatsApp. sent_at is set. |
DELIVERED | Reached the recipient device. delivered_at is set. |
READ | Opened by the recipient, if they share read receipts. read_at is set. |
FAILED | Could not be sent after all retries. See error_message. |
CANCELLED | Cancelled before it was sent. |
Polling works, but webhooks tell you about each change as it happens.