Complete tutorial — send and receive iMessages using the Blooio REST API and Python. Code examples with requests, Flask webhooks, and async messaging.
You'll need Python, pip, and a Blooio API key. Setup takes under 2 minutes.
python --versionpip install requests flaskpip install requests flaskSend your first iMessage from Python in under a minute.
import requests
from urllib.parse import quote
API_KEY = "your-api-key"
BASE = "https://backend.blooio.com/v2/api"
to = quote("+15551234567") # URL-encode the phone number
resp = requests.post(
f"{BASE}/chats/{to}/messages",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"text": "Hello from Python!"},
)
print(resp.json())
# {"message_id": "msg_abc123", "status": "queued"}That's it. The chatId path parameter is a URL-encoded E.164 phone number (e.g. %2B15551234567). The message body goes in the text field. Auth is a Bearer token from app.blooio.com.
Text, attachments, group messages, and sender selection.
import requests
from urllib.parse import quote
API_KEY = "your-api-key"
BASE = "https://backend.blooio.com/v2/api"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def send_text(to: str, text: str):
chat_id = quote(to)
resp = requests.post(
f"{BASE}/chats/{chat_id}/messages",
headers=HEADERS,
json={"text": text},
)
resp.raise_for_status()
return resp.json()
result = send_text("+15551234567", "Meeting at 3pm tomorrow")
print(result)
# {"message_id": "msg_abc123", "status": "queued"}def send_with_attachment(to: str, text: str, file_url: str):
chat_id = quote(to)
resp = requests.post(
f"{BASE}/chats/{chat_id}/messages",
headers=HEADERS,
json={
"text": text,
"attachments": [file_url],
},
)
resp.raise_for_status()
return resp.json()
# Attachments can be URLs or objects with url + name
send_with_attachment(
"+15551234567",
"Here's the invoice",
"https://example.com/invoice.pdf",
)def send_to_group(recipients: list, text: str):
"""Send to multiple recipients — chatId is comma-separated numbers.
Blooio auto-creates or reuses an unnamed group for this combo."""
chat_id = quote(",".join(recipients))
resp = requests.post(
f"{BASE}/chats/{chat_id}/messages",
headers=HEADERS,
json={"text": text},
)
resp.raise_for_status()
return resp.json()
result = send_to_group(
["+15551234567", "+15559876543", "+15550001111"],
"Team standup in 10 minutes",
)
print(result)
# {"message_id": "msg_...", "status": "queued", "group_id": "grp_...", ...}def send_from_number(to: str, text: str, from_number: str):
"""Use X-From-Number header to pick a specific sender from your pool."""
chat_id = quote(to)
resp = requests.post(
f"{BASE}/chats/{chat_id}/messages",
headers={
**HEADERS,
"X-From-Number": from_number,
},
json={"text": text},
)
resp.raise_for_status()
return resp.json()
send_from_number("+15551234567", "Hello!", "+15559999999")Set up a Flask webhook to receive incoming iMessages in real time.
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def handle_message():
payload = request.json
event = payload.get("event") # "message.received"
sender = payload.get("external_id") # "+15551234567"
text = payload.get("text") # message body
msg_id = payload.get("message_id") # "msg_xyz789"
protocol = payload.get("protocol") # "imessage" | "sms" | "rcs"
print(f"[{event}] {sender}: {text}")
return "", 200
if __name__ == "__main__":
app.run(port=5000)Register your webhook via the API: POST /webhooks with {"webhook_url": "https://yourserver.com/webhook", "webhook_type": "all"}. Set webhook_type to "message", "status", or "all".
Local development: Use ngrok http 5000 to expose your local Flask server to the internet during development.
{
"event": "message.received",
"message_id": "msg_xyz789",
"external_id": "+15551234567",
"status": "received",
"protocol": "imessage",
"timestamp": 1711648200000,
"internal_id": "+15559876543",
"text": "Hey, what are your hours?",
"attachments": []
}Event types: message.received, message.sent, message.delivered, message.failed, message.read. external_id is the contact's phone/email, internal_id is your Blooio number.
Combine sending and receiving into an echo bot, then upgrade to a ChatGPT-powered assistant.
from flask import Flask, request
import requests as http
from urllib.parse import quote
app = Flask(__name__)
API_KEY = "your-api-key"
BASE = "https://backend.blooio.com/v2/api"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
@app.route("/webhook", methods=["POST"])
def echo():
payload = request.json
if payload.get("event") != "message.received":
return "", 200
sender = payload["external_id"]
text = payload.get("text", "")
chat_id = quote(sender)
http.post(
f"{BASE}/chats/{chat_id}/messages",
headers=HEADERS,
json={"text": f"You said: {text}"},
)
return "", 200
if __name__ == "__main__":
app.run(port=5000)from flask import Flask, request
import requests as http
from urllib.parse import quote
from openai import OpenAI
app = Flask(__name__)
API_KEY = "your-api-key"
BASE = "https://backend.blooio.com/v2/api"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
openai_client = OpenAI(api_key="your-openai-key")
@app.route("/webhook", methods=["POST"])
def ai_reply():
payload = request.json
if payload.get("event") != "message.received":
return "", 200
sender = payload["external_id"]
text = payload.get("text", "")
completion = openai_client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "You are a helpful assistant. Keep replies under 300 chars.",
},
{"role": "user", "content": text},
],
)
reply = completion.choices[0].message.content
chat_id = quote(sender)
http.post(
f"{BASE}/chats/{chat_id}/messages",
headers=HEADERS,
json={"text": reply},
)
return "", 200
if __name__ == "__main__":
app.run(port=5000)Poll the message status endpoint or use status webhooks to track delivery.
from urllib.parse import quote
def check_status(chat_id: str, message_id: str):
"""GET /chats/{chatId}/messages/{messageId}/status"""
encoded = quote(chat_id)
resp = requests.get(
f"{BASE}/chats/{encoded}/messages/{message_id}/status",
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()
# After sending a message
result = send_text("+15551234567", "Hello!")
status = check_status("+15551234567", result["message_id"])
print(status)
# {
# "message_id": "msg_abc123",
# "chat_id": "+15551234567",
# "direction": "outbound",
# "status": "delivered",
# "protocol": "imessage",
# "time_sent": 1711648200000,
# "time_delivered": 1711648201000
# }Status values: queued → pending → sent → delivered. Failures show as failed with an error field. Protocol can be imessage, sms, or rcs.
Common errors and how to handle them gracefully.
Invalid or missing Bearer token. Check the Authorization header.
headers={"Authorization": "Bearer bl_live_..."}Too many requests. Default limit is 60 req/min.
import time; time.sleep(1) # back offInvalid phone number format or missing required fields.
chatId must be URL-encoded E.164: "%2B15551234567"No active phone number available to send from. Check your dashboard.
Ensure at least one number is active in your Blooio accountimport time
import requests
from urllib.parse import quote
def send_with_retry(to: str, text: str, max_retries: int = 3):
chat_id = quote(to)
for attempt in range(max_retries):
resp = requests.post(
f"{BASE}/chats/{chat_id}/messages",
headers=HEADERS,
json={"text": text},
)
if resp.status_code in (200, 202):
return resp.json()
if resp.status_code == 429:
wait = 2 ** attempt
print(f"Rate limited. Retrying in {wait}s...")
time.sleep(wait)
continue
if resp.status_code == 401:
raise ValueError("Invalid API key — check your Bearer token")
resp.raise_for_status()
raise RuntimeError("Max retries exceeded")A production-ready script combining sending, receiving, status checks, and error handling.
"""
Blooio iMessage Python Client
Send and receive iMessages via the Blooio REST API (v2).
API docs: https://docs.blooio.com
"""
import time
import requests
from urllib.parse import quote
from flask import Flask, request as flask_request
API_KEY = "your-api-key"
BASE = "https://backend.blooio.com/v2/api"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
app = Flask(__name__)
# ── Sending ──────────────────────────────────────────
def send_message(to: str, text: str, attachments: list = None,
from_number: str = None):
"""Send an iMessage. `to` is E.164 phone number or email."""
chat_id = quote(to)
payload = {"text": text}
if attachments:
payload["attachments"] = attachments
headers = {**HEADERS}
if from_number:
headers["X-From-Number"] = from_number
return _post_with_retry(f"{BASE}/chats/{chat_id}/messages", payload, headers)
def send_bulk(recipients: list, text: str):
"""Send the same message to multiple individual recipients."""
results = []
for number in recipients:
r = send_message(number, text)
results.append(r)
time.sleep(0.1)
return results
def send_to_group(recipients: list, text: str):
"""Send to a multi-recipient group chat (auto-created or reused)."""
chat_id = quote(",".join(recipients))
return _post_with_retry(f"{BASE}/chats/{chat_id}/messages", {"text": text})
# ── Status ───────────────────────────────────────────
def get_status(chat_id: str, message_id: str):
"""Check delivery status of a sent message."""
encoded = quote(chat_id)
resp = requests.get(
f"{BASE}/chats/{encoded}/messages/{message_id}/status",
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()
# ── Receiving (webhook) ─────────────────────────────
@app.route("/webhook", methods=["POST"])
def handle_incoming():
payload = flask_request.json
event = payload.get("event")
if event == "message.received":
sender = payload["external_id"]
text = payload.get("text", "")
print(f"[iMessage] {sender}: {text}")
send_message(sender, "Thanks for your message! We'll get back to you soon.")
return "", 200
# ── Helpers ──────────────────────────────────────────
def _post_with_retry(url: str, payload: dict, headers: dict = None,
max_retries: int = 3):
hdrs = headers or HEADERS
for attempt in range(max_retries):
resp = requests.post(url, headers=hdrs, json=payload)
if resp.status_code in (200, 202):
return resp.json()
if resp.status_code == 429:
time.sleep(2 ** attempt)
continue
resp.raise_for_status()
raise RuntimeError(f"Failed after {max_retries} retries")
# ── Main ─────────────────────────────────────────────
if __name__ == "__main__":
# Send a test message
result = send_message("+15551234567", "Hello from Python!")
print("Sent:", result)
# Check delivery status
status = get_status("+15551234567", result["message_id"])
print("Status:", status)
# Start webhook server (for receiving messages)
# app.run(port=5000)Common questions about sending iMessages with Python.
Not directly — Apple doesn't expose a public iMessage API. However, you can use Blooio's REST API to send and receive iMessages from any Python script using the requests library. Blooio handles the iMessage routing so your code just makes standard HTTP calls.
There's no free iMessage API since iMessage is a proprietary Apple protocol. Blooio offers a REST API starting at $39/month with unlimited messages. You can start a free trial at app.blooio.com to test the Python integration before committing.
You can use subprocess to call AppleScript on a Mac, but it only works locally on macOS with a signed-in iMessage account, can't run on servers, and breaks with macOS updates. The Blooio API approach works from any OS (Linux, Windows, macOS), runs on servers, and is production-ready.
Get your API key and send your first iMessage in under a minute. Full REST API, webhooks, and Python examples included.
Step-by-step guide to sending iMessages via API from your application.
Read guideBuild an AI-powered iMessage assistant using OpenAI, Blooio webhooks, and n8n.
Read guideCreate automated iMessage bots for customer support, sales, and notifications.
Read guide