Skip to main content
Fanvue delivers webhooks at least once. Your endpoint will occasionally see the same event twice, and events are not guaranteed to arrive in the order they occurred. This page covers what the delivery pipeline guarantees, how retries work, and how to deduplicate correctly.
The event id is unique per event on every topic except read receipts (message.read / creator.message.read). Read Deduplicating events before you key on it there.

Delivery guarantees

Return a 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

An attempt that returns a non-2xx status, or that does not respond within 10 seconds, is retried up to 5 times at 30-second intervals. After the last retry the event is not delivered again — there is no manual replay, so treat a sustained failure as data loss and reconcile from the API.
Every attempt for the same event carries the same event id, so retries are safe to collapse. See Deduplicating events.

Endpoint auto-disable

After 20 consecutive failed deliveries, the destination is automatically disabled and stops receiving events. This protects both sides from indefinite retry traffic against a dead endpoint. The counter is consecutive: a single successful delivery resets it. Re-enable a disabled webhook from the Events & Endpoints list in the Developer Area, or by re-subscribing via the API. Events that occurred while the endpoint was disabled are not backfilled — reconcile the gap from the relevant API endpoints.
Return a 2xx for events you do not recognise rather than a 4xx. Rejecting unknown event types counts as a failure and can walk an otherwise healthy endpoint towards auto-disable.

Deduplicating events

Every delivery carries an event id:
  • On the Standard-Webhooks envelope events (creator.*, checkout_link.*, app.*, payout.*) it is the top-level id, also sent as the webhook-id header.
  • On the legacy flat events it is the eventId field appended to the payload body.
The id is a deterministic hash of the event’s identifying inputs, and those inputs include something that identifies the individual occurrence — the invoice number for a charge, the message uuid for a message, the follow timestamp for a follow. Two properties follow from that, and you want both:
  • Every delivery attempt for the same event carries the same id, so collapsing on it removes retries.
  • Two genuinely distinct events carry different ids, even when the same fan and creator are involved, so collapsing on it does not remove real events.
On all but the topics called out below, the id alone is a valid idempotency key:

Read receipts

On message.read and creator.message.read the id is derived from the creator and the fan only. Every read receipt in a conversation therefore carries one id indefinitely, and a consumer keying on it records the first receipt and discards every later one. A read receipt reports a count for a conversation rather than a specific message, so the payload carries no occurrence identifier to key on. Use the timestamp as the occurrence component instead:
The timestamp is set once when the event is emitted, not per attempt, so it stays constant across retries while differing between two genuine receipts.
This is a known gap rather than the intended contract, and read receipts are expected to gain an occurrence component in the id. id + timestamp keeps working either way, which is why it is the recommendation here.

Repeated subscription state transitions

Subscription lifecycle events (creator.subscription.*, app.subscription.*) key on the state transition rather than the occurrence. A cancel and a later reactivation differ, but the same transition repeated inside one billing period — cancel, reactivate, cancel again — reuses the earlier id, so the second cancel is dropped and your copy of the subscription is left showing auto-renew on. If a subscription can change state more than once within a period, pair the id with the timestamp on these topics as well, or read the current state back from the API rather than tracking it from the event stream alone.