# iMessage API for Search, Replies, and Agent Actions

Canonical: https://figment.so/imessage-api

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`.

## Why a Mac Is Required

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](https://figment.so/apple-connect/account) to monitor and manage connection setup.

## Register and Connect

Register your application at [figment.so/apple-connect/developers](https://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:

```http
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`.

## Reading Conversations

Both history and search require `messages:read`. Confirm the connection is `ready` before issuing either.

**Check connection status:**

```http
GET /v1/connections/{connection_id}
Authorization: Bearer ACCESS_TOKEN
```

```json
{
  "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:**

```http
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:

```http
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.

## Searching Message History

Full-text search runs against the locally available message store and shares `messages:read`—there is no separate search scope:

```http
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 Send Contract: Idempotency and Delivery States

The contract is two steps: submit the message, then poll the receipt with the same key.

**Step 1 — Send:**

```http
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:**

```http
GET /v1/connections/{connection_id}/messages/receipt
Authorization: Bearer ACCESS_TOKEN
Idempotency-Key: order-8821-shipped-notify
```

Fresh send result (sandbox):

```json
{
  "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.**

## Agents and Automation

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.

## SMS, MMS, and RCS

[Apple's forwarding guide](https://support.apple.com/en-us/102545) 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.

## Pricing

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](https://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](https://figment.so/apple-connect/account).
