If you've ever tried to send an iMessage programmatically, you've probably hit the same wall everyone does: Apple has no public iMessage API. There's no POST /imessage in the developer docs, no SDK, no OAuth scope. Yet "blue bubble" delivery has 3–4× the open rates of SMS, so the demand to send iMessages from code — for CRMs, bots, notifications, and outbound — keeps growing.
This guide covers the realistic options, then walks through actually sending and receiving iMessages over a REST API with working Python, Node.js, and curl examples you can paste and run today.
Why there's no official iMessage API
iMessage is a closed, end-to-end-encrypted protocol tied to Apple IDs and Apple hardware. Apple has never shipped a public API to send iMessages, and "Messages for Business" is a support-inbox product gated behind an approval process — not a way to send outbound messages from a script.
So historically, developers reached for hacks:
| Approach | Works from a server? | Reliability | Receiving messages | Notes |
|---|---|---|---|---|
AppleScript / osascript
|
No — needs a logged-in Mac with Messages open | Brittle | Polling the local SQLite chat.db
|
Mac-only, breaks on macOS updates |
| Shortcuts automation | No | Brittle | No | Manual, not built for scale |
| "Just use SMS" (Twilio etc.) | Yes | High | Yes | Green bubbles, no typing indicators/tapbacks/HD media |
| Hosted iMessage REST API | Yes | High | Yes (webhooks) | What this guide uses |
The AppleScript route is fine for a one-off script on your own Mac. The moment you want to send from a server, send at scale, or receive replies reliably, you need a hosted API that manages the Apple side for you and exposes a normal HTTP interface.
The setup
For the examples below I'm using Blooio, an iMessage REST API. Any provider with a similar HTTP surface will follow the same patterns — the concepts (Bearer auth, a send endpoint, webhooks for inbound) are what matter.
You'll need:
- An API key (Blooio gives you one in the dashboard — no credit card, no A2P/10DLC registration, no DUNS number)
- A phone number you can test against
Base URL for all calls:
https://api.blooio.com/v2/api
Send your first iMessage (curl)
The whole thing is one authenticated POST. The chat is identified by the recipient's phone number (URL-encoded, because of the +):
curl -X POST \
"https://api.blooio.com/v2/api/chats/%2B15551234567/messages" \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"text": "Hello from the command line 👋"}'
Response:
{ "message_id": "msg_a1b2c3", "status": "queued" }
That's it — no Mac, no AppleScript, no polling. If the recipient has iMessage, it lands as a blue bubble.
Send an iMessage from Python
The minimal version uses requests. Note the quote(..., safe='') on the phone number — forgetting to URL-encode the + is the #1 cause of silent 400s.
import os
import requests
from urllib.parse import quote
API_KEY = os.environ["BLOOIO_API_KEY"]
BASE_URL = "https://api.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 {result['message_id']} -> {result['status']}")
export BLOOIO_API_KEY=sk_live_your_key_here
python send.py
Send an iMessage from Node.js
Node 18+ has native fetch, so the basic case needs zero dependencies:
const API_KEY = process.env.BLOOIO_API_KEY;
const BASE_URL = "https://api.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 ${result.message_id} -> ${result.status}`);
Blue bubble or green? Check capability first
Not every number can receive iMessage. If you're building an iMessage-first flow with SMS fallback, check capability before sending:
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,
)
if not res.ok:
return False
return bool(res.json().get("capabilities", {}).get("imessage"))
if has_imessage("+15551234567"):
send_imessage("+15551234567", "Blue bubbles only ✨")
else:
# fall back to your SMS provider here
...
Receiving iMessages (webhooks)
Sending is half the story — to build a bot or a two-way CRM thread you need inbound messages. Instead of polling, you register a webhook URL and the API POSTs events to you. Verify the HMAC signature so you only trust real events:
import express from "express";
import crypto from "node:crypto";
const app = express();
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");
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
if (!ok) 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}`);
// ...reply, route to your CRM, trigger an LLM, etc.
}
res.sendStatus(200);
},
);
app.listen(3001, () => console.log("Listening on :3001"));
For local development, tunnel the endpoint with ngrok and point the webhook at the tunnel URL. Now you have a full send + receive loop — enough to build an iMessage bot.
Automating without writing code
If you'd rather wire iMessage into existing tools than write a service, the same API drops into no-code/low-code platforms. Blooio has native nodes/actions for n8n, GoHighLevel, HubSpot, and Zapier — so "new lead in CRM → send iMessage" is a couple of clicks rather than a deploy.
Gotchas worth knowing up front
-
URL-encode the phone number.
+15551234567must become%2B15551234567. This bites everyone once. - No A2P/10DLC registration, DUNS, or campaign approval with a hosted iMessage API — that paperwork is an SMS/carrier thing. iMessage rides Apple's network.
-
Use idempotency keys for retries. Pass an
Idempotency-Keyheader derived from your domain entity (e.g.order-123-shipped) so a retry never double-sends. -
Throttle bulk sends. Don't fire 1,000 parallel requests — cap concurrency (e.g.
p-limitin Node, a semaphore in Python) and back off on429. - iMessage ≠ guaranteed. If a number isn't on iMessage, check capability and fall back to SMS rather than assuming a blue bubble.
Wrapping up
You don't need a Mac mini farm or fragile AppleScript to send iMessages programmatically anymore. A hosted iMessage REST API turns it into a normal HTTP call — POST to send, webhooks to receive — that works the same from Python, Node.js, curl, or your automation tool of choice.
If you want to try the exact examples above, you can grab a free key (no credit card, no A2P registration) and read the full reference here:
What are you building — a bot, a CRM integration, outbound? Drop it in the comments. 👇
Top comments (0)