Send WhatsApp message
curl --request POST \
--url https://api.60db.ai/whatsapp/messages \
--header 'Authorization: <authorization>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--data '
{
"number_id": "<string>",
"to": "<string>",
"type": "<string>",
"text": {},
"template": {},
"context": {}
}
'import requests
url = "https://api.60db.ai/whatsapp/messages"
payload = {
"number_id": "<string>",
"to": "<string>",
"type": "<string>",
"text": {},
"template": {},
"context": {}
}
headers = {
"Authorization": "<authorization>",
"Idempotency-Key": "<idempotency-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
Authorization: '<authorization>',
'Idempotency-Key': '<idempotency-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
number_id: '<string>',
to: '<string>',
type: '<string>',
text: {},
template: {},
context: {}
})
};
fetch('https://api.60db.ai/whatsapp/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.60db.ai/whatsapp/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'number_id' => '<string>',
'to' => '<string>',
'type' => '<string>',
'text' => [
],
'template' => [
],
'context' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>",
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.60db.ai/whatsapp/messages"
payload := strings.NewReader("{\n \"number_id\": \"<string>\",\n \"to\": \"<string>\",\n \"type\": \"<string>\",\n \"text\": {},\n \"template\": {},\n \"context\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<authorization>")
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.60db.ai/whatsapp/messages")
.header("Authorization", "<authorization>")
.header("Idempotency-Key", "<idempotency-key>")
.header("Content-Type", "application/json")
.body("{\n \"number_id\": \"<string>\",\n \"to\": \"<string>\",\n \"type\": \"<string>\",\n \"text\": {},\n \"template\": {},\n \"context\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.60db.ai/whatsapp/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<authorization>'
request["Idempotency-Key"] = '<idempotency-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"number_id\": \"<string>\",\n \"to\": \"<string>\",\n \"type\": \"<string>\",\n \"text\": {},\n \"template\": {},\n \"context\": {}\n}"
response = http.request(request)
puts response.read_bodyWhatsApp
Send WhatsApp message
Queue a workspace-scoped message with idempotent retries
POST
/
whatsapp
/
messages
Send WhatsApp message
curl --request POST \
--url https://api.60db.ai/whatsapp/messages \
--header 'Authorization: <authorization>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--data '
{
"number_id": "<string>",
"to": "<string>",
"type": "<string>",
"text": {},
"template": {},
"context": {}
}
'import requests
url = "https://api.60db.ai/whatsapp/messages"
payload = {
"number_id": "<string>",
"to": "<string>",
"type": "<string>",
"text": {},
"template": {},
"context": {}
}
headers = {
"Authorization": "<authorization>",
"Idempotency-Key": "<idempotency-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
Authorization: '<authorization>',
'Idempotency-Key': '<idempotency-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
number_id: '<string>',
to: '<string>',
type: '<string>',
text: {},
template: {},
context: {}
})
};
fetch('https://api.60db.ai/whatsapp/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.60db.ai/whatsapp/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'number_id' => '<string>',
'to' => '<string>',
'type' => '<string>',
'text' => [
],
'template' => [
],
'context' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>",
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.60db.ai/whatsapp/messages"
payload := strings.NewReader("{\n \"number_id\": \"<string>\",\n \"to\": \"<string>\",\n \"type\": \"<string>\",\n \"text\": {},\n \"template\": {},\n \"context\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<authorization>")
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.60db.ai/whatsapp/messages")
.header("Authorization", "<authorization>")
.header("Idempotency-Key", "<idempotency-key>")
.header("Content-Type", "application/json")
.body("{\n \"number_id\": \"<string>\",\n \"to\": \"<string>\",\n \"type\": \"<string>\",\n \"text\": {},\n \"template\": {},\n \"context\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.60db.ai/whatsapp/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<authorization>'
request["Idempotency-Key"] = '<idempotency-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"number_id\": \"<string>\",\n \"to\": \"<string>\",\n \"type\": \"<string>\",\n \"text\": {},\n \"template\": {},\n \"context\": {}\n}"
response = http.request(request)
puts response.read_bodyRequires a connected workspace number, an active paid platform plan and
Poll
whatsapp API key scope. Owner, admin, developer and member roles may send.
Request
string
required
Bearer followed by your 60db API key.string
required
Unique message key, 8–128 characters using letters, digits, dots, colons, underscores or hyphens. The
broadcast: prefix is reserved for internal campaign messages. Keep the same key and identical payload when retrying an uncertain API response. Reusing a key with a different payload returns 409 IDEMPOTENCY_CONFLICT.string
required
The 60db UUID from
GET /whatsapp/numbers, rather than the provider’s phone number ID.string
required
Recipient’s phone number including country code, 7–15 digits without
+, spaces or punctuation.string
required
text, template, image, video, audio, document or interactive. Supply exactly the matching content object.object
For text messages:
{ "body": "Hello", "preview_url": false }. Body length is 1–4,096 characters.object
For approved templates: name,
language.code and optional Meta-format components. Sync the template to 60db first.object
Optional
{ "message_id": "wamid..." } to reply to a provider message.Text reply
Replace the example UUID and phone number with your connected number and consenting recipient. Text requires an inbound message within the preceding 24 hours.curl --request POST 'https://api.60db.ai/whatsapp/messages' \
--header "Authorization: Bearer $SIXTYDB_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-123-reply-1' \
--data '{
"number_id": "11111111-1111-4111-8111-111111111111",
"to": "919000000001",
"type": "text",
"text": {"body": "Your order is ready."}
}'
Approved template
{
"number_id": "11111111-1111-4111-8111-111111111111",
"to": "919000000001",
"type": "template",
"template": {
"name": "order_update",
"language": {"code": "en_US"},
"components": [{
"type": "body",
"parameters": [{"type": "text", "text": "ORDER-123"}]
}]
}
}
Other message types
First upload media to the sending number, then use its returnedprovider_id, for example "image": {"id": "123456789", "caption": "Order photo"}. Media IDs must belong to the same number and workspace and remain unexpired. This also applies to template and interactive media headers. Arbitrary media URLs are not accepted. Video has the same fields; audio accepts an ID and optional voice: true for mono Ogg/Opus voice notes; documents can also include a filename. Interactive messages accept button or list, a body, action and optional header/footer in Meta format.
Response and delivery
202 Accepted means the message is durably queued, not delivered. A replay may return 200 OK with the existing message once it has left the queue.
{
"success": true,
"data": {
"id": "22222222-2222-4222-8222-222222222222",
"number_id": "11111111-1111-4111-8111-111111111111",
"direction": "outbound",
"type": "text",
"status": "queued"
}
}
GET /whatsapp/messages/{id} or subscribe to message status events.
| Status | Meaning |
|---|---|
queued | Stored, awaiting dispatch |
sending | Dispatch in progress |
sent | Provider accepted the message |
delivered | Delivery confirmed |
read | Read confirmed |
failed | Dispatch or delivery failed |
unknown | Acceptance could not be confirmed; not automatically resent |
Errors
| HTTP | Code | Recovery |
|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED / VALIDATION_ERROR | Correct the request |
| 403 | POLICY_DENY | Check membership, plan and WhatsApp API key scope |
| 403 | CONTACT_OPTED_OUT | Respect the customer’s opt-out |
| 409 | NUMBER_DISCONNECTED | Reconnect the number |
| 409 | TEMPLATE_REQUIRED | Send an approved template outside the reply window |
| 409 | TEMPLATE_NOT_APPROVED | Sync and select an approved template |
| 409 | OPT_IN_REQUIRED | Record explicit consent before initiating the conversation |
| 409 | IDEMPOTENCY_CONFLICT | Use a new key for a different message |
| 404 | MEDIA_NOT_FOUND | Upload the attachment to this number and workspace |
| 410 | MEDIA_EXPIRED | Upload the file again and use the new ID |
| 503 | Authorization unavailable | Retry the same request and idempotency key later |