BetaThe V4 API is in beta — endpoints and functionality may change.

Message sending basics

A complete guide to sending messages, including chat IDs, options, and common quirks

This guide covers everything you need to know about sending messages through the Blooio API, including chat ID formats, message options, and common pitfalls.

The basics

Send a message by POSTing to /chats/{chatId}/messages:

cURL

curl -X POST 'https://api.blooio.com/v2/api/chats/%2B15551234567/messages' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"text": "Hello!"}'
Try it

Node.js

const chatId = encodeURIComponent('+15551234567')
const res = await fetch(`https://api.blooio.com/v2/api/chats/${chatId}/messages`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.BLOOIO_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ text: 'Hello!' })
})

Python


from urllib.parse import quote

chat_id = quote('+15551234567', safe='')
res = requests.post(f'https://api.blooio.com/v2/api/chats/{chat_id}/messages',
headers={
'Authorization': f"Bearer {os.environ['BLOOIO_API_KEY']}",
'Content-Type': 'application/json'
},
json={'text': 'Hello!'}
)

Chat ID formats

The chatId parameter determines who receives your message. Blooio supports four formats:

Format Example Description
Phone number +15551234567 E.164 format (must be URL-encoded)
Email user@example.com Send to iMessage-enabled email
Group ID grp_abc123 Send to an existing group
Multi-recipient +15551234567,+15559876543 Comma-separated list

URL encoding phone numbers

Phone numbers contain a + character which has special meaning in URLs. Always URL-encode your chat ID:

// ✅ Correct
const chatId = encodeURIComponent('+15551234567')  // becomes %2B15551234567

// ❌ Wrong - the + becomes a space
const chatId = '+15551234567'

Warning

Forgetting to URL-encode phone numbers is the most common integration mistake. The + character becomes a space, resulting in invalid phone numbers.

Multi-recipient messages

Send to multiple people at once with a comma-separated list:

curl -X POST 'https://api.blooio.com/v2/api/chats/%2B15551234567,%2B15559876543/messages' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"text": "Hello everyone!"}'
Try it

When you send to multiple recipients:

  • Blooio creates an unnamed group automatically (or reuses an existing one with the same participants)
  • The response includes group_id and group_created: true if a new group was created
  • Subsequent messages to that combination reuse the same group
{
  "message_id": "msg_abc123",
  "status": "queued",
  "group_id": "grp_xyz789",
  "group_created": true,
  "participants": ["+15551234567", "+15559876543"]
}

Message body options

Text messages

The text field can be a string or an array of strings:

// Single message
{"text": "Hello!"}

// Multiple messages (sent in sequence)
{"text": ["Hello!", "How are you?", "Let me know if you have questions."]}

When you pass an array, each string becomes a separate message. The response includes message_ids (plural) instead of message_id.

Attachments

Include media by providing publicly accessible URLs:

{
  "text": "Check out this image",
  "attachments": ["https://cdn.example.com/image.png"]
}

Or provide named attachments:

{
  "attachments": [
    { "url": "https://cdn.example.com/doc.pdf", "name": "contract.pdf" }
  ]
}

Note

Attachment URLs must be publicly accessible. Blooio downloads them before sending.

Typing indicators

Show a typing indicator before sending:

{
  "text": "Hello!",
  "use_typing_indicator": true
}

This makes messages feel more natural. The default follows your organization's preference in the dashboard.

Sender number

For accounts with multiple numbers, specify which number to send from:

{
  "text": "Hello!",
  "from_number": "+15559876543"
}

See Number Pools for details on how sender selection works.

Message status lifecycle

Messages progress through these statuses:

Status Description
queued Accepted by Blooio, waiting to be handed to Apple/the carrier
sent Transmitted to Apple/the carrier (protocol resolves around here)
delivered Confirmed delivery to the recipient
failed Delivery failed (see the error field)
cancellation_requested A cancel was requested for a still-queued message (best-effort)
cancelled Cancelled before it was dispatched

Inbound messages arrive via webhooks with status received, and read receipts arrive as a read event. See the Message fields reference for the full field-by-field breakdown.

Checking status

Poll the status endpoint:

curl -H 'Authorization: Bearer YOUR_API_KEY' \
  'https://api.blooio.com/v2/api/chats/%2B15551234567/messages/msg_abc123/status'
Try it

Or subscribe to message.status webhooks for real-time updates.

Protocol detection

Blooio automatically detects whether the recipient supports iMessage. The protocol field indicates how the message was sent. It is never null — a freshly-accepted message starts at pending and resolves to a concrete transport within seconds of send:

Protocol Description
pending Accepted and dispatched; the wire service is not resolved yet (transient, settles quickly)
imessage Sent via iMessage (blue bubble)
rcs Sent via RCS
sms Fell back to SMS/MMS (green bubble)
unknown Accepted by the carrier, but the wire service couldn't be resolved before the tracking window closed (see error)

Note

Protocol is resolved at send time. A contact may switch between iMessage and SMS if they change devices or disable iMessage.

Warning

The legacy non-imessage value has been replaced. Unresolved sends now report pending (transient) or unknown (terminal) instead. If you branch on protocol, treat pending as "not yet known" rather than a final transport.

Common quirks and gotchas

Phone numbers must be E.164 format

Always include the country code with + prefix:

// ✅ Correct
"+15551234567";

// ❌ Wrong
"5551234567"; // Missing country code
"1-555-123-4567"; // Contains dashes
"(555) 123-4567"; // Contains formatting

Messages return 202, not 200

Successful message sends return 202 Accepted because messages are queued asynchronously. The message isn't sent yet—it's accepted for delivery.

Use idempotency keys for retries

Network issues can cause duplicate messages. Always include an Idempotency-Key header:

curl -X POST 'https://api.blooio.com/v2/api/chats/%2B15551234567/messages' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-12345-confirmation' \
  -d '{"text": "Order confirmed!"}'
Try it

If you retry with the same key, Blooio returns the original message with 200 instead of creating a duplicate.

Group messages need a linked chat

When sending to a group ID (grp_xxx), the group must have an active iMessage chat linked. Groups created via API don't automatically have chats—the first message creates the link.

Emergency numbers are blocked

Blooio refuses to send to emergency numbers (911, etc.) with a 403 Forbidden response.

Sender availability matters

If no active number is currently available for your account, message sends fail with 503 Service Unavailable. Retry once one of your provisioned senders is available again.

Messaging safety limits exist

Blooio enforces per-conversation allowances and applies graduated protections when a sending number shows risky one-way, repeated-template, delivery, or ban-related activity. Some limits return 403 or 429; queue and slow instead accept the message and defer dispatch. See Messaging safety limits and recovery for every limit, machine-readable error code, and recovery step.

Response codes summary

Code Meaning
200 Duplicate request (idempotency key matched)
202 Message accepted for sending
400 Invalid request (bad phone format, missing text, etc.)
401 Invalid or missing API key
403 Action forbidden (emergency number, wrong number ownership)
404 Chat or group not found
429 Conversation or configured outbound limit reached
503 No active number available

Next steps