# Turn Selected iMessage Threads into CRM Updates

Canonical: https://figment.so/blog/imessage-crm-sync

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](/oauth-for-icloud).

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

## What the API returns

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 identifier
- `text` — plain-text body
- `isFromMe` — `true` for sent messages, `false` for received
- `chatGuid` — the parent thread's guid

**Two 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](/apple-connect/account) to confirm service status before fetching.

## Map iMessage fields to your CRM schema

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.

## Build the review-before-write workflow

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

1. **Fetch and map** — pull the selected `chatGuid` values and transform fields using the table above.
2. **Write to a staging store** — append mapped records to a pending queue: a database table, a spreadsheet, or a lightweight list you control.
3. **Surface for review** — display each staged record with the proposed CRM contact match and the note body.
4. **Approve or discard** — a person or a rules engine marks each record `approved` or `rejected` with a reason code.
5. **Commit approved records** — POST to your CRM API; archive rejections with their reason for audit.

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.

## Fetch and filter threads

The `messages:read` scope covers listing chats, fetching message bodies, and searching by keyword — no separate scope is needed for search.

```python
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](/blog/read-imessages-python).

## SMS and RCS threads

Use the service and conversation identifiers returned by your connector to classify messages. Per [Apple's forwarding setup guide](https://support.apple.com/en-us/102545), 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](/imessage-api). 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](/blog/send-imessage-nodejs).

**[Connect your Apple Account](/apple-connect/developers)** to start pulling selected threads into your CRM pipeline.
