> ## Documentation Index
> Fetch the complete documentation index at: https://api.fanvue.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Efficient Chat Sync

> Keep chats in sync without burning your rate limit: receive messages in real time with webhooks, and pull deltas in bulk instead of re-polling.

If your integration keeps a local copy of chats in sync, the way you fetch new messages is the single biggest driver of your request volume. This guide shows the efficient pattern: **receive messages in real time with webhooks**, and when you do need to pull, **pull only what changed, in bulk**.

## The polling anti-pattern

The most common cause of throttling is re-polling every chat for new messages on a fixed interval:

```text theme={null}
every ~45s:
  for each creator:
    for each chat:
      GET /chats/{userUuid}/messages   # almost always returns nothing new
```

This scales with `creators × chats × poll frequency`, and the overwhelming majority of those requests return no new data. It exhausts your [rate-limit budget](/docs/authentication/rate-limits) on wasted calls and is the first thing to fix when you see sustained 429s.

There are two cheaper paths, and you should prefer them in this order.

## Primary: receive messages in real time with webhooks

Instead of asking "anything new?" on a loop, let Fanvue tell you. Register a webhook endpoint and subscribe to the **`message.received`** event (and **`message.read`** if you track read state). Fanvue then POSTs each new message to your endpoint as it happens — with **zero steady-state request cost**.

<Steps>
  <Step title="Enable the read:chat scope">
    Both `message.received` and `message.read` require the `read:chat` scope. Enable it in your app's **Authentication** tab. Users who connected before you added the scope must re-authorize. See [Scopes](/docs/authentication/scopes).
  </Step>

  <Step title="Register your endpoint">
    In the Developer Area, open your app's **Events** tab, add your HTTPS endpoint URL, and select `message.received` (and `message.read`). See [Webhooks Overview](/docs/webhooks/webhooks-overview) for the full setup walkthrough.
  </Step>

  <Step title="Verify every signature">
    Each delivery carries an `X-Fanvue-Signature` header. Verify it on every request so forged or replayed events are rejected. See [Verify Webhook Signatures](/docs/webhooks/signature-verification).
  </Step>

  <Step title="Persist and 2xx fast">
    Acknowledge with a 2xx as soon as you have persisted the event. Fanvue retries failed deliveries, so do slow processing asynchronously.
  </Step>
</Steps>

The `message.received` payload contains the message, sender, and recipient, so for most surfaces you never need to call the API back. See the [Message Received](/docs/webhooks/message-received) event reference for the exact payload shape.

<Tip>Webhooks replace polling for the steady state. You still need a pull path for the initial backfill and for catching up after downtime — that's what the next section covers.</Tip>

## Secondary: pull deltas in bulk, not the world

When you do pull — initial sync, or catching up after your endpoint was unreachable — use the bulk endpoint **`POST /chats/messages/batch`** instead of looping `GET /chats/{userUuid}/messages` per chat. It fetches up to **50 chats in a single request** and supports an incremental cursor.

| Field              | Meaning                                                                                                        |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `chatUuids`        | Array of 1–50 chat UUIDs (the counterpart user UUIDs) to fetch.                                                |
| `sinceMessageUuid` | Optional cursor. When set, returns only messages **strictly after** that message's publish date, in each chat. |
| `limit`            | Max messages per chat, 1–50 (default 20).                                                                      |

The response is keyed by chat UUID under `byChat`. Each entry is either a success (`messages`, plus `hasMore` and `oldestMessageUuid` for paging) or a per-key error (`{ "error": "not_found" }` for a chat that doesn't exist or that you aren't part of). **A single unreachable chat never fails the whole request** — you keep the rest and retry only the failing keys.

### Two modes

* **Default (no cursor):** returns the latest `limit` messages per chat, newest first. `hasMore` tells you older messages exist. Use this for the **initial backfill**.
* **Incremental (`sinceMessageUuid`):** returns only messages newer than the cursor. Use this to **catch up** — store the newest message UUID you've seen per chat (or a shared high-water mark) and pass it back on the next walk.

```bash Incremental delta walk theme={null}
curl -X POST https://api.fanvue.com/chats/messages/batch \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Fanvue-API-Version: 2025-06-26" \
  -H "Content-Type: application/json" \
  -d '{
    "chatUuids": ["<chat-1>", "<chat-2>", "<chat-3>"],
    "sinceMessageUuid": "<last-seen-message-uuid>",
    "limit": 50
  }'
```

<Note>Backward history pagination (a `before` cursor) is not yet available. For now, page the initial backfill with `limit`/`hasMore` and use `oldestMessageUuid` as the anchor.</Note>

For the per-chat endpoint, `GET /chats/{userUuid}/messages` pages with `page` (default 1) and `size` (default 15, max 50). Prefer the batch endpoint whenever you're syncing more than one chat.

## Recommended pattern

<Steps>
  <Step title="Backfill once">
    On first connect, call `POST /chats/messages/batch` in pages of up to 50 chats with no cursor to seed your local store.
  </Step>

  <Step title="Stay live with webhooks">
    Subscribe to `message.received` / `message.read`. This is your steady state — no polling.
  </Step>

  <Step title="Reconcile after gaps">
    If your endpoint was down or you suspect a missed delivery, run one incremental `batch` walk with `sinceMessageUuid` set to your last-seen message to close the gap. Then return to webhooks.
  </Step>
</Steps>

## Capacity planning

A worked example for an agency at scale. Suppose 100 creators, \~50 active chats each:

* **Polling every 45s:** `100 × 50` reads every 45s ≈ **6,600 requests/minute** — far past any per-bucket budget, and almost all of it returns nothing.
* **Webhooks + reconciliation:** **0 requests/minute** in steady state. Catch-up walks batch 50 chats per request, so reconciling all 5,000 chats costs \~100 requests total, run only when needed.

Two facts make this scale cleanly (both from the [rate-limit keying model](/docs/authentication/rate-limits#how-the-bucket-is-keyed)):

* **Creator-scoped routes get a bucket per creator.** Per-creator work — including the creator-scoped `POST /creators/{creatorUserUuid}/chats/messages/batch` — draws from its own 200/60s budget, so throughput scales with your creator count rather than contending for one bucket.
* **Aggregate `/agencies/*` routes share one bucket.** Reserve the shared `clientId:userUuid` bucket for genuinely cross-creator queries, not per-message sync.

<Tip>If you're still seeing 429s after moving to webhooks, you're likely polling somewhere else (online-status checks, insights refreshes). Audit which `clientId:userUuid` keys are spending budget and apply the same delta + bulk principles. See [Rate Limits](/docs/authentication/rate-limits).</Tip>
