Skip to main content
Fanvue delivers every webhook at least once and in no guaranteed order, so your handler has to tolerate duplicates and out-of-order arrivals. Read this before you write the code that stores events: it tells you what Fanvue promises, what happens when your endpoint fails, and which key to deduplicate on for each topic. You need a receiver that already verifies signatures. Everything here applies after a delivery has passed that check.

Delivery guarantees

Return 2xx as soon as you have persisted the event, then process it asynchronously. Doing the work before you respond risks a timeout, which produces a retry and therefore a duplicate.

Retries

Fanvue retries failed deliveries with backoff. Every attempt for one event carries the same id, so collapsing on it removes retries. Once the retries are exhausted the event is not delivered again, and there is no manual replay. Treat a sustained failure as data loss and reconcile from the API. Fanvue also drops a duplicate id emitted within a short window before delivery, so a re-emit of the same event from Fanvue’s side normally reaches you once.

Endpoint auto-disable

A destination that keeps failing is disabled and stops receiving events. The app owner is notified in the Developer Area. Re-enable the destination with its toggle in the Events & endpoints list. Subscriptions created with POST /webhooks/subscriptions receive no notification, and GET /webhooks/subscriptions has no enabled flag. Detect silence yourself, then delete the subscription and create a new one. Events that occurred while a destination was disabled are not backfilled. Reconcile the gap from the relevant API endpoints.
Return 2xx for events you don’t recognise rather than 4xx. Rejecting unknown event types counts as a failure and walks an otherwise healthy endpoint towards auto-disable.

Deduplicating events

Every delivery carries an event id:
  • On the envelope events (creator.*, checkout_link.*, payout.*, app.*) it is the top-level id.
  • On the legacy flat events it is the eventId field appended to the body.
The id is a deterministic hash of the event’s identifying inputs. Those inputs include something that identifies the occurrence, such as the invoice number for a charge, the message uuid for a message or the read time for a receipt. Two properties follow:
  • Every delivery attempt for the same event carries the same id, so collapsing on it removes retries.
  • Two distinct events carry different ids, even for the same fan and creator, so collapsing on it does not remove real events.
On every topic except the two called out in Read receipts and Repeated subscription state transitions, the id alone is a valid idempotency key:

Read receipts

A read receipt reports a count for a conversation rather than a specific message, so the read topics key their ids differently.
Legacy message.read reuses one eventId for every read receipt in a conversation, so keying on it alone keeps only the first receipt and drops the rest. creator.message.read and creator.message.read_by_fan are not affected.
The legacy eventId is derived from the creator, the fan and the event type only, so it stays the same for a conversation indefinitely. Pair it with timestamp as the occurrence component:
creator.message.read keys on the creator, the fan and the time the read was handled, so every occurrence has its own id and a retry of the same occurrence keeps it. The id alone is a valid key. creator.message.read_by_fan keys on the creator, the fan and the newest message in the conversation. A fan cannot mark a conversation unread, so consecutive receipts without a new message in between don’t occur, and the id alone is a valid key. See Read-by-fan receipt resource. creator.chat.marked_unread keys on marked_at, so every mark is a distinct id; see Deduplicating a mark.

Repeated subscription state transitions

app.subscription.* ids hash the subscription id and the topic, so the same transition repeated for one subscription reuses the earlier id. A cancel, a reactivation and a second cancel produce two app.subscription.cancel_at_period_end_changed events with one id, and the second is dropped. Pair the id with timestamp on these topics, or read the current state back from GET /apps/{appUuid}/subscription-status rather than tracking it from the event stream alone. creator.subscription.* and creator.experience_subscription.* fold the time of the transition into the id, so a lapse and a later resubscription carry distinct ids and the id alone is a valid key.