Send iMessage with Node.js
A Node.js (JavaScript and TypeScript) tutorial for sending and receiving iMessages via the Blooio REST API — fetch, async/await, Express webhooks, and bulk patterns.
This guide walks through sending and receiving iMessages from Node.js using the Blooio REST API. Works with any Node 18+ runtime — the native fetch means zero dependencies for the basic case.
What you'll need
- Node.js 18 or newer (for native
fetch) - A Blooio API key from the dashboard
- A phone number the recipient can receive iMessages on
No SDK required. Blooio's REST API works with fetch, axios, undici, or any HTTP client.
Send your first iMessage
const API_KEY = process.env.BLOOIO_API_KEY
const BASE_URL = 'https://backend.blooio.com/v2/api'
async function sendImessage(to, text) {
const chatId = encodeURIComponent(to)
const res = await fetch(`${BASE_URL}/chats/${chatId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text }),
})
if (!res.ok) {
throw new Error(`Blooio ${res.status}: ${await res.text()}`)
}
return res.json()
}
const result = await sendImessage('+15551234567', 'Hello from Node.js!')
console.log(`Message ID: ${result.message_id}, status: ${result.status}`)type SendResponse = {
message_id: string
status: 'queued' | 'sent' | 'delivered' | 'failed'
}
const API_KEY = process.env.BLOOIO_API_KEY!
const BASE_URL = 'https://backend.blooio.com/v2/api'
async function sendImessage(to: string, text: string): Promise<SendResponse> {
const chatId = encodeURIComponent(to)
const res = await fetch(`${BASE_URL}/chats/${chatId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text }),
})
if (!res.ok) {
throw new Error(`Blooio ${res.status}: ${await res.text()}`)
}
return res.json() as Promise<SendResponse>
}
const result = await sendImessage('+15551234567', 'Hello from TypeScript!')
console.log(`Message ID: ${result.message_id}, status: ${result.status}`)Run it:
export BLOOIO_API_KEY=sk_live_your_key_here
node send.jsAlways encodeURIComponent phone numbers. Without it the + becomes a space and the request fails silently with a 400.
Check capability before sending
Blooio only charges for iMessages that successfully dispatch through Apple's network, but you should still check capability when building fallback flows:
async function hasImessage(phone: string): Promise<boolean> {
const chatId = encodeURIComponent(phone)
const res = await fetch(`${BASE_URL}/contacts/${chatId}/capabilities`, {
headers: { 'Authorization': `Bearer ${API_KEY}` },
})
if (!res.ok) return false
const { capabilities } = await res.json()
return Boolean(capabilities?.imessage)
}
if (await hasImessage('+15551234567')) {
await sendImessage('+15551234567', 'Blue bubbles only!')
}Attachments and multipart messages
Send images, videos, or PDFs by passing public URLs:
await fetch(`${BASE_URL}/chats/${chatId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: 'Check out this photo',
attachments: ['https://example.com/photo.jpg'],
}),
})For a single bubble containing text and a link/photo use parts:
body: JSON.stringify({
parts: [
{ text: 'Order update — tracking attached' },
{ url: 'https://tracking.example.com/abc', name: 'Track package' },
],
}),See Attachments for size limits and Link previews for rich-link behaviour.
Bulk sending with concurrency control
Don't fire 1,000 parallel requests. Use a concurrency limiter — the p-limit package is the simplest option:
npm install p-limitimport pLimit from 'p-limit'
const limit = pLimit(10) // 10 concurrent sends
const recipients = ['+15551234567', '+15557654321' /* ... */]
const results = await Promise.allSettled(
recipients.map((to) =>
limit(() => sendImessage(to, `Hi ${to}!`))
)
)
const successes = results.filter((r) => r.status === 'fulfilled').length
const failures = results.filter((r) => r.status === 'rejected').length
console.log(`Sent ${successes}, failed ${failures}`)For reliable bulk delivery use a queue (BullMQ, PgBoss) and persist an idempotency key per recipient so retries don't create duplicates.
Idempotency
Pass an Idempotency-Key header on POST requests to make them safe to retry:
await fetch(`${BASE_URL}/chats/${chatId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `order-${orderId}-shipped`,
},
body: JSON.stringify({ text: `Order ${orderId} shipped!` }),
})Retries with the same key within 24 hours return the original response without creating a duplicate. Use a deterministic key derived from your domain entity, not a random UUID.
Receive incoming iMessages (Express)
Blooio delivers inbound messages and status events via webhooks. Minimal Express receiver:
import express from 'express'
import crypto from 'node:crypto'
const app = express()
// Raw body is required for signature verification
app.post(
'/webhooks/blooio',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.header('x-blooio-signature') ?? ''
const event = req.header('x-blooio-event') ?? ''
const secret = process.env.BLOOIO_WEBHOOK_SECRET
const expected = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex')
if (
!crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
)
) {
return res.sendStatus(401)
}
const payload = JSON.parse(req.body.toString('utf8'))
if (event === 'message.received') {
const { from, text } = payload.data
console.log(`${from}: ${text}`)
}
res.sendStatus(200)
}
)
app.listen(3001, () => console.log('Listening on :3001'))Register the webhook URL with Blooio once it's reachable — for local dev, tunnel with ngrok: see Receive webhooks locally.
See Webhook signatures for the full signing algorithm and replay-attack prevention.
Error handling and retries
Blooio returns standard HTTP status codes and a consistent JSON error body:
async function sendWithRetry(
to: string,
text: string,
maxAttempts = 3,
): Promise<SendResponse> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const chatId = encodeURIComponent(to)
const res = await fetch(`${BASE_URL}/chats/${chatId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text }),
})
if (res.ok) return res.json()
// Don't retry client errors except 429
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
throw new Error(`Blooio ${res.status}: ${await res.text()}`)
}
const retryAfter = Number(res.headers.get('retry-after') ?? '1')
const backoffMs = Math.max(retryAfter * 1000, 2 ** attempt * 250)
await new Promise((r) => setTimeout(r, backoffMs))
}
throw new Error(`Blooio: failed after ${maxAttempts} attempts`)
}See Error handling for the complete error taxonomy.
Environment and secrets
Keep keys out of the codebase:
# .env
BLOOIO_API_KEY=sk_live_...
BLOOIO_WEBHOOK_SECRET=whsec_...import 'dotenv/config'Going further
- Message sending basics — chat IDs, mentions, reply threading
- iMessage REST API reference — conventions, pagination, idempotency rules
- Send with Python — same patterns in Python
- Twilio fallback integration — SMS backup for non-iMessage numbers
- API reference — every endpoint and parameter
Send iMessage with Python
A hands-on Python tutorial for sending and receiving iMessages via the Blooio REST API — requests, async with httpx, webhooks, and a working FastAPI example.
Contact cards (Name & Photo)
How to manage and share your iMessage contact card — the Name & Photo identity others see in conversations