# Search Message History Without Missing Results

Canonical: https://figment.so/blog/imessage-search-pagination

A search that returns 12 results when the inbox holds 400 is not a bug you can retry your way out of. Two independent mechanisms produce that gap, and each one needs a different response.

The first is **sync lag**: the connected Mac has not yet received every message from iCloud, so queries run against a partial local copy. The second is a **local history bound**: search scans only the messages available on that Mac. No amount of offset-incrementing surfaces messages that were never synced. Address both before trusting your result count.

## Why searches return incomplete results

iMessage reads and searches operate on messages available on the connected Mac ([OAuth for iCloud](/oauth-for-icloud)). Apple's Messages in iCloud syncs your message history — including SMS, MMS, RCS, and iMessage — across signed-in devices, but that transfer takes time ([Apple Support](https://support.apple.com/en-us/102545)). While a sync is in progress, a search returns only what has arrived locally.

Even after sync completes, results are limited to what the Mac holds. If messages were never downloaded to this Mac, they will not appear. Both causes produce the same surface symptom: a result set smaller than expected, with no error code to distinguish them.

## Check service state before you query

The connection endpoint shows the current state of each service:

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

The response includes `connection.services.messages.state`:

```json
{
  "connection": {
    "id": "con_abc123",
    "mode": "live",
    "services": {
      "messages": {
        "state": "ready",
        "observedAt": 1731388800000
      }
    }
  }
}
```

Possible states: `connecting`, `needs_approval`, `syncing`, `ready`, `offline`, `reconnect_required`, `revoked`. Read operations fail on non-ready connections; check this state before issuing any search.

`syncing` is not an error. A fresh connection passes through it while history downloads. Poll until `ready` before querying, or surface a "history loading" caveat to your user. `needs_approval` and `reconnect_required` require the account holder to act in the [account connection screen](/apple-connect/account); your application cannot resolve them.

## Search parameters

```http
GET /v1/connections/{connection_id}/messages/search
  ?query=contract%20renewal
  &chatGuid={ENCODED_CHAT_GUID}
  &limit=50
  &offset=0
Authorization: Bearer ACCESS_TOKEN
```

| Parameter | Description |
|---|---|
| `query` | Search term applied to message text |
| `chatGuid` | Scope search to one conversation; required for a selected-conversation grant |
| `limit` | Results per page, 1–100 (default 20) |
| `offset` | Results to skip; maximum 10 000 |

Always build the URL with a URL encoder. `chatGuid` values come from `GET /v1/connections/{connection_id}/chats` — do not construct them from phone numbers. chatGuid values contain literal `+` characters; a raw `+` in a query string decodes as a space and silently breaks lookups.

Offset is capped at 10 000. If a search would yield more than that many matching results, narrow the query or scope it with `chatGuid`.

## Paginate with `hasMore` and `nextOffset`

Each search response carries pagination fields alongside a `historyComplete` flag:

```json
{
  "data": [...],
  "mode": "live",
  "offset": 0,
  "nextOffset": 50,
  "hasMore": true,
  "historyComplete": true,
  "scope": "messages:read"
}
```

`hasMore: true` means additional results exist. `nextOffset` is the value to pass as `offset` on the next request. When `hasMore` is `false` or `nextOffset` is `null`, you have reached the end of the available result set. An empty `data` array is valid and not an error.

`historyComplete: false` means the Mac's sync is still in progress. Results are a snapshot of partial history. Surface this to users rather than presenting the result set as exhaustive.

New messages arriving during a paginated search can shift the result set. Do not assume pages form a stable real-time snapshot.

Here is the pagination model in Python pseudocode:

```python
import urllib.parse

def search_messages(connection_id, token, query, chat_guid=None):
    base = "https://figment.so/api/apple-connect/v1"
    params = {"query": query, "limit": 50, "offset": 0}
    if chat_guid:
        params["chatGuid"] = chat_guid

    results = []
    history_complete = False

    while True:
        url = (
            f"{base}/connections/{connection_id}/messages/search"
            f"?{urllib.parse.urlencode(params)}"
        )
        resp = get(url, headers={"Authorization": f"Bearer {token}"})
        # get() is your HTTP client call; raise on non-2xx before this point

        results.extend(resp["data"])
        history_complete = resp.get("historyComplete", False)

        if not resp.get("hasMore") or resp.get("nextOffset") is None:
            break

        next_offset = resp["nextOffset"]
        if next_offset <= params["offset"]:
            break  # non-advancing offset guard
        params["offset"] = next_offset

    return results, history_complete
```

Pass a `chat_guid` from the chats endpoint when querying a specific conversation. Check `history_complete` on return and surface a warning to your user when it is `False`.

## SMS, MMS, and RCS

iMessage and carrier messages share the same search endpoint. SMS, MMS, and RCS require an eligible iPhone that is turned on, online, and signed into the same Apple Account ([Apple Support](https://support.apple.com/en-us/102545)). With Messages in iCloud enabled, carrier message sync is built in and does not require a separate Text Message Forwarding switch.

If the iPhone is offline, existing carrier-message history can remain on the Mac but new messages will not arrive. A `ready` service state reflects the iMessage connector, not a guarantee that carrier-message history is current.

## Get started

Reliable search starts with a connected, synced Apple Account. Figment handles iCloud sign-in, trusted-device approval, and permission grant. Your app receives a scoped access token — never the Apple password.

For the full API reference see [iMessage API for Search, Replies, and Agent Actions](/imessage-api). For a Python walkthrough of reading and filtering messages, see [Read and Search iMessages with Python](/blog/read-imessages-python). For the send path and idempotency model, see [Send an iMessage from Node.js Without Duplicates](/blog/send-imessage-nodejs).

**[Connect Apple Account](/apple-connect/developers)** to try the search and pagination endpoints.
