
Apple has no public iMessage API. Figment routes around that gap with a user-approved connection to a signed-in Mac, then exposes a REST interface your app calls like any other HTTP service. The user grants access through Figment's hosted consent screen; your app receives a scoped bearer token—not an Apple password. Reading history, searching conversations, and sending replies live behind exactly two scopes: messages:read and messages:send.
iMessage operates on a device-local basis: message history is accessible on a Mac signed in to your Apple Account. Figment runs a signed-in isolated Mac that hosts the native Messages app; your application calls a REST API, and the Mac executes the Apple-native operations against its local message store.
A service can be in any of these states: connecting, needs_approval, syncing, ready, offline, reconnect_required, or revoked. ready is required for the service to accept read and send operations; non-ready services reject requests outright. A ready state does not prove Automation permission, guaranteed delivery, complete history, or continuous uptime. The status endpoint enforces authorization as well—it may return a non-2xx response for a revoked or non-ready connection rather than a diagnostic state object. Use figment.so/apple-connect/account to monitor and manage connection setup.
Register your application at figment.so/apple-connect/developers to receive a client_id. Authorization uses PKCE with S256; generate a fresh verifier and state for each attempt:
GET https://figment.so/apple-connect/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.example.com/callback
&response_type=code
&scope=messages%3Aread%20messages%3Asend
&state=OPAQUE_RANDOM_VALUE
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
The redirect_uri is the exact HTTPS URL you registered; its path is not fixed by Figment. When redirected back, validate that the returned state matches the value you sent before proceeding to the token exchange.
Figment's consent screen lets the user select an existing Mac connection and approve the requested scopes. When messages:send is in scope, the user selects which existing conversations your app may send into—that selection is recorded in the grant and enforced on every send. Figment issues the grant and token; this is not an Apple-issued authorization redirect.
Exchange the authorization code for tokens:
POST https://figment.so/api/apple-connect/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fyour-app.example.com%2Fcallback&code=AUTHORIZATION_CODE&code_verifier=YOUR_CODE_VERIFIER
The response includes access_token, refresh_token, expires_in (900 s for the access token), scope, and connection_id. Tokens are opaque strings, not JWTs. Store connection_id alongside your tokens; refresh using grant_type=refresh_token, client_id, and refresh_token as form fields, and store the new refresh token returned by each successful refresh—refresh tokens rotate per grant; serialize refresh calls per grant.
All paths below are relative to BASE_URL = https://figment.so/api/apple-connect.
Both history and search require messages:read. Confirm the connection is ready before issuing either.
Check connection status:
GET /v1/connections/{connection_id}
Authorization: Bearer ACCESS_TOKEN
{
"connection": {
"id": "con_...",
"mode": "live",
"services": {
"messages": { "state": "ready", "reason": null, "observedAt": 1794398400000 }
}
}
}
Each service entry is an object with state, reason, and observedAt (epoch milliseconds). Read connection.services.messages.state; only granted services appear. A ready state confirms the connection is accepted, but does not guarantee complete history—older messages may still be arriving from iCloud.
List open chats:
GET /v1/connections/{connection_id}/chats?limit=20&offset=0
Authorization: Bearer ACCESS_TOKEN
Responses use {data, mode, offset, nextOffset, hasMore, historyComplete, scope}. Follow hasMore and nextOffset to paginate; nextOffset is null when exhausted. Use each chat item's guid as chatGuid in history and search calls; displayName may be absent.
Pull messages from a thread:
URL-encode chatGuid from the chats response before placing it in a query string—a raw + decodes as a space:
GET /v1/connections/{connection_id}/messages?chatGuid=iMessage%3B-%3B%2B15551234567&limit=20
Authorization: Bearer ACCESS_TOKEN
Message items include guid, text, isFromMe, and chatGuid. Check historyComplete and hasMore rather than treating an empty page as a definitive absence of history.
Full-text search runs against the locally available message store and shares messages:read—there is no separate search scope:
GET /v1/connections/{connection_id}/messages/search?query=invoice&chatGuid=iMessage%3B-%3B%2B15551234567&limit=20
Authorization: Bearer ACCESS_TOKEN
Coverage is bounded by history present on the Mac at query time. Accepted parameters: query, chatGuid, limit (default 20, max 100), offset (max 10 000). When the grant covers selected conversations, chatGuid is required. Encode chatGuid with a URL builder.
A chat appearing in search results is not automatically authorized for sends. The messages:send grant must already include that exact existing conversation from the user's original approval; a search result alone does not confer send access.
The contract is two steps: submit the message, then poll the receipt with the same key.
Step 1 — Send:
POST /v1/connections/{connection_id}/messages/send
Authorization: Bearer ACCESS_TOKEN
Idempotency-Key: order-8821-shipped-notify
Content-Type: application/json
{"chatGuid": "iMessage;-;+15551234567", "text": "Your order has shipped. Tracking: 1Z999AA1."}
Generate and persist the Idempotency-Key before the first request; derive it from a stable intent identifier like an order ID plus action label. Do not use a body hash alone—two separately intended identical messages would share that hash and wrongly merge. A repeated request with the same key and body returns the same recorded action; a changed body is rejected.
Step 2 — Poll the receipt:
GET /v1/connections/{connection_id}/messages/receipt
Authorization: Bearer ACCESS_TOKEN
Idempotency-Key: order-8821-shipped-notify
Fresh send result (sandbox):
{
"data": {
"idempotencyKey": "order-8821-shipped-notify",
"state": "simulated",
"delivery": "simulated",
"retrySend": false
},
"mode": "sandbox",
"replayed": false
}
Re-polling the same receipt returns replayed: true. Receipt states and their meanings:
| State | Meaning |
|---|---|
submitted | Bridge accepted; delivery in progress |
delivered | Confirmed delivered to recipient's device |
read | Recipient opened the message |
failed | Send failed; same key returns the recorded failure, not a new attempt |
unknown | Outcome could not be determined |
simulated | Sandbox result; no real recipient contacted |
A submitted state is not proof of delivery. A failed receipt is preserved as-is; decide independently whether to initiate a fresh action under a new key. An unknown outcome means the system could not confirm whether the message was sent: hold the original key, wait, and re-poll. Never resend an unknown outcome under a new key.
An AI agent uses the same REST endpoints with a stored bearer token. Surface context with GET /messages/search, produce a reply, and submit with POST /messages/send. Idempotency rules apply—unknown outcomes must not trigger a blind retry. Confirm the chatGuid is in the granted conversation set before sending.
To use these operations in an MCP-compatible workflow, build a local adapter over the Figment REST API; there is no published remote MCP endpoint.
Apple's forwarding guide explains that Messages in iCloud includes SMS, MMS, RCS, and iMessage—forwarding is built in when Messages in iCloud is enabled on iPhone. Previously synced carrier history remains available on the Mac when the iPhone is offline; phone availability affects only new carrier messages sent or received. If your use case covers mixed threads, confirm the iPhone is set up with Messages in iCloud before building the read path.
Sandbox access is free. Developer-paid live connections cost $29 per month per active Apple connection. The initial checkout covers one connection; access starts after setup. Mac rental is a separate product line.
Start in sandbox first at figment.so/apple-connect/developers. Set up a live connection when you are ready to work with your own messages.
Manage an existing grant at figment.so/apple-connect/account.