Developers
Send WhatsApp templates and replies from your own system, follow each message's delivery, and read every conversation.
Before you start
- Your WhatsApp number is connected in Pixl, under Connections › Channels.
- Templates are created in Pixl and approved by WhatsApp before you can send them.
- A template starts a conversation; a free-text reply only works within 24 hours of the person's last message.
- Scopes:
campaigns:writeto send a template to a person or to numbers,chat:writeto send in a conversation and read messages, anddeployments:readandworkspace:readto find your number's id.
Send a template to one person
POST/whatsapp/messagesScope: campaigns:write
Sends one approved template to one person, with that person's own values, and returns the message. The person is found in your contacts by number, or added. The message goes into their conversation on that number, and when they reply, the agent answers there as usual.
Body
tostringRequired- Their number, with the country code:
+919876543210. template_namestringRequired- The approved template's name.
languagestringRequired- The template's language code, as approved:
en,hi. variablesstring[] or object- The body's values in order, up to 20. For a template with named variables you can send an object instead:
{ "name": "Asha", "slot": "Sat 10:30 am" }. The count must match the template. agent_idstring- The agent whose WhatsApp number sends, and who answers the reply. Give this or phone_number_id.
phone_number_idstring- Your sending number's id (see below). With agent_id too, it must be that agent's number.
contact_namestring- Their name, up to 120 characters, for a new contact or one with no name yet. A saved name is never changed.
external_refstring- Your own reference, up to 128 characters. It comes back on the message, its events and the messages list.
curl -X POST "https://api.pixlagents.com/api/v1/whatsapp/messages" \
-H "Authorization: Bearer $PIXL_API_KEY" \
-H 'Idempotency-Key: ext_123-reminder-1' \
-H "Content-Type: application/json" \
-d '{
"to": "+919876543210",
"template_name": "appointment_reminder",
"language": "en",
"variables": [
"Asha",
"Saturday 10:30 am"
],
"agent_id": "agent_123",
"contact_name": "Asha Rao",
"external_ref": "ext_123"
}'{
"message_id": "msg_456",
"conversation_id": "conv_987",
"contact_id": "contact_321",
"status": "sent",
"external_ref": "ext_123"
}statusissent: WhatsApp accepted it. Its delivery and reading come asmessage.statusevents, or with the message when you read it.- A marketing template goes out only between 09:00 and 21:00 where the person is. Outside that, it's refused with
outside_send_window, anddetail.opens_at(UTC) says when to send. Pixl doesn't queue it. Utility and authentication templates go at any hour. - Send an
Idempotency-Keyheader, up to 128 characters, to retry safely: the same key within 7 days returns the first answer and sends nothing. A refused send doesn't keep its key. Without one, a retry sends again.
Every refusal is { "detail": { "code", "message" } }, and nothing is saved:
Refusals
400template_not_found,template_not_approved,template_not_sendable(it needs a header or button value),variables_mismatch,variable_blank,variable_too_long,no_sending_number(the agent has no WhatsApp number),connection_expired,channel_refused(WhatsApp refused it;messagesays why).402plan: your plan doesn't cover it.403opted_out,resting(a marketing template while they rest from promotions),no_optin(your workspace asks for an opt-in before marketing),not_sendable,human_send_off(the agent's conversations don't allow sends).404agent_not_found,phone_number_not_found.409idempotency_key_in_use: another request with this key is still running. Try again shortly.422invalid_phone,no_sending_number(neither id given),phone_number_not_agents,outside_send_window,send_blocked(the number can't send now, for example its quality rating),idempotency_key_too_long.423sending_paused: your workspace's Pause all sending is on.503sending_disabled: sending is switched off for now. Try again later.
Find your number's id
Sending to a list of numbers, or to one person by phone_number_id, needs the id Pixl gives your WhatsApp number. Find it once and keep it:
- Your business:
GET /workspaces/{workspace_id}/businesses. Most workspaces have one; use the one withis_default: true. - Its WhatsApp accounts:
GET /businesses/{business_id}/whatsapp-accounts. - The account's numbers:
GET /businesses/{business_id}/whatsapp-accounts/{account_id}/phones. Theidof your number is the one to send from.
curl "https://api.pixlagents.com/api/v1/businesses/business_123/whatsapp-accounts/account_456/phones" \
-H "Authorization: Bearer $PIXL_API_KEY"[
{
"id": "number_789",
"e164": "+918012345678",
"display_name": "Sample Clinic",
"quality_rating": "GREEN"
}
]Send a template to numbers
POST/agents/{agent_id}/broadcastScope: campaigns:write
Sends one approved template to a list of numbers, from your number, in your agent's name. Replies come back to that agent. Every number gets the same values; for one person with their own, use the send above.
Body
namestringRequired- A name for this send, shown in Pixl's campaign list.
template_namestringRequired- The approved template's name.
template_languagestring- The template's language code, as approved:
en,en_US,hi.enwhen left out. phone_number_idstringRequired- Your number's id (see above).
recipient_idsstring[]Required- The numbers to send to, with their country codes:
["+919876543210"]. Each becomes a contact, found by number or created. template_paramsobject- The template's values.
body_variablesfills the body's variables in order. For a header image, video or document, setheader_typeand put the file's URL first inheader_variables. For a link button with a variable, addbuttons: [{ "type": "url", "index": 0, "url": "…" }].
The values are shared
template_params.variable_mappings: { "index": 1, "mode": "contact_field", "field_key": "name", "fallback": "there" } for the name, { "index": 2, "mode": "static", "value": "Saturday" } for a fixed value. For one person with their own values, use POST /whatsapp/messages instead.curl -X POST "https://api.pixlagents.com/api/v1/agents/agent_123/broadcast" \
-H "Authorization: Bearer $PIXL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Appointment reminder, Asha Rao",
"template_name": "appointment_reminder",
"template_language": "en",
"phone_number_id": "number_789",
"recipient_ids": [
"+919876543210"
],
"template_params": {
"body_variables": [
"Asha",
"Saturday 10:30 am"
]
}
}'{
"campaign_id": "campaign_456",
"status": "queued"
}It sends straight away. Each number still passes the consent rules below, so one may be skipped while the others go. Keep campaign_id to see who got it.
See who got it
GET/campaigns/{campaign_id}/recipientsScope: campaigns:read
curl "https://api.pixlagents.com/api/v1/campaigns/campaign_456/recipients" \
-H "Authorization: Bearer $PIXL_API_KEY"{
"campaign_id": "campaign_456",
"channel": "whatsapp",
"recipients": [
{
"recipient_id": "recipient_789",
"status": "read",
"phone": "+919876543210",
"name": "Asha Rao",
"contact_id": "contact_321",
"sent_at": "2026-10-26T05:00:04",
"delivered_at": "2026-10-26T05:00:06",
"read_at": "2026-10-26T05:03:41",
"replied_at": null,
"whatsapp_message_id": "wamid.HBgM",
"error": null,
"conversation_id": "conv_987"
}
],
"total": 1,
"status_counts": {
"read": 1
}
}statusmoves frompendingtosent,delivered,readandreplied.failedandskippedsay why inerror.conversation_idis the person's conversation, once the template is in it. Use it to reply.- Large lists come in pages:
limit(up to 2,000) andoffset.status_countsalways covers the whole send.
Send a template in a conversation
POST/chat/conversations/{conversation_id}/send-templateScope: chat:write
Sends one template in an existing WhatsApp conversation, from the number that conversation is on. Use it when the 24-hour window has closed. To see which templates the conversation's number can send, and why any can't, use GET /chat/conversations/{conversation_id}/templates.
Body
template_namestringRequired- The approved template's name.
languagestring- The template's language code, when it's approved in more than one.
variablesstring[]- One value for each variable in the body, in order, up to 1,024 characters each. The count must match the template.
curl -X POST "https://api.pixlagents.com/api/v1/chat/conversations/conv_987/send-template" \
-H "Authorization: Bearer $PIXL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_name": "appointment_reminder",
"variables": [
"Asha",
"Saturday 10:30 am"
]
}'{
"status": "ok",
"message_id": "msg_456",
"content": "Hi Asha, a reminder of your appointment on Saturday 10:30 am."
}Templates with a header image, video or document, a header variable or a variable button link can't be sent here; send them to the number instead. Refusals come back with the reason in detail:
Refusals
400- Not a WhatsApp conversation, an unknown or unapproved template, or the wrong number of values.
402- Your credits, plan or spending limit doesn't cover this message.
403- The person opted out, is resting from promotions, or hasn't agreed to promotions your workspace requires agreement for. Or your team turned off people sending from this agent's conversations.
422- Your WhatsApp account can't send right now.
Reply inside 24 hours
POST/chat/{conversation_id}/send-as-humanScope: chat:write
Sends a free-text reply in a conversation, as a person on your team. On WhatsApp it only works within 24 hours of the person's last message; after that, send a template. A reply doesn't switch the agent off: it still answers the person's next message.
Body
textstringRequired- The message.
curl -X POST "https://api.pixlagents.com/api/v1/chat/conv_987/send-as-human" \
-H "Authorization: Bearer $PIXL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Thanks Asha, see you on Saturday."
}'{
"status": "ok",
"message_id": "msg_457"
}400: outside the 24-hour window, or the message couldn't be sent.detailsays which.403: the person opted out, or your team turned off people sending from this agent's conversations.
Opt-outs and consent
Every send checks the person's consent first. You can't switch these checks off.
- Someone who texted STOP, or who is on your opt-out list, gets nothing you start: no template, no reply. A send to a list skips them with
error: "opted out". - If they opted out and then wrote to you again, you may answer inside the 24 hours their message opened.
- Someone who stopped promotions from WhatsApp itself still gets utility templates, like an appointment reminder, but no marketing ones.
- People who ignore promotions rest from them for 30 days. Utility templates still reach them. The switch is in Pixl, under your consent settings.
- If your workspace asks for an opt-in before promotions, marketing templates go only to people with one on record.
To keep Pixl in step with your own records, import opt-ins and add opt-outs through the API: see Contacts and consent.
Read conversations
GET/chatScope: chat:read
Your workspace's conversations, most recently active first.
Query
channelstringwhatsapp,instagram,voice,emailand others.contact_idstring- Only this person's conversations.
agent_idstring- Only this agent's conversations.
date_from, date_todate-time- Only conversations whose last message falls in this range.
include_unreplied_campaignsboolean- Conversations that only hold a template nobody answered are left out unless this is
true. skip, limitinteger- Paging. limit is up to 500, and 50 when left out.
curl -G "https://api.pixlagents.com/api/v1/chat" \
-H "Authorization: Bearer $PIXL_API_KEY" \
--data-urlencode 'channel=whatsapp' \
--data-urlencode 'include_unreplied_campaigns=true' \
--data-urlencode 'limit=50'{
"conversations": [
{
"id": "conv_987",
"channel": "whatsapp",
"agent_id": "agent_123",
"contact_id": "contact_321",
"contact_name": "Asha Rao",
"contact_phone": "+919876543210",
"status": "active",
"ai_active": true,
"last_message_at": "2026-10-26T05:04:02",
"last_inbound_at": "2026-10-26T05:04:02",
"message_count": 4
}
],
"total": 1,
"hidden_count": 0
}One conversation, with its messages
GET/chat/{conversation_id}Scope: chat:read
curl "https://api.pixlagents.com/api/v1/chat/conv_987" \
-H "Authorization: Bearer $PIXL_API_KEY"{
"id": "conv_987",
"channel": "whatsapp",
"contact_opted_out": false,
"hours_since_last_inbound": 0.4,
"messages": [
{
"id": "msg_456",
"role": "assistant",
"content": "Hi Asha, a reminder of your appointment on Saturday 10:30 am.",
"template_name": "appointment_reminder",
"delivery_status": "read",
"delivered_at": "2026-10-26T05:00:06",
"read_at": "2026-10-26T05:03:41",
"failure_code": null,
"failure_reason": null,
"provider_message_id": "wamid.HBgM",
"created_at": "2026-10-26T05:00:04"
},
{
"id": "msg_458",
"role": "user",
"content": "Yes, I will be there.",
"delivery_status": "queued",
"created_at": "2026-10-26T05:04:02"
}
]
}roleisuserfor the person andassistantfor your side. A reply from your team carriesmetadata_json.human_sent: true.hours_since_last_inboundtells you whether the 24-hour window is open.contact_opted_outtells you whether they opted out.- Attachments are in
attachments, each with astorage_url. - To find a person's conversation from their number, look them up with
GET /contacts(search, withinclude_channel_auto=true), then use theirlast_conversation_id, or list withcontact_id. - A long conversation comes in pages when you ask:
messages_limit(up to 200) gives the newest that many, oldest first, andmessages_next_cursor; pass it asbeforefor older ones. It'snullwhen there are none. Withoutmessages_limit, you get every message.
Every message since a time
GET/messagesScope: chat:read
Every message in your workspace's customer conversations, on every channel, oldest first. Use it to keep your system in step, or to catch up after your webhook endpoint was down.
Query
sincedate-time- Messages created at or after this time. Needed on the first page.
cursorstring- The previous page's
next_cursor. It takes the place ofsince. limitinteger- 1 to 200, 100 when left out.
channelstring- Only one channel, like
whatsapp.
curl -G "https://api.pixlagents.com/api/v1/messages" \
-H "Authorization: Bearer $PIXL_API_KEY" \
--data-urlencode 'since=2026-10-27T00:00:00Z' \
--data-urlencode 'limit=100'{
"items": [
{
"id": "msg_458",
"conversation_id": "conv_987",
"contact_id": "contact_321",
"channel": "whatsapp",
"direction": "inbound",
"sender": {
"type": "customer",
"agent_id": null,
"user_id": null,
"campaign_id": null
},
"text": "Yes, I will be there.",
"content_restricted": false,
"template_name": null,
"status": "received",
"failure": null,
"created_at": "2026-10-27T05:04:02Z",
"sent_at": null,
"delivered_at": null,
"read_at": null,
"failed_at": null,
"attachments": [],
"external_ref": null
}
],
"next_cursor": "eyJ0IjoiMjAyNi0xMC0yNyJ9",
"has_more": false
}next_cursoris always there, even on an empty page. Whenhas_moreisfalse, keep the lastnext_cursorand ask again with it later.- A message appears about a minute after it's saved, so a page never skips one still being written. For real time, use webhooks.
statusand its times are as they are when you read. A later change doesn't bring a message back into the list: it comes as amessage.statusevent, or read the conversation.sender.type:customer,agent,person(a teammate),campaign,api(sent withPOST /whatsapp/messages) orsystem.- Not in it: the turns of phone calls (use the call log), the agent chat inside Pixl, and test conversations.
- In a health business, the words of call conversations are left out unless your key may read them:
text: null,content_restricted: true.
Delivery and failure codes
Subscribe to message.status on a webhook to hear of each move as it happens, and to message.received for replies. Each message your side sends carries delivery_status: queued, then sent, delivered and read, or failed. A failed message has WhatsApp's own code in failure_code and a reason in failure_reason. The ones you'll see most:
failure_code
131047- More than 24 hours since the person last wrote. Send a template.
131026- The number can't receive this message: not on WhatsApp, or an old app version.
131049- WhatsApp held back a marketing message to this person for now. Try again another day.
131050- The person stopped marketing messages from you. Pixl records it as an opt-out from promotions.
130429- Too many messages too fast. Slow down and retry.
