HTTP API
Send WhatsApp messages from your CRM
Use the Send API from CRMs, automations, backend jobs, and support tools. v1 supports direct text, image, and document messages from the connected WhatsApp account.
Authentication
Send a Send API bearer token with every request. Send API tokens are separate from MCP tokens and only grant message-send access. Personal API usage sends from the WhatsApp account attached to the token owner. Owners and admins can also use workspace sender mode by passing a from number that belongs to the same workspace.
Authorization: Bearer YOUR_SEND_API_TOKENEndpoint
POST https://whatan.app/api/v1/messages/sendSender Rules
| Request shape | Who can use it | Sender behavior |
|---|---|---|
| from + to | Workspace owners and admins | Sends from the connected workspace number in from. |
| to only | Owners, admins, and members | Sends from the WhatsApp account attached to that token. |
Workspace Sender Mode
Use this mode for Attio workflows and other workspace-level automations that need to choose the sender at runtime. Include both from and to. The from number must be one of the connected WhatsApp numbers in this workspace. This mode is available to owner/admin tokens.
curl -X POST "https://whatan.app/api/v1/messages/send" \
-H "Authorization: Bearer YOUR_SEND_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attio-workflow-123" \
-d '{
"from": "+917000000001",
"to": "+917999709798",
"type": "text",
"text": "Following up from Attio."
}'The API normalizes from, finds the matching connected WhatsApp account in the workspace, and queues the send against that account. If the number is not part of the workspace, the request is rejected before sending.
Personal Sender Mode
This is the original Send API behavior and remains supported for backwards compatibility. Omit from; the bearer token determines the sender. Owners and members using this mode send only from their own connected WhatsApp account.
curl -X POST "https://whatan.app/api/v1/messages/send" \
-H "Authorization: Bearer YOUR_SEND_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-123-reminder" \
-d '{
"to": "+917999709798",
"type": "text",
"text": "Your invoice is ready."
}'Send Text
Text messages support both sender modes. Use from for workspace automation, or omit it for personal-token sending.
curl -X POST "https://whatan.app/api/v1/messages/send" \
-H "Authorization: Bearer YOUR_SEND_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: attio-workflow-123" \
-d '{
"from": "+917000000001",
"to": "+917999709798",
"type": "text",
"text": "Following up from Attio."
}'Send Image
Image URLs must be public HTTPS URLs. Localhost and private network URLs are rejected.
curl -X POST "https://whatan.app/api/v1/messages/send" \
-H "Authorization: Bearer YOUR_SEND_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: promo-image-456" \
-d '{
"to": "+917999709798",
"type": "image",
"image": {
"url": "https://example.com/promo.png",
"caption": "New arrivals are live."
}
}'Send Document
Documents use public HTTPS URLs. Provide a file name and MIME type when your CRM knows them.
curl -X POST "https://whatan.app/api/v1/messages/send" \
-H "Authorization: Bearer YOUR_SEND_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quote-789" \
-d '{
"to": "+917999709798",
"type": "document",
"document": {
"url": "https://example.com/quote.pdf",
"fileName": "quote.pdf",
"mimetype": "application/pdf",
"caption": "Attached quote for review."
}
}'Node Example
const response = await fetch("https://whatan.app/api/v1/messages/send", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_SEND_API_TOKEN",
"Content-Type": "application/json",
"Idempotency-Key": "attio-record-1001",
},
body: JSON.stringify({
from: "+917000000001",
to: "+917999709798",
type: "text",
text: "Thanks for contacting support. We are checking this now.",
}),
});
const result = await response.json();Idempotency
Include an Idempotency-Key header for every CRM event or automation run. If the CRM retries the same request with the same key, CRM returns the original send result instead of sending a duplicate WhatsApp message.
Queue, Pacing, and Rate Limits
The Send API stores every accepted request in the database with a queued status. In production, the worker checks the queue about every 10 seconds and examines up to 25 of the oldest queued requests during each check.
Pacing is calculated separately for each connected WhatsApp account. After the worker sends one message from an account, the next message from that same account must wait at least 3-8 seconds. Because the production worker checks every 10 seconds, one busy account normally sends about one message every 10 seconds, or about 6 messages per minute. The 3-8 second check prevents the worker from sending several messages from the same account during a single queue check.
Example: one account queues 200 messages
- All 200 accepted API requests are stored as queued.
- The worker loads the oldest 25. It sends the first message, then skips the other 24 because that account just sent.
- About 10 seconds later, the worker checks again and sends the next eligible message.
- At this pace, 100 messages take about 16 minutes 30 seconds after the first send, plus up to one initial worker interval.
- After the account reaches 100 API/MCP sends in the rolling 24-hour window, the remaining API messages stay queued. They resume as older sends fall outside that window.
The rolling limit is 100 combined API and MCP sends per WhatsApp account in 24 hours. API messages wait in the queue when the account has no remaining capacity. MCP sends use the same daily allowance but send immediately instead of using this API queue. Team inbox replies are immediate and are not part of the API/MCP daily allowance.
Repeating a request with the same Idempotency-Key returns the original attempt instead of creating a duplicate queued message.
The Send API is for transactional CRM messages, support replies, and expected automations. Bulk CSV/XLSX sending belongs in the upcoming Mass Messaging workflow, which will have campaign scheduling, reports, and dedicated safety controls.
Responses
{
"accepted": true,
"sendAttemptId": "f1f7a5a0-2ff4-4ef7-b4bf-1a3b50ad6f53",
"messageId": null,
"remoteJid": null,
"from": "+917000000001",
"to": "+917999709798",
"normalizedPhone": "917999709798",
"status": "queued"
}The Send API queues accepted requests. Use the send attempt ID to poll final status. Personal tokens can read their own send attempts. Owner/admin tokens can also read workspace sender mode attempts created from connected senders in the same workspace.
GET https://whatan.app/api/v1/messages/SEND_ATTEMPT_IDcurl "https://whatan.app/api/v1/messages/SEND_ATTEMPT_ID" \
-H "Authorization: Bearer YOUR_SEND_API_TOKEN"{
"sendAttemptId": "f1f7a5a0-2ff4-4ef7-b4bf-1a3b50ad6f53",
"status": "accepted",
"messageId": "BAE5...",
"remoteJid": "[email protected]",
"errorCode": null,
"errorMessage": null,
"createdAt": "2026-05-14T10:00:00.000Z",
"updatedAt": "2026-05-14T10:00:04.000Z"
}Error Codes
| Code | Meaning |
|---|---|
| invalid_request | Request JSON is missing required fields. |
| invalid_phone_number | Phone number must be international format. |
| whatsapp_not_connected | The connected WhatsApp account is offline. |
| sender_selection_forbidden | The token owner is not an owner or admin. |
| sender_not_allowed | The from number is not a sender in this workspace. |
| recipient_not_found | The phone number is not reachable on WhatsApp. |
| invalid_media_url | Media URL is not a safe public HTTPS URL. |
| idempotency_conflict | The key was reused with a different request. |
| rate_limited | This WhatsApp account hit the v1 send limit. |