For developers
Building on an iMessage API
Apple ships no endpoint, so everything here is a third-party REST API in front of Apple infrastructure. The calls are simple. The parts that decide whether your integration is still working in three months are not, and they are what this page is about.
The mental model
A provider runs real Apple accounts on real Apple hardware and assigns you a phone number. You POST a recipient and some text; a blue bubble arrives. Replies and delivery events come back to you as webhooks.
Everything unusual about the category follows from that. Pricing is per line rather than per message. Daily sending limits are real and low. And a line can degrade without any error appearing in your logs, because the API and the channel are different systems. What is actually behind the number.
Do you need an SDK?
Almost certainly not. These are small REST APIs — send, status, webhook — and a hand-written client is about forty lines with no supply chain attached. If a provider does publish an SDK, check the last release date before depending on it; in this category a package that has been quiet for two years is common and is a warning rather than a sign of stability.
The exception worth taking is an OpenAPI specification. Given one you can generate a typed client, diff it when it changes, and mock the whole API in tests.
Start with four curl commands, not an integration
status_callback at a request bin and watch the delivery events. Reply from the phone and watch the inbound webhook. Send to an Android number and see what the provider does. Twenty minutes, and it tells you more than the marketing site.Never expose these client-side
NEXT_PUBLIC_ is a two-character mistake that ships it to every visitor. Anyone reading your bundle can then send from your number, at your cost, in your business’s name.Quickstart
Sending your first message
Field names follow Sendblue's documented shape because theirs is public. Every provider differs in naming and matches in structure — auth header, recipient, body, callback URL.
curl --request POST 'https://api.sendblue.co/api/send-message' \
--header 'sb-api-key-id: YOUR_SB_API_KEY_ID' \
--header 'sb-api-secret-key: YOUR_SB_API_SECRET_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"number": "+19998887777",
"content": "Your 2pm window is confirmed.",
"status_callback": "https://acme.co/webhooks/imessage"
}'The whole API surface, in one request.
const response = await fetch("https://api.sendblue.co/api/send-message", {
method: "POST",
headers: {
"sb-api-key-id": process.env.IMESSAGE_KEY_ID!,
"sb-api-secret-key": process.env.IMESSAGE_SECRET!,
"Content-Type": "application/json",
},
body: JSON.stringify({
number: "+19998887777",
content: "Your 2pm window is confirmed.",
status_callback: "https://acme.co/webhooks/imessage",
}),
// Without this, a hung provider hangs your request handler with it.
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) {
// 429 and 5xx are worth retrying. A 422 — recipient not on iMessage —
// will fail identically forever, so route it to your SMS fallback.
throw new Error(`send failed ${response.status}`);
}Node 20+. No dependencies — fetch and AbortSignal.timeout are built in.
import os, httpx
response = httpx.post(
"https://api.sendblue.co/api/send-message",
headers={
"sb-api-key-id": os.environ["IMESSAGE_KEY_ID"],
"sb-api-secret-key": os.environ["IMESSAGE_SECRET"],
},
json={
"number": "+19998887777",
"content": "Your 2pm window is confirmed.",
"status_callback": "https://acme.co/webhooks/imessage",
},
timeout=httpx.Timeout(10.0, connect=5.0),
)
response.raise_for_status()
print(response.json()["message_handle"])Reuse a module-level httpx.Client in production so the TLS handshake is not paid per send.
What the quickstarts leave out
The four things that decide whether this survives
Retryable versus permanent failures
A 429 will succeed in four seconds. A 422 — recipient not on iMessage — will fail identically forever. Treating them the same wastes your rate limit and never delivers the message. This is the most common defect in these integrations.
Retry logic that works →Webhook verification and idempotency
Your endpoint is public and guessable. Verify the HMAC over the raw bytes before any parser touches them, return 200 before doing work, and make every consequence safe to apply twice — because at-least-once delivery means it will be.
The inbound guide →Pacing and warm-up
A line is one number behaving like a person. Two hundred messages in ninety seconds is not a person. New lines want single-digit sends per minute and a daily cap well under a hundred for the first fortnight.
The warm-up schedule →Watching delivered rate, not uptime
A throttled line usually returns 200 OK. The status page stays green while delivery quietly collapses. Track delivered events as a ratio of sends, alert on a floor, and run an hourly canary to a phone you own.
What to instrument →Documentation
Whose docs you can read before signing up
Documentation posture is the cheapest available proxy for engineering quality, and it varies more here than the pricing does.
Reference is published; confirm current field names against their docs before you build.
API referenceEndpoints worth checking for
The features that reveal platform depth
Sending text is table stakes. These are the ones nobody builds first, and their presence tells you how deep the integration goes.
| Capability | Why it is a signal |
|---|---|
| Sending and removing a reaction | Removal is a different Apple operation from adding. Support for it is the most reliable tell in the category. |
| Group threads | Creating, adding a participant, leaving. Substantially harder than one-to-one, and frequently missing. |
| Typing indicators on demand | Sent as a real indicator rather than faked with a client-side delay. |
| Number lookup | Is this recipient reachable on iMessage? Saves failed sends and, on metered plans, money. |
| Message status by handle | Reconcile state without depending on a webhook you might have missed. |
| Documented rate limits, in numbers | "Generous" is not a limit. You need a figure per minute and per day, and the behaviour when you cross it. |
Before you commit
Read these first
There is no official Apple API
Every framework Apple ships keeps a human or a prior customer action in the loop. Knowing exactly which is which saves a week.
The open-source landscape
BlueBubbles, Matrix bridges, chat.db readers — what each is genuinely good at, and where the free path stops.
Every way to send programmatically
AppleScript, self-hosted bridge, third-party API, Apple's own channel — ranked by how long they keep working.
iMessage API vs RCS API
Same feature list, completely different integration. Which population each actually reaches.
Is there a SOC 2 compliant provider?
More than one claims it. What the report covers, what it cannot cover, and the questions to ask.
Building it into a SaaS product
Multi-tenant line allocation, consent you do not own, and a cost structure that can invert your margins.
Further reading
Read next
iMessage from Node.js
A working Node.js integration: a typed client, webhook verification with Express, retries that respect rate limits, and a send queue that will not get your line flagged.
12 min readOperationsiMessage from Python
A production-shaped Python integration — httpx client with retries, a FastAPI webhook with signature verification, and the asyncio pacing that keeps a line alive.
11 min readOperationsWebhooks and inbound replies
The inbound half of an iMessage integration — verifying signatures, surviving at-least-once delivery, handling out-of-order events, and what to do with a reply once you have it.
9 min read