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

# Delivery, Retries and Idempotency

> How Fanvue retries failed webhook deliveries, when an endpoint is auto-disabled, and how to deduplicate events safely.

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.

<Warning>
  The event id is unique per event on every topic **except read receipts**
  (`message.read` / `creator.message.read`). Read [Deduplicating
  events](#deduplicating-events) before you key on it there.
</Warning>

## Delivery guarantees

| Guarantee | Behaviour                                                                                                      |
| --------- | -------------------------------------------------------------------------------------------------------------- |
| Delivery  | At least once. Duplicates are possible and expected                                                            |
| Ordering  | Not guaranteed. Do not assume two related events arrive in causal order                                        |
| Transport | HTTP `POST`, `Content-Type: application/json`                                                                  |
| Success   | Any `2xx` response. Anything else, or a timeout, counts as a failure and is retried                            |
| Signature | Every attempt carries `X-Fanvue-Signature` — see [Verify Webhook Signatures](/docs/webhooks/signature-verification) |

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

| Setting                         | Value      |
| ------------------------------- | ---------- |
| Request timeout                 | 10 seconds |
| Retries after the first attempt | 5          |
| Interval between attempts       | 30 seconds |

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.

<Note>
  Every attempt for the same event carries the **same** event id, so retries are
  safe to collapse. See [Deduplicating events](#deduplicating-events).
</Note>

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

<Tip>
  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.
</Tip>

## Deduplicating events

Every delivery carries an event id:

* On the [Standard-Webhooks envelope](/docs/creator/overview#event-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](/docs/webhooks/webhooks-overview) 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:

```ts theme={null}
const idempotencyKey = event.id ?? event.eventId;

if (await seen(idempotencyKey)) {
  return res.sendStatus(200); // already handled, drop it
}
await persist(idempotencyKey, event);
res.sendStatus(200);
```

### 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:

```ts theme={null}
// Read receipts only.
const idempotencyKey = `${event.id}:${event.timestamp}`;
```

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.

<Note>
  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.
</Note>

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