
Your Python code can list conversations, walk message history, and run full-text searches over iMessage — all through the Figment Apple Connect REST API. Standard HTTP, the requests library, a bearer token: the developer app receives a scoped access token, never an Apple password, and your code runs on any platform.
The Messages app on the connected Mac is the read source. When Messages in iCloud is enabled, Apple syncs iMessage, SMS, MMS, and RCS history across devices configured for Messages in iCloud under the same Apple Account (Apple support HT102545). Without Messages in iCloud, Text Message Forwarding can be configured separately on the iPhone. The iPhone must be on and connected for new carrier traffic to arrive; previously synced carrier history can remain on the Mac while the phone is offline. The bridge reads the local database on the Mac; your server never touches the SQLite file directly.
A Mac that has just reconnected may still be downloading older history. The API exposes a per-service status — connecting, needs_approval, syncing, ready, offline, reconnect_required, or revoked — so you can guard against premature reads. Only a ready service accepts message operations; a non-ready service rejects them. Even a ready service may have incomplete history if the account was recently reconnected; check historyComplete in the list envelope rather than assuming full coverage.
Register your app at the developer dashboard, then walk the user through Figment's PKCE consent screen. This grants your app scoped access to an existing Apple connection — it is not the Apple sign-in step itself, which the account owner completed during connection setup.
Generate a fresh code verifier and challenge for every authorization request. Send the user to:
GET https://figment.so/apple-connect/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.example/callback
&scope=messages:read
&state=RANDOM_OPAQUE_VALUE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
Validate the returned state before exchanging the code, then POST to the token endpoint:
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%2Fyourapp.example%2Fcallback&code=RETURNED_CODE&code_verifier=YOUR_CODE_VERIFIER
The response includes access_token, refresh_token, expires_in (900 seconds for the access token), scope, and connection_id. Store the connection_id — a con_… value — alongside both tokens. To refresh, POST grant_type=refresh_token, client_id, and refresh_token as a serialized application/x-www-form-urlencoded body to the same token endpoint — one refresh at a time, per grant; always persist the new refresh_token from each response, as tokens rotate on use.
Export the credentials and install the HTTP client:
export FIGMENT_ACCESS_TOKEN=ACCESS_TOKEN
export FIGMENT_CONNECTION_ID=con_01abc...
pip install requests
Manage existing grants at figment.so/apple-connect/account.
Call GET /v1/connections/{connection_id} to confirm the service is ready before reading. This endpoint returns {connection:{id,mode,services,...}} — a different shape from the list endpoints — and can return a non-2xx error if the connection is unavailable or revoked. Handle that before proceeding:
import os, sys, requests
BASE_URL = "https://figment.so/api/apple-connect"
ACCESS_TOKEN = os.environ["FIGMENT_ACCESS_TOKEN"]
CONNECTION_ID = os.environ["FIGMENT_CONNECTION_ID"]
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
def get_connection():
r = requests.get(
f"{BASE_URL}/v1/connections/{CONNECTION_ID}",
headers=HEADERS, timeout=10,
)
r.raise_for_status()
return r.json()["connection"]
def list_chats(limit=20, offset=0):
r = requests.get(
f"{BASE_URL}/v1/connections/{CONNECTION_ID}/chats",
headers=HEADERS,
params={"limit": limit, "offset": offset},
timeout=10,
)
r.raise_for_status()
return r.json()
conn = get_connection()
# Each service entry is an object: {state, reason, observedAt}.
# Only granted services appear in the services map.
messages_svc = conn.get("services", {}).get("messages", {})
state = messages_svc.get("state")
if state != "ready":
sys.exit(
f"Messages service not ready (state={state!r}). "
"Check https://figment.so/apple-connect/account"
)
page = list_chats(limit=20)
chats = page["data"]
if not chats:
sys.exit("No chats returned.")
print(f"Mode: {page['mode']} — {len(chats)} chat(s) on this page")
The mode field in every list response is "live" or "sandbox". Sandbox responses contain only synthetic data; nothing reaches real recipients.
Use each chat item's guid as the chatGuid parameter to the messages endpoint. Drive the loop with hasMore and nextOffset — not a length comparison — and stop before the documented offset limit:
def list_messages(chat_guid, limit=20, offset=0):
r = requests.get(
f"{BASE_URL}/v1/connections/{CONNECTION_ID}/messages",
headers=HEADERS,
params={"chatGuid": chat_guid, "limit": limit, "offset": offset},
timeout=15,
)
r.raise_for_status()
return r.json()
MAX_OFFSET = 10000 # inclusive public offset limit; nextOffset > 10000 must stop
first_guid = chats[0]["guid"] # each chat item's guid
all_messages, offset, stop_reason = [], 0, "hasMore=False"
while True:
envelope = list_messages(first_guid, limit=20, offset=offset)
all_messages.extend(envelope["data"])
if not envelope.get("hasMore"):
stop_reason = "hasMore=False"
break
next_offset = envelope.get("nextOffset")
if next_offset is None:
stop_reason = "nextOffset=None"
break
if next_offset <= offset:
stop_reason = f"non-advancing nextOffset ({next_offset})"
break
if next_offset > MAX_OFFSET:
stop_reason = f"nextOffset {next_offset} exceeds public limit"
break
offset = next_offset
print(f"Loaded {len(all_messages)} message(s). Stopped: {stop_reason}.")
# historyComplete in the last envelope reflects what the Mac's local store covered.
# It does not prove all iCloud history is present locally.
The requests library encodes the params dict automatically — never build the query string by hand with values containing colons or plus characters. Message items include guid, text, isFromMe, and chatGuid; displayName on a chat item may be absent. Empty data arrays are valid responses.
History reads and full-text searches share the single messages:read scope — there is no separate search permission. Supply query; optionally scope results to one conversation with chatGuid. With a selected-conversation grant, chatGuid is required:
def search_messages(query, chat_guid=None, limit=20, offset=0):
params = {"query": query, "limit": limit, "offset": offset}
if chat_guid:
params["chatGuid"] = chat_guid
r = requests.get(
f"{BASE_URL}/v1/connections/{CONNECTION_ID}/messages/search",
headers=HEADERS,
params=params,
timeout=20,
)
r.raise_for_status()
return r.json()
results = search_messages("invoice", chat_guid=first_guid, limit=20)
print(f"Found {len(results['data'])} result(s). hasMore: {results.get('hasMore')}")
Search scans the messages the Mac has downloaded locally — it is not a guaranteed complete full-text index of all-time history. Pagination follows the same hasMore / nextOffset pattern and the same offset ≤ 10000 bound.
Non-ready services reject reads. The connection endpoint can reject a non-ready or revoked connection with a non-2xx error rather than returning a state object; handle that with raise_for_status as the code above does. If the service is syncing, offline, or reconnect_required, message and search calls will be rejected. For connection setup status, go to figment.so/apple-connect/account. Once the service is ready, check historyComplete in the list envelope — it reflects what the Mac's local store covered, not whether all iCloud history is present locally.
SMS, MMS, and RCS. When Messages in iCloud is enabled on the user's iPhone, carrier message types sync to the Mac automatically alongside iMessage (Apple HT102545). Without Messages in iCloud, Text Message Forwarding can be configured separately. The iPhone must be on and connected for new carrier traffic to arrive; previously synced carrier history can remain on the Mac while the phone is offline.
Reconnect interruptions. A reconnect_required state means account status needs attention; there is no single asserted cause. Direct users to figment.so/apple-connect/account rather than returning stale data as current.
Sending. messages:read covers both history reads and full-text search. Sending requires the separate messages:send scope and an Idempotency-Key header on every POST. A chat visible in read results is not automatically available for sends; the send grant must include that specific existing conversation.
Pricing. The developer pays $29/month per active live connection. Sandbox is free, and access starts after setup.
For app registration and the full OAuth reference, start at the developer dashboard.
Connect Apple Account to link your iMessage history to your Python app.