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 requestspip install httpxSend your first iMessage
The minimal send looks like this:
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.pyAlways 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:
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:
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 3001Then 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:
raiseSee 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
- Message sending basics — multipart messages, mentions, reply threading
- Bulk iMessage patterns
- Twilio fallback integration — route to SMS when iMessage isn't available
- Authentication — scoping API keys and rotation
- API reference — every endpoint