Skip to main content
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:
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 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.
1

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

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 for the full setup walkthrough.
3

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

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.
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 event reference for the exact payload shape.
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.

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.
FieldMeaning
chatUuidsArray of 1–50 chat UUIDs (the counterpart user UUIDs) to fetch.
sinceMessageUuidOptional cursor. When set, returns only messages strictly after that message’s publish date, in each chat.
limitMax 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.
Incremental delta walk
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
  }'
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.
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.
1

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

Stay live with webhooks

Subscribe to message.received / message.read. This is your steady state — no polling.
3

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.

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):
  • 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.
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.