Sending messages
Send text, media, or poll messages immediately or schedule them for later via /v1/scheduled-messages.
This guide assumes you have an API key (Authentication) and an SDK client set up (Quickstart).
There are two ways to send a WhatsApp message. Pick based on whether you want a tracked record:
| Endpoint | When to use it | Tracked? |
|---|---|---|
POST /v1/scheduled-messages | The default for almost everything. Send right now or schedule for later. | Yes — returns a record you can list, poll for status, and edit. |
POST /v1/messages/{chat_id} | A lightweight, fire-and-forget send into one chat. | No — dispatched directly, no record kept. |
Despite the name, POST /v1/scheduled-messages is the main "send a message"
endpoint — not just for scheduling. Omit sendAt and the message goes out
immediately; add sendAt and it's scheduled for later. This guide uses it for
every example below.
The body is a flat object — set type to one of text, media, or poll;
type selects which fields are required. The optional fields sendAt
(schedule) and replyTo (quote a message) work on every variant.
Send a text message
Omit sendAt to send it right now:
curl -X POST 'https://api.blueticks.co/v1/scheduled-messages' \ -H 'Authorization: Bearer BLUETICKS_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "type": "text", "to": "string"}'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.scheduled_messages.create( # request body fields…,)import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.scheduledMessages.create({ /* … */ });use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->scheduled_messages->create(/* opts */);require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.scheduled_messages.create( # request body fields…,)import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.ScheduledMessages.Create(context.Background(), params){ "type": "text", "to": "string"}Response
{ "success": true, "data": { "id": "string", "key": "string", "to": "string", "type": "text", "text": "string", "mediaUrl": "string", "mediaKind": "image", "pollQuestion": "string", "status": "pending", "sendAt": "2026-01-01T00:00:00Z", "createdAt": "2026-01-01T00:00:00Z", "confirmedAt": "2026-01-01T00:00:00Z", "receivedAt": "2026-01-01T00:00:00Z", "readAt": "2026-01-01T00:00:00Z", "playedAt": "2026-01-01T00:00:00Z", "failedAt": "2026-01-01T00:00:00Z", "failureReason": "string" }}Send to a group or channel
The to field accepts a phone number in international format OR a WhatsApp JID. JIDs come in three flavors:
<digits>@c.us— individual chat (digits-only equivalent of an international-format phone number with+stripped).<id>@g.us— group chat (e.g.120363427920657250@g.us). Find a group's id withGET /v1/groupsor by inspecting the URL in WhatsApp Web.<id>@newsletter— channel (e.g.12345@newsletter).
The send call is identical — just swap the to:
import blueticks
bt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")
# Send to a group
msg = bt.scheduled_messages.create(
to="120363427920657250@g.us",
type="text",
text="Team standup in 10 minutes!",
)
# Send to a channel
msg = bt.scheduled_messages.create(
to="12345@newsletter",
type="text",
text="New announcement for all subscribers.",
)import { Blueticks } from 'blueticks';
const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });
// Send to a group
const groupMsg = await bt.scheduledMessages.create({
to: '120363427920657250@g.us',
type: 'text',
text: 'Team standup in 10 minutes!',
});
// Send to a channel
const channelMsg = await bt.scheduledMessages.create({
to: '12345@newsletter',
type: 'text',
text: 'New announcement for all subscribers.',
});use Blueticks\Blueticks;
$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);
// Send to a group
$groupMsg = $bt->scheduled_messages->create([
'to' => '120363427920657250@g.us',
'type' => 'text',
'text' => 'Team standup in 10 minutes!',
]);
// Send to a channel
$channelMsg = $bt->scheduled_messages->create([
'to' => '12345@newsletter',
'type' => 'text',
'text' => 'New announcement for all subscribers.',
]);require "blueticks"
client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")
# Send to a group
client.scheduled_messages.create(
to: "120363427920657250@g.us",
type: "text",
text: "Team standup in 10 minutes!",
)
# Send to a channel
client.scheduled_messages.create(
to: "12345@newsletter",
type: "text",
text: "New announcement for all subscribers.",
)import (
"context"
blueticks "github.com/serenix-com/blueticks-go"
)
c, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))
// Send to a group
c.ScheduledMessages.Create(context.Background(), blueticks.CreateScheduledMessageParams{
To: "120363427920657250@g.us",
Type: "text",
Text: "Team standup in 10 minutes!",
})
// Send to a channel
c.ScheduledMessages.Create(context.Background(), blueticks.CreateScheduledMessageParams{
To: "12345@newsletter",
Type: "text",
Text: "New announcement for all subscribers.",
})# Send to a group
curl -X POST https://api.blueticks.co/v1/scheduled-messages \
-H "Authorization: Bearer BLUETICKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "120363427920657250@g.us",
"type": "text",
"text": "Team standup in 10 minutes!"
}'
# Send to a channel
curl -X POST https://api.blueticks.co/v1/scheduled-messages \
-H "Authorization: Bearer BLUETICKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "12345@newsletter",
"type": "text",
"text": "New announcement for all subscribers."
}'The validator accepts a phone number in international format (+<digits>) or any of the three JID suffixes (@c.us, @g.us, @newsletter).
Send media
type: "media" takes the file as mediaUrl (an HTTPS URL) or mediaBase64
(the raw bytes base64-encoded, or a data: URL); mediaUrl wins if both are
present. The optional mediaKind
(image · video · audio · document · sticker · voice · gif)
is auto-detected from the URL or the Content-Type of the fetched asset
when omitted. text doubles as the caption under image/video in WhatsApp;
mediaFilename is shown for documents.
import blueticks
bt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")
msg = bt.scheduled_messages.create(
to="+972501234567",
type="media",
media_url="https://cdn.example.com/receipt.pdf",
media_kind="document",
media_filename="receipt-A42.pdf",
text="Order #A-42", # caption
)
print(msg.id, msg.status)import { Blueticks } from 'blueticks';
const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });
const msg = await bt.scheduledMessages.create({
to: '+972501234567',
type: 'media',
mediaUrl: 'https://cdn.example.com/receipt.pdf',
mediaKind: 'document',
mediaFilename: 'receipt-A42.pdf',
text: 'Order #A-42', // caption
});use Blueticks\Blueticks;
$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);
$msg = $bt->scheduled_messages->create([
'to' => '+972501234567',
'type' => 'media',
'mediaUrl' => 'https://cdn.example.com/receipt.pdf',
'mediaKind' => 'document',
'mediaFilename' => 'receipt-A42.pdf',
'text' => 'Order #A-42', // caption
]);Ruby and Go nest the file details under a media object (caption lives there):
require "blueticks"
client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")
msg = client.scheduled_messages.create(
to: "+972501234567",
type: "media",
media: {
"url" => "https://cdn.example.com/receipt.pdf",
"kind" => "document",
"filename" => "receipt-A42.pdf",
"caption" => "Order #A-42",
},
)
puts msg.id, msg.statusimport (
"context"
blueticks "github.com/serenix-com/blueticks-go"
)
c, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))
msg, _ := c.ScheduledMessages.Create(context.Background(), blueticks.CreateScheduledMessageParams{
To: "+972501234567",
Type: "media",
Media: &blueticks.SendMediaObject{
URL: "https://cdn.example.com/receipt.pdf",
Kind: "document",
Filename: "receipt-A42.pdf",
Caption: "Order #A-42",
},
})curl -X POST https://api.blueticks.co/v1/scheduled-messages \
-H "Authorization: Bearer BLUETICKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+972501234567",
"type": "media",
"mediaUrl": "https://cdn.example.com/receipt.pdf",
"mediaKind": "document",
"mediaFilename": "receipt-A42.pdf",
"text": "Order #A-42"
}'Voice notes are media with mediaKind: "voice":
{ "to": "+972...", "type": "media", "mediaUrl": "https://.../note.ogg", "mediaKind": "voice" }mediaUrl must be https:// and resolve to a non-private host — we fetch
and forward it on your behalf. If the file has no public URL, send the bytes
inline as mediaBase64 instead.
Send a poll
pollOptions must contain 2–12 entries. pollAllowMultiple lets recipients
pick more than one (default false).
import blueticks
bt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")
msg = bt.scheduled_messages.create(
to="+972501234567",
type="poll",
poll_question="Pizza tonight?",
poll_options=["Yes", "No", "Maybe"],
poll_allow_multiple=False,
)
print(msg.id, msg.status)import { Blueticks } from 'blueticks';
const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });
const msg = await bt.scheduledMessages.create({
to: '+972501234567',
type: 'poll',
pollQuestion: 'Pizza tonight?',
pollOptions: ['Yes', 'No', 'Maybe'],
pollAllowMultiple: false,
});use Blueticks\Blueticks;
$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);
$msg = $bt->scheduled_messages->create([
'to' => '+972501234567',
'type' => 'poll',
'pollQuestion' => 'Pizza tonight?',
'pollOptions' => ['Yes', 'No', 'Maybe'],
'pollAllowMultiple' => false,
]);require "blueticks"
client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")
msg = client.scheduled_messages.create(
to: "+972501234567",
type: "poll",
poll: {
"question" => "Pizza tonight?",
"options" => ["Yes", "No", "Maybe"],
"allow_multiple" => false,
},
)
puts msg.id, msg.statusimport (
"context"
blueticks "github.com/serenix-com/blueticks-go"
)
c, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))
msg, _ := c.ScheduledMessages.Create(context.Background(), blueticks.CreateScheduledMessageParams{
To: "+972501234567",
Type: "poll",
Poll: &blueticks.SendPollObject{
Question: "Pizza tonight?",
Options: []string{"Yes", "No", "Maybe"},
AllowMultiple: false,
},
})curl -X POST https://api.blueticks.co/v1/scheduled-messages \
-H "Authorization: Bearer BLUETICKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+972501234567",
"type": "poll",
"pollQuestion": "Pizza tonight?",
"pollOptions": ["Yes", "No", "Maybe"],
"pollAllowMultiple": false
}'Schedule for later
sendAt (ISO 8601 with offset, ≥ 10 s in the future, ≤ 365 d out)
works on every type. The response status is pending — the message waits
until the sendAt time before dispatching.
{ "to": "+972...", "type": "text", "text": "Reminder", "sendAt": "2026-12-01T09:00:00Z" }Quote-reply to a prior message
Set replyTo to the wire key of a message in the same chat
(returned in MessageResponse.key once the original has been sent).
Works with text, media, and poll.
{ "to": "+972...", "type": "text", "text": "Got it 👍", "replyTo": "false_972...@c.us_3EB0..." }React to a message
Reactions are a separate endpoint — POST /v1/messages/reactions/{chat_id}/{key}
with body { "emoji": "❤️" }. Pass an empty string to remove a reaction.
Use the key from MessageResponse, not the id.
import blueticks
bt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")
bt.chats.react(
chat_id="972501234567@c.us",
key="false_972501234567@c.us_3EB0ABCDEF",
emoji="❤️",
)import { Blueticks } from 'blueticks';
const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });
await bt.messages.react(
'972501234567@c.us',
'false_972501234567@c.us_3EB0ABCDEF',
{ emoji: '❤️' },
);use Blueticks\Blueticks;
$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);
$bt->chats->react(
'972501234567@c.us',
'false_972501234567@c.us_3EB0ABCDEF',
'❤️',
);require "blueticks"
client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")
client.chats.react(
"972501234567@c.us",
"false_972501234567@c.us_3EB0ABCDEF",
emoji: "❤️",
)import (
"context"
blueticks "github.com/serenix-com/blueticks-go"
)
c, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))
c.Chats.React(
context.Background(),
"972501234567@c.us",
"false_972501234567@c.us_3EB0ABCDEF",
"❤️",
)curl -X POST "https://api.blueticks.co/v1/messages/reactions/972501234567@c.us/false_972501234567@c.us_3EB0ABCDEF" \
-H "Authorization: Bearer BLUETICKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emoji":"❤️"}'Idempotency
Safe retries via the Idempotency-Key header. Same key + same body
within 24 h returns the original response (as a 200 replay instead of
201). Different body with the same key returns 409.
import blueticks
bt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")
msg = bt.scheduled_messages.create(
to="+972501234567",
type="text",
text="Your order is confirmed.",
idempotency_key="order-42-reminder",
)import { Blueticks } from 'blueticks';
const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });
const msg = await bt.scheduledMessages.create({
to: '+972501234567',
type: 'text',
text: 'Your order is confirmed.',
idempotencyKey: 'order-42-reminder',
});use Blueticks\Blueticks;
$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);
$msg = $bt->scheduled_messages->create([
'to' => '+972501234567',
'type' => 'text',
'text' => 'Your order is confirmed.',
'idempotency_key' => 'order-42-reminder',
]);require "blueticks"
client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")
msg = client.scheduled_messages.create(
to: "+972501234567",
type: "text",
text: "Your order is confirmed.",
idempotency_key: "order-42-reminder",
)import (
"context"
blueticks "github.com/serenix-com/blueticks-go"
)
c, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))
msg, _ := c.ScheduledMessages.Create(context.Background(), blueticks.CreateScheduledMessageParams{
To: "+972501234567",
Type: "text",
Text: "Your order is confirmed.",
IdempotencyKey: "order-42-reminder",
})curl -X POST https://api.blueticks.co/v1/scheduled-messages \
-H "Authorization: Bearer BLUETICKS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-42-reminder" \
-d '{
"to": "+972501234567",
"type": "text",
"text": "Your order is confirmed."
}'Status lifecycle
pending ──▶ confirmed ──▶ received ──▶ read ──▶ played
└──▶ failedpending— accepted by the API; waiting to dispatch (either immediately or atsendAt).confirmed— WhatsApp accepted the message (wire key received).confirmedAtis populated.received— double grey tick: the recipient's device has it.receivedAtis populated.read— double blue tick: the recipient opened the message.readAtis populated.played— voice note played by the recipient.playedAtis populated.failed— terminal. See thefailureReasonfield on the message for the cause.
Poll the current state with GET /v1/scheduled-messages/{id}. Read receipts
(readAt populated, message.read webhook fires) are delivered via
webhooks — see the Webhooks guide.
Manage scheduled messages
List all your messages (pending, in-flight, or delivered) with GET /v1/scheduled-messages:
curl -X GET 'https://api.blueticks.co/v1/scheduled-messages' \ -H 'Authorization: Bearer BLUETICKS_API_KEY'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.scheduled_messages.list()import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.scheduledMessages.list();use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->scheduled_messages->list();require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.scheduled_messages.list()import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.ScheduledMessages.List(context.Background())Fetch one by id to read its current status:
curl -X GET 'https://api.blueticks.co/v1/scheduled-messages/string' \ -H 'Authorization: Bearer BLUETICKS_API_KEY'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.scheduled_messages.retrieve( "id_01H7...",)import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.scheduledMessages.retrieve('id_01H7...');use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->scheduled_messages->retrieve('id_01H7...');require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.scheduled_messages.retrieve( "id_01H7...",)import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.ScheduledMessages.Retrieve(context.Background(), "id_01H7...")Before a message dispatches (while still pending) you can edit it with
PATCH /v1/scheduled-messages/{id} — reschedule (sendAt), rewrite the body
(text), or swap the attachment (mediaUrl, mediaCaption). At least one
field is required; once the message has dispatched, the edit returns 400:
curl -X PATCH 'https://api.blueticks.co/v1/scheduled-messages/string' \ -H 'Authorization: Bearer BLUETICKS_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "text": "string", "mediaUrl": "https://example.com", "mediaCaption": "string", "sendAt": "2026-01-01T00:00:00Z"}'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.scheduled_messages.update( "id_01H7...", # request body fields…,)import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.scheduledMessages.update('id_01H7...', { /* … */ });use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->scheduled_messages->update('id_01H7...', /* opts */);require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.scheduled_messages.update( "id_01H7...", # request body fields…,)import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.ScheduledMessages.Update(context.Background(), "id_01H7...", params){ "text": "string", "mediaUrl": "https://example.com", "mediaCaption": "string", "sendAt": "2026-01-01T00:00:00Z"}Immediate send into an open chat
The second send path from the top of this guide. POST /v1/messages/{chat_id}
posts straight into one conversation and returns the WhatsApp wire key. It's
fire-and-forget — no record is created, so there's nothing to list, poll, or
edit afterwards. Reach for it only when you don't need tracking; otherwise
use POST /v1/scheduled-messages (above). The body is the same flat text /
media / poll shape, minus to (it's in the URL) and sendAt. It also
accepts multipart/form-data with the file in a mediaFile part, for media
sends without a public URL:
curl -X POST 'https://api.blueticks.co/v1/messages/string' \ -H 'Authorization: Bearer BLUETICKS_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "type": "text"}'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.chats.send_message( "chat_id_01H7...", # request body fields…,)import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.messages.send('chat_id_01H7...', { /* … */ });use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->chats->sendMessage('chat_id_01H7...', /* opts */);require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.chats.send_message( "chat_id_01H7...", # request body fields…,)import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.Chats.SendMessage(context.Background(), "chat_id_01H7...", params){ "type": "text"}Response
A successful send returns 200 OK; the wire key is under data.key:
{
"success": true,
"data": {
"id": null,
"key": "false_972501234567@c.us_3EB0ABCDEF",
"to": "972501234567@c.us",
"type": "text",
"text": "Got it 👍",
"mediaUrl": null,
"mediaKind": null,
"pollQuestion": null,
"status": "confirmed",
"sendAt": null,
"createdAt": "2026-06-12T09:00:00.000Z",
"confirmedAt": "2026-06-12T09:00:01.000Z",
"receivedAt": null,
"readAt": null,
"playedAt": null,
"failedAt": null,
"failureReason": null
}
}Beyond sending: read, groups & channels
Sending is one half of the API. The flows below each have a full, interactive reference page — here are the common entry points:
- Read chats & messages — list chats, page through a conversation (scope with
?chatId=, or search across all chats), and inspect a single message: List chats · List messages · Get message · List participants - Delivery & read state — check ticks for one message, or mark a chat read: Delivery status · Mark chat as read
- Inbound media — resolve a temporary download URL or pull the bytes for media on an incoming message: Get media URL · Get media
- Groups — create and administer groups (members, admins, picture), beyond
just sending to a
@g.usJID: Groups reference - Channels — WhatsApp channels are exposed as newsletters: Newsletters reference
Not yet supported
These message types are on the roadmap but not yet exposed in the v1 API. Use the MCP server or watch the changelog for ship dates:
- Location — share a coordinate with optional name/address.
- Contacts (vcard) — share a contact card.
- Buttons / list — interactive replies and selectable options.