Blooio API Reference

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.

This guide walks through sending and receiving iMessages from Python using the Blooio REST API. By the end you'll have a minimal send script, an async version, and a FastAPI webhook receiver you can drop into a real app.

What you'll need

  • Python 3.9 or newer
  • A Blooio API key (get one in the dashboard)
  • A phone number the recipient can receive iMessages on

Install a single HTTP dependency:

pip install requests
pip install httpx

Send your first iMessage

The minimal send looks like this:

send.py
import os
import requests
from urllib.parse import quote

API_KEY = os.environ['BLOOIO_API_KEY']
BASE_URL = 'https://backend.blooio.com/v2/api'

def send_imessage(to: str, text: str) -> dict:
    chat_id = quote(to, safe='')
    res = requests.post(
        f'{BASE_URL}/chats/{chat_id}/messages',
        headers={
            'Authorization': f'Bearer {API_KEY}',
            'Content-Type': 'application/json',
        },
        json={'text': text},
        timeout=10,
    )
    res.raise_for_status()
    return res.json()

if __name__ == '__main__':
    result = send_imessage('+15551234567', 'Hello from Python!')
    print(f"Message ID: {result['message_id']}, status: {result['status']}")

Run it:

export BLOOIO_API_KEY=sk_live_your_key_here
python send.py

Always URL-encode the phone number with urllib.parse.quote(..., safe=''). The + in +15551234567 has special meaning in URLs and will be interpreted as a space if not encoded.

Check iMessage capability first

Not every phone number can receive iMessages. Before sending, hit the capabilities endpoint:

def has_imessage(phone: str) -> bool:
    chat_id = quote(phone, safe='')
    res = requests.get(
        f'{BASE_URL}/contacts/{chat_id}/capabilities',
        headers={'Authorization': f'Bearer {API_KEY}'},
        timeout=10,
    )
    res.raise_for_status()
    return res.json().get('capabilities', {}).get('imessage', False)

if has_imessage('+15551234567'):
    send_imessage('+15551234567', 'Blue bubbles only!')
else:
    print('Recipient does not support iMessage; fall back to SMS.')

Send an iMessage with attachments

Pass a list of publicly-reachable URLs:

def send_with_attachments(to: str, text: str, attachments: list[str]) -> dict:
    chat_id = quote(to, safe='')
    res = requests.post(
        f'{BASE_URL}/chats/{chat_id}/messages',
        headers={
            'Authorization': f'Bearer {API_KEY}',
            'Content-Type': 'application/json',
        },
        json={
            'text': text,
            'attachments': attachments,
        },
        timeout=30,
    )
    res.raise_for_status()
    return res.json()

send_with_attachments(
    '+15551234567',
    'Check out this photo',
    ['https://example.com/photo.jpg'],
)

Attachments can be images, videos, PDFs, or any file type iMessage supports. See Attachments for size limits and MIME handling.

Send to a group

Groups are chats too. Pass the group ID as the chatId:

def send_to_group(group_id: str, text: str) -> dict:
    res = requests.post(
        f'{BASE_URL}/chats/{group_id}/messages',
        headers={
            'Authorization': f'Bearer {API_KEY}',
            'Content-Type': 'application/json',
        },
        json={'text': text},
        timeout=10,
    )
    res.raise_for_status()
    return res.json()

send_to_group('grp_abc123', 'Morning team!')

Async with httpx

For high-throughput apps use httpx with asyncio:

send_async.py
import asyncio
import os
import httpx
from urllib.parse import quote

API_KEY = os.environ['BLOOIO_API_KEY']
BASE_URL = 'https://backend.blooio.com/v2/api'

async def send_imessage(client: httpx.AsyncClient, to: str, text: str) -> dict:
    chat_id = quote(to, safe='')
    res = await client.post(
        f'{BASE_URL}/chats/{chat_id}/messages',
        headers={'Authorization': f'Bearer {API_KEY}'},
        json={'text': text},
        timeout=10.0,
    )
    res.raise_for_status()
    return res.json()

async def main():
    recipients = ['+15551234567', '+15557654321', '+15559876543']
    async with httpx.AsyncClient() as client:
        results = await asyncio.gather(
            *[send_imessage(client, r, 'Hi!') for r in recipients]
        )
    for r in results:
        print(r['message_id'])

if __name__ == '__main__':
    asyncio.run(main())

Idempotency

To make retries safe, pass an Idempotency-Key header:

import uuid

def send_idempotent(to: str, text: str, key: str) -> dict:
    chat_id = quote(to, safe='')
    res = requests.post(
        f'{BASE_URL}/chats/{chat_id}/messages',
        headers={
            'Authorization': f'Bearer {API_KEY}',
            'Content-Type': 'application/json',
            'Idempotency-Key': key,
        },
        json={'text': text},
        timeout=10,
    )
    res.raise_for_status()
    return res.json()

# Derive the key from your domain entity — order ID, job ID, etc.
send_idempotent('+15551234567', 'Order #4281 shipped', 'order-4281-shipped')

Retries with the same key within 24 hours return the original response without creating a duplicate.

Receive incoming iMessages (FastAPI)

Blooio delivers inbound messages and status events via webhooks. Here is a minimal FastAPI receiver:

webhook.py
import hashlib
import hmac
import os
from fastapi import FastAPI, Header, HTTPException, Request

SIGNING_SECRET = os.environ['BLOOIO_WEBHOOK_SECRET']
app = FastAPI()

def verify_signature(body: bytes, signature: str) -> bool:
    expected = hmac.new(
        SIGNING_SECRET.encode(),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or '')

@app.post('/webhooks/blooio')
async def blooio_webhook(
    request: Request,
    x_blooio_signature: str = Header(default=''),
    x_blooio_event: str = Header(default=''),
):
    body = await request.body()
    if not verify_signature(body, x_blooio_signature):
        raise HTTPException(status_code=401, detail='Invalid signature')

    event = await request.json()

    if x_blooio_event == 'message.received':
        text = event.get('data', {}).get('text')
        sender = event.get('data', {}).get('from')
        print(f'{sender}: {text}')

    return {'ok': True}

Run it:

pip install fastapi uvicorn
export BLOOIO_WEBHOOK_SECRET=whsec_your_secret
uvicorn webhook:app --port 3001

Then register the webhook URL with Blooio — see Receive webhooks locally to tunnel localhost.

See Webhook signatures for the exact signing algorithm and replay-attack prevention.

Handling errors

Blooio returns standard HTTP status codes and a consistent error body. Handle them explicitly:

from requests.exceptions import HTTPError

try:
    send_imessage('+15551234567', 'hi')
except HTTPError as e:
    status = e.response.status_code
    body = e.response.json()
    if status == 429:
        retry_after = int(e.response.headers.get('Retry-After', '1'))
        print(f'Rate limited; retry in {retry_after}s')
    elif status == 400:
        print(f"Invalid request: {body.get('message')}")
    elif status >= 500:
        print('Blooio is having issues; retry with backoff')
    else:
        raise

See Error handling for retry strategies and backoff patterns.

Environment variables

Keep your API key out of your codebase:

# .env
BLOOIO_API_KEY=sk_live_...
BLOOIO_WEBHOOK_SECRET=whsec_...
from dotenv import load_dotenv
load_dotenv()

Going further

On this page