Skip to content

Sending messages

Send messages with Turbo Notify and follow the WhatsApp policies (opens in a new tab) .

The body of POST /v1/numbers/{ref}/messages (or the POST /v1/messages shortcut for the main number) carries the message content in the content object. The type comes from the anchor field present in content (text, image_url, latitude, start_at, …). Do not send a content_type field in the request: it is never accepted (including it returns 422). Text, location, contact, calendar, message with a link button (interactive_type) and media (image, video, audio, document, sticker, see Sending media) are supported.

To quote (reply to) an existing message (any content variant), use the dedicated POST /v1/messages/{message_id}/reply endpoint, where {message_id} is the public id of the quoted message (msg_{hex32}). The body of that request contains only content. The resulting webhook is message.replied (instead of message.sent); see Webhooks.

Quote to bring an earlier message into view, when the conversation has moved on and your answer depends on it (the contact’s, yours or, in a group, any participant’s). To answer the message that just arrived, send a normal message, as people do in a chat: the chat already shows what came before, and a quote on every answer is only noise.

You can reply to any message from the last 12 months, including one whose content already expired (Message retention): the conversation stays threaded through the quoted message, only the quote bubble goes without the original text.

Before sending (or replying), Turbo Notify opens the chat the way a person would: the number shows as online, the unread messages in the destination chat are marked as read, and only then is the message sent. To mark a chat as read without sending anything, see Mark as read.

Basic send

A basic send takes only the recipient’s number and the message text. The sender is your account’s main number.

turbo-notify.sh
curl -X POST \
https://api.turbonotify.com/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": {"number": "+5511988887777"},
"content": {
"text": "Hello, this message was sent with Turbo Notify!"
}
}'

After sending, the text can be corrected with Edit a message.

Mentions (@mentions)

To mention someone, write the @<number> token in the text (or in a media caption, see Sending media): @ followed by the contact’s phone number as E.164 digits, without +. It is a real WhatsApp mention, with a tappable name and a notification on the mentioned person’s phone.

POST /v1/messages: text with a mention (1:1)
{
"to": { "number": "+5511988887777" },
"content": {
"text": "Hi @5511977776666, your order has arrived!"
}
}

In groups, mention a participant by number:

POST /v1/messages: group mentioning a participant
{
"to": { "group_id": "grp_e442553a56b14e2695f6acfd7d651b1a" },
"content": {
"text": "Good morning @5511977776666, can you check today's order?"
}
}

The request has no mentions field: Turbo Notify detects mentions in the text itself. Every valid token becomes a real mention on the recipient’s device.

Token rules

RuleBehavior
Syntax@ + 7 to 15 digits, delimited by a word boundary: name@domain.com, @@123, sequences longer than 15 digits and @+55… do not become mentions
Normalization12-digit Brazilian numbers are normalized to the 13-digit form: the stored and returned content carries the normalized token (e.g. @551177776666 → @5511977776666)
Invalid tokenInvalid or non-normalizable digits stay as plain text: a send never fails because of a mention
Where it worksIn 1:1 chats and groups, in text and in the caption of image/video/document (audio/sticker have no caption)
Reusing a received tokenThe @<number> token you receive in message.received can be pasted back into a send: the mention works without any conversion
QuotaMentioning someone never consumes message or contact quota

After the send, the message carries the mentions structure (a list of {contact_id?, name?, number}; contact_id and name are omitted when the mention does not match a known contact) in GET /v1/messages/{message_id} and in the data.mentions of the message.sent/message.replied/message.edited events. A literal @<digits> in the text is a mention only if it appears in that structure.

The same detection rules apply to POST .../messages/{message_id}/reply (the reply’s text/caption) and to POST .../messages/{message_id}/edit (the new text or caption of the edit replaces the message’s entire set of mentions, see Edit and delete messages).

Sending from an extra number

To send from an extra number registered and active in your account, put the number’s alias directly in the URL path.

turbo-notify.sh
curl -X POST \
https://api.turbonotify.com/v1/numbers/vendas-sp/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": {"number": "+5511988887777"},
"content": {
"text": "Message sent from an extra number."
}
}'

If the alias in the URL is not registered as an extra number or is not active, the send is rejected.

Learn more in Adding extra numbers.

Sending without duplicates

Sending (POST .../messages) and replying (POST .../messages/{message_id}/reply) accept the optional Idempotency-Key header: a string of up to 255 characters that you choose. If the request fails with a timeout or a network error and you repeat it with the same key, on the same number, the message is not sent again: the response carries the message_id of the original message. Without the key, repeating can deliver the message twice.

Use a new key for each new message, derived from something stable on your side (for example the id of the order that triggered the notification). An empty key or one longer than 255 characters is refused with 400 invalid_idempotency_key.

Terminal window
curl -X POST https://api.turbonotify.com/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711-confirmation" \
-d '{"to": {"number": "+5511988887777"}, "content": {"text": "Your order is confirmed."}}'

Possible errors

StatusError codeDescription
400contact_not_foundThe contact_id provided does not belong to the organization.
400contacts_not_enabledSending by contact_id is not set up.
400invalid_phone_numberThe to.number provided is not a valid phone number in E.164 format (e.g. +5511988887777).
400invalid_recipientThe recipient given in to could not be resolved to a sending address. Providing none, or more than one, of number, group_id and contact_id returns 422 validation_error.
400not_group_memberYour number is no longer a participant of the given group_id, see Sending to a group.
402feature_not_availableSending to a group (to.group_id) requires the Business plan or higher, see Sending to a group.
402subscription_inactiveSubscription with a pending payment, ended, or with an expired trial (details.status). Blocks every send until the subscription is active again.
402contact_quota_exceededThe current cycle’s allowance of distinct contacts is used up, see Quota and usage.
401not_authenticatedThe API key is invalid or missing.
403insufficient_scopeThe API key does not have the messages:send scope (the response includes required_scope).
404number_not_foundThe alias or id in the URL does not match any number registered in the account.
400invalid_number_idThe value in the URL starts with num_ but is not a valid number id.
404group_not_foundThe group_id given in to was not found in the account.
409number_not_connectedThe sending number is not connected to WhatsApp. Reconnect the number and try again.
422validation_errorThe request body is malformed or failed validation. The response includes details.errors with the rejected fields.
422group_not_owned_by_numberThe group_id provided belongs to another number in the account, not to the alias in the URL path.
422message_not_replyable(only POST .../messages/{message_id}/reply) The quoted message has not been sent to WhatsApp yet or has already been deleted.
429messages_quota_exceededThe current cycle’s message allowance is used up, see Quota and usage.
429send_rate_spacingSent before the minimum interval between messages for your plan, see Rate limits.
429rate_limit_exceededThe API’s requests-per-second limit was exceeded. The other send protection limits (messages_per_minute_limit_exceeded, recipients_per_day_limit_exceeded, warmup_limit_exceeded, duplicate_burst_blocked, reply_ratio_too_low) have their own codes, see Rate limits.
500internal_errorUnexpected internal server error.

Message status

The status shows which stage the message is in. Each value is explained in Status.

HTTP status: 202
{
  "message_id": "msg_01993c5a1b24d3e9f01a2b3c4d5e6f70",
  "status": {
    "state": "pending",
    "at": "2025-07-16T10:00:00Z",
    "error": null
  }
}

The final outcome arrives by webhook with the message.sent, message.delivered, message.read or message.send.failed events.

Security and privacy

All communication between your system and the Turbo Notify API uses HTTPS, so data is encrypted in transit.

How long message content is kept (the messages themselves stay available for 12 months) is covered in Message retention.


POST /v1/messages accepts four rich content variants besides text. Each variant has its own anchor field inside the content object and reaches WhatsApp as the corresponding native type (locationMessage, contactMessage, eventMessage).

Send a location

send-location.sh
curl -X POST \
https://api.turbonotify.com/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": {"number": "+5511988887777"},
"content": {
"latitude": -23.5505,
"longitude": -46.6333,
"name": "Avenida Paulista",
"address": "São Paulo, SP",
"url": "https://maps.google.com/?q=-23.5505,-46.6333"
}
}'
FieldTypeRequiredValidation
content.latitudenumberyes-90 ≤ x ≤ 90
content.longitudenumberyes-180 ≤ x ≤ 180
content.namestringnoup to 200 characters
content.addressstringnoup to 500 characters
content.urlstringnohttp:///https:// only

A location that was sent cannot be edited: deleting it is the only change. See what can be changed after sending.

Send a contact

send-contact.sh
curl -X POST \
https://api.turbonotify.com/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": {"number": "+5511988887777"},
"content": {
"display_name": "João Silva",
"phones": [
{ "label": "Mobile", "number": "+5511977776666" },
{ "label": "Work", "number": "+551133334444" }
],
"emails": [
{ "label": "Work", "address": "joao@acme.com" }
],
"organization": "Acme Corp",
"title": "CTO",
"address": "Av. Paulista 1578, São Paulo - SP"
}
}'
FieldTypeRequiredNotes
content.display_namestringyes1 to 200 characters
content.phones[]arrayyes1 to 10 items; number in E.164
content.phones[].labelstringnodefaults to "Mobile" (Mobile/Home/Work/…)
content.emails[]arrayno0 to 10 items; address validated as an email
content.emails[].labelstringnodefaults to "Email"
content.organizationstringnoup to 200 characters
content.titlestringnoup to 100 characters
content.addressstringnoup to 500 characters

Turbo Notify builds the contact card (vCard) from these fields. A card that was sent cannot be edited: deleting it is the only change. See what can be changed after sending.

Send a calendar event

send-calendar.sh
curl -X POST \
https://api.turbonotify.com/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": {"number": "+5511988887777"},
"content": {
"name": "Sprint Planning",
"description": "Sprint 24 planning.",
"start_at": "2026-05-01T14:00:00Z",
"end_at": "2026-05-01T15:00:00Z",
"join_link": "https://meet.example.com/abc",
"has_reminder": true,
"reminder_offset_sec": 600,
"location": {
"latitude": -23.5505,
"longitude": -46.6333,
"name": "HQ - Main Room"
}
}
}'
FieldTypeRequiredNotes
content.namestringyesevent title; up to 200 characters
content.descriptionstringnoup to 2000 characters
content.start_atdatetimeyesISO-8601 with time zone (anchor field of the calendar variant)
content.end_atdatetimenoISO-8601 with time zone; must be > start_at
content.join_linkstringnohttp:///https:// only
content.locationobjectnosame format as the location object
content.has_reminderboolnodefaults to false
content.reminder_offset_secintnoseconds before the event; only takes effect with has_reminder=true and is ignored, without an error, when has_reminder is false or absent
content.extra_guests_allowedboolnodefaults to false

A message with a button that opens a link. The anchor field is interactive_type, always "cta_url".

POST /v1/messages: link button
{
"to": { "number": "+5511988887777" },
"content": {
"interactive_type": "cta_url",
"body": "Your order has shipped.",
"footer": "Example Store",
"url_button": { "display_text": "Track", "url": "https://example.com/tracking/123" }
}
}
FieldTypeRequiredNotes
content.interactive_typestringyesalways "cta_url"
content.bodystringyes1 to 1024 characters
content.headerobjectno{ "type": "text", "text": "..." }, text with 1 to 60 characters
content.footerstringnoup to 60 characters
content.url_button.display_textstringyes1 to 20 characters
content.url_button.urlstringyeshttp:// or https:// URL

A message with a link button cannot be edited after it is sent: deleting it is the only change. See what can be changed after sending.

How it looks to the recipient

Each type appears in the recipient’s WhatsApp with the native interface: location with a map and a button to open it, contact with a card that can be saved, calendar with an “Add to calendar” button, link button with the button right below the text.

The entry a guest creates in their calendar with the “Add to calendar” button does not follow an edit or a cancel of the event: see What happens to the guests.

To see exactly how the matching message.sent event reaches your webhook, open the Webhook Inspector. The “Enriched” panel renders these types with dedicated cards.

Sending to a group

To send to a group, the to field takes the object { "group_id": "grp_..." } instead of { "number": "+5511988887777" }, which is used for 1:1 chats. See Groups overview for details of the model.

POST /v1/messages: group
{
"to": { "group_id": "grp_e442553a56b14e2695f6acfd7d651b1a" },
"content": {
"text": "Good morning, team"
}
}

The to field is polymorphic. It accepts exactly one of three forms (no kind or type):

to formDestination
{ "number": "+5511988887777" }1:1 message (a person)
{ "group_id": "grp_..." }Group message
{ "contact_id": "ct_..." }1:1 message by Turbo Notify contact ID
  • The three forms are mutually exclusive. Send exactly one.
  • group_id is the public grp_{hex32} identifier received in message.received webhooks/polling (to.group_id) or in the group lifecycle events.
  • The content object uses the same format as 1:1 sends (text, image, video, audio, document, sticker, location, contact, calendar, link button), with no differences.
  • To quote a message inside the group, use the dedicated POST /v1/numbers/{ref}/messages/{message_id}/reply endpoint.

The response has the standard format (message_id, status). The matching message.sent webhook uses to.group_id (the same value you sent in the request); the envelope’s number_alias identifies the number that sent it, which helps you match received and sent messages in the same group.

Sending by contact_id

Use to.contact_id to send to a contact Turbo Notify already knows, without providing the phone number. Turbo Notify uses the number recorded for that contact.

{
"to": { "contact_id": "ct_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" },
"content": { "text": "Hi, how are you?" }
}