
iMessage threads carry participant handles, conversation text, and message direction — the same fields a CRM needs to log a client interaction. With a connected Mac and the messages:read scope, your app can fetch selected threads through the Figment API, map their fields to your CRM's schema, and hold the result in a staging queue until a person or rules engine approves each record before it touches your contact data.
This guide covers the data model, a concrete field-mapping table, and the review-before-write workflow. For connection and consent setup, see OAuth for iCloud: Connect Apple Data to Your App.
Design guide: The examples below are illustrative and have not been executed against a live account. Verify field names and response shapes against a live connection before writing any records to production.
The API exposes two resource types: chats (conversation threads) and messages (individual events within a thread).
Chat items include:
guid — a stable thread identifier. One-on-one threads use iMessage;-;+15551234567; group threads use iMessage;+;<uuid>.displayName — the group thread name when set; absent on most 1:1 threads.Message items include:
guid — stable message identifiertext — plain-text bodyisFromMe — true for sent messages, false for receivedchatGuid — the parent thread's guidTwo things to check before a full import. First, history on a freshly connected Mac may be incomplete while sync is running — check the historyComplete flag in every list response before treating a pull as a complete record. Second, non-ready connections reject API operations; use the account screen to confirm service status before fetching.
This table is the core artifact of an iMessage-CRM workflow. Adapt field names to your CRM; the transformations apply regardless of whether you target Salesforce, HubSpot, or a custom system.
| iMessage field | API location | CRM field | Transformation |
|---|---|---|---|
| Thread guid | chats[].guid | Contact lookup key | For 1:1 threads, parse the handle from the guid suffix — iMessage;-;+15551234567 → +15551234567 — and normalize to E.164. Group guids contain a UUID; use displayName or a manual rule for those. |
displayName | chats[].displayName | Subject / deal hint | Useful for named group threads tied to a project or deal; absent on most 1:1 threads. |
text | messages[].text | Activity note body | Null-check; strip trailing whitespace; truncate to your CRM's character limit. |
isFromMe | messages[].isFromMe | Activity direction | true → outbound; false → inbound. |
chatGuid | messages[].chatGuid | Thread reference | Confirm the message's chatGuid matches the thread being processed when you merge across fetches. |
The API does not document a timestamp field in message items. If your live connection returns additional date fields, verify the field name and unit before writing any date value to production. Do not depend on undocumented fields in a production pipeline.
Writing directly from API output to a CRM introduces two predictable failures: a handle matches more than one contact record, or a thread you didn't intend to sync gets pulled in. A staging step catches both before they create data problems.
Five-stage pattern:
chatGuid values and transform fields using the table above.approved or rejected with a reason code.The review step is exactly where you catch the ambiguous case: +15551234567 resolves to two contacts in your CRM. Without a staging queue, you write to whichever record appears first. With it, a person makes the call and the reason is logged.
If your workflow eventually adds outbound iMessages, the same staged, reviewed payload also works with the idempotency key pattern required by the send endpoint — preventing duplicates when a record is committed more than once.
The messages:read scope covers listing chats, fetching message bodies, and searching by keyword — no separate scope is needed for search.
import os
import requests
BASE_URL = "https://figment.so/api/apple-connect"
TOKEN = os.environ["FIGMENT_ACCESS_TOKEN"] # opaque Figment OAuth token
CONNECTION_ID = os.environ["FIGMENT_CONNECTION_ID"] # e.g. con_abc123
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# Step 1 — list recent chats; follow hasMore / nextOffset for full pagination
resp = requests.get(
f"{BASE_URL}/v1/connections/{CONNECTION_ID}/chats",
params={"limit": 20, "offset": 0},
headers=HEADERS,
)
resp.raise_for_status()
body = resp.json()
chats = body["data"] # may be empty; check body["hasMore"] and body["nextOffset"]
# Step 2 — keep only threads whose 1:1 handle is tracked in your CRM
CRM_HANDLES = {"+15551234567", "+15559876543"}
def handle_from_guid(guid: str) -> str | None:
"""Return the E.164 handle for a 1:1 thread guid, or None for group threads."""
parts = guid.split(";")
if len(parts) == 3 and parts[1] == "-":
return parts[2]
return None # group thread — requires a separate association strategy
selected = [c for c in chats if handle_from_guid(c["guid"]) in CRM_HANDLES]
# Step 3 — fetch messages per thread and build a staging list
STAGED = []
for chat in selected:
chat_guid = chat["guid"]
offset = 0
while True:
msg_resp = requests.get(
f"{BASE_URL}/v1/connections/{CONNECTION_ID}/messages",
params={"chatGuid": chat_guid, "limit": 50, "offset": offset},
headers=HEADERS,
)
msg_resp.raise_for_status()
msg_body = msg_resp.json()
for m in msg_body["data"]:
STAGED.append({
"lookup_handle": handle_from_guid(chat_guid),
"direction": "outbound" if m["isFromMe"] else "inbound",
"body": (m.get("text") or "").strip(),
"source_guid": m["guid"],
"_status": "pending",
})
if not msg_body.get("hasMore"):
break
offset = msg_body["nextOffset"]
For keyword-filtered activity — every thread that mentions a contract number, say — replace the per-chat fetch with the search endpoint. Use a URL builder to encode query values; a raw + in a query string decodes as a space, and chatGuid values contain semicolons and plus signs that must be percent-encoded:
GET /v1/connections/{connection_id}/messages/search
?query=renewal
&chatGuid=iMessage%3B-%3B%2B15551234567
&limit=20
For a deeper tutorial covering reads, pagination, and threading, see Read and Search iMessages with Python.
Use the service and conversation identifiers returned by your connector to classify messages. Per Apple's forwarding setup guide, Messages in iCloud syncs SMS, MMS, RCS, and iMessage history across all signed-in devices. When Messages in iCloud is active, forwarding is built in and a separate Text Message Forwarding switch is not required. The iPhone must be on, connected to a network, signed into the same Apple Account, and able to send and receive messages.
The same field-mapping table applies once SMS threads appear in the chat list: guid extraction, E.164 normalization, and direction mapping work identically across prefix types.
For the full scope of messages:read and messages:send — including agent-driven replies and search — see iMessage API for Search, Replies, and Agent Actions. For the idempotency key pattern that prevents duplicate sends when a staged record is committed more than once, see Send an iMessage from Node.js Without Duplicates.
Connect your Apple Account to start pulling selected threads into your CRM pipeline.