Blooio API Reference

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

send.js
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}`)
send.ts
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.js

Always 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-limit
import 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:

webhook.js
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

On this page