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

# Creator Experience Subscriptions

> Reference for the creator.experience_subscription.deactivated and activated webhooks: payload fields, end reasons, and gating fan-experience access.

Two events report whether a fan's paid **fan-experience subscription** currently grants access. They are the grant and revoke pair an app uses to open and close whatever the experience gates, a Discord or Telegram group for example.

| Event (`type`)                                | Name in Developer Area          | Fires when                                                                                                |
| --------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `creator.experience_subscription.deactivated` | Experience Subscription Ended   | The subscription stops granting access, for any reason                                                    |
| `creator.experience_subscription.activated`   | Experience Subscription Started | The subscription starts granting access: a first purchase, or a failed renewal recovered by a later retry |

Both require the `read:experience` scope, not the `read:creator` scope the rest of the `creator.*` domain uses. Both are delivered in the [Standard-Webhooks envelope](/docs/creator/overview#event-envelope); the fields below describe the `data` object.

<Warning>
  **A fan-experience subscription is not a profile subscription.** They are separate resources and a fan can hold both against the same creator at the same time, plus one per experience. `creator.experience_subscription.deactivated` says a fan lost access to **one experience**. It says nothing about their profile subscription, and revoking profile-gated content on it removes access the fan still pays for. Profile subscriptions ride [`creator.subscription.*`](/docs/creator/subscriptions) with `data.object: "subscription"`; these events carry `data.object: "experience_subscription"`. Discriminate on `type` or on `data.object`, never on the word "subscription".
</Warning>

## Access ended, not billing changed

<Warning>
  **A cancellation is not an access-ended signal.** A fan turning auto-renew off keeps access until the period they paid for runs out. `creator.experience_subscription.deactivated` fires at that point, not at the moment they cancel.

  Key every revocation on `access_ends_at` and on the event arriving. Never key it on `cancel_at_period_end` being `true`, and never key it on `expires_at` alone, a refund, chargeback, ban or unpublish ends access mid-period and leaves `expires_at` in the future.

  Fan experiences have no intent-to-cancel event. [`creator.subscription.cancel_at_period_end_changed`](/docs/creator/subscriptions) covers auto-renew changes for **profile subscriptions only** and has no fan-experience equivalent.
</Warning>

## Delivery

These two topics do **not** use the `creator.*` delivery model. Other `creator.*` events fan out to every app holding the required scope for that creator. These reach only:

* the app that owns the experience, resolved from the subscription's own `app.uuid`, unconditionally, **and**
* the creator's connector destinations subscribed to the topic.

An experience subscription belongs to one app, so a creator-wide fan-out would tell app A which fans stopped paying app B. If your app does not own the experience, you do not receive the event, however many scopes you hold for that creator.

## Experience subscription resource

| Field                  | Type            | Description                                                                                                                                                    |
| ---------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `object`               | string          | Always `"experience_subscription"`                                                                                                                             |
| `id`                   | string          | The subscription's own uuid. Stable across renewals, and reused if the fan lapses and resubscribes to the same experience                                      |
| `status`               | string          | `expired` on `deactivated`, `active` on `activated`                                                                                                            |
| `end_reason`           | string          | **`deactivated` only.** Why access ended, see [End reasons](#end-reasons). Always present on this event                                                        |
| `access_ends_at`       | string          | **`deactivated` only.** ISO 8601 moment access was revoked. **This is the field to act on**, it is not always `expires_at`                                     |
| `activation_reason`    | string          | **`activated` only.** `subscribed` or `renewal_recovered`, see [Activation reasons](#activation-reasons)                                                       |
| `cancel_at_period_end` | boolean         | Whether auto-renew was already off. On `deactivated`, `true` means the fan had cancelled and this event is the end of the period they had paid for             |
| `cancelled_at`         | string \| null  | ISO 8601 time auto-renew was turned off. `null` when the fan never cancelled                                                                                   |
| `expires_at`           | string \| null  | ISO 8601 paid-through date of the last period the fan bought. Carried alongside `access_ends_at` so you can tell a mid-period revocation from a period-end one |
| `price`                | integer \| null | The price the fan subscribed at, in minor units. Frozen at subscribe time, so it is not the amount of any particular charge                                    |
| `created_at`           | string \| null  | ISO 8601 time the subscription was first created. Never advances on renewal                                                                                    |
| `experience`           | object          | `{ uuid }` of the experience                                                                                                                                   |
| `app`                  | object          | `{ uuid }` of the app that owns the experience                                                                                                                 |
| `fan`                  | object          | `{ uuid }` of the fan. Carries no handle, display name or email, these are grant and revoke signals for a fan the receiving app already knows by uuid          |
| `creator`              | object          | `{ uuid }` of the creator                                                                                                                                      |
| `metadata`             | object          | Always `{}` on these events                                                                                                                                    |

<Note>
  **`access_ends_at` and `expires_at` agree only on a period-end cancellation.** Every other `end_reason` revokes mid-period, leaving `expires_at` the later of the two. Compare them to tell the two cases apart: `access_ends_at < expires_at` means the fan lost time they had paid for.
</Note>

### End reasons

`end_reason` is a required discriminator on `creator.experience_subscription.deactivated`.

| `end_reason`                | Meaning                                                                                                                                                                    |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cancelled`                 | The fan turned auto-renew off and the period they had paid for has now ended                                                                                               |
| `payment_failed`            | The renewal charge was declined. A dunning retry may still restore access, see [Activation reasons](#activation-reasons)                                                   |
| `payment_method_invalid`    | No usable stored payment method to renew with: none on file, or Pix, which cannot recur                                                                                    |
| `refunded`                  | The charge was refunded, so the access it bought was revoked with it                                                                                                       |
| `chargeback`                | The charge was charged back, so the access it bought was revoked with it                                                                                                   |
| `experience_unavailable`    | The experience stopped being sold as a recurring paid experience: unpublished, no longer sold under the paid access mode, re-priced as one-off, or its app was uninstalled |
| `creator_earnings_disabled` | The creator's earnings were disabled, so Fanvue stopped billing for them                                                                                                   |
| `account_closed`            | The fan's or the creator's account was deleted or banned                                                                                                                   |
| `system_error`              | A failure on Fanvue's side left the fan charged but not entitled, so the charge was reversed                                                                               |

The payload carries no retry state. A `payment_failed` event tells you the charge that was due has been declined and access is gone as of `access_ends_at`, nothing more, any retry state would already be stale by the time you process it. Revoke on it and wait for `creator.experience_subscription.activated`.

### Activation reasons

`activation_reason` is a required discriminator on `creator.experience_subscription.activated`.

| `activation_reason` | Meaning                                                                                        |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `subscribed`        | The fan bought the subscription, access starts for the first time                              |
| `renewal_recovered` | A renewal charge that had previously failed was recovered by a dunning retry, restoring access |

<Warning>
  **Handling `renewal_recovered` is required if you revoke on `deactivated`.** A declined renewal revokes access and fires `deactivated` with `end_reason: "payment_failed"` **before** the dunning retries run. A retry that settles pays for the period and restores access. An app that revoked and never re-grants leaves a fan who has paid locked out permanently.
</Warning>

## Example: `creator.experience_subscription.deactivated`

A cancellation reaching the end of its paid period, the one case where `access_ends_at` and `expires_at` agree.

```json theme={null}
{
  "id": "9b3c1d20-4d5e-4f60-8a71-1c2d3e4f5a60",
  "type": "creator.experience_subscription.deactivated",
  "timestamp": "2026-07-09T08:39:33.310Z",
  "data": {
    "object": "experience_subscription",
    "id": "7f2a91c4-58d3-4b0e-9c11-6a4e8b2d0f37",
    "status": "expired",
    "end_reason": "cancelled",
    "access_ends_at": "2026-07-09T08:39:33.139Z",
    "cancel_at_period_end": true,
    "cancelled_at": "2026-06-21T14:02:10.000Z",
    "expires_at": "2026-07-09T08:39:33.139Z",
    "price": 999,
    "created_at": "2026-05-09T08:39:33.139Z",
    "experience": { "uuid": "3d8f0b52-77a1-4c9e-b6d4-2e5a1f9c8b03" },
    "app": { "uuid": "c41e7a68-9b25-4d13-8f7a-0b6c2d94e5f1" },
    "fan": { "uuid": "5939ae62-3eb5-4433-954f-f78c7b4282f3" },
    "creator": { "uuid": "f4b2a184-2b4a-4a04-9b70-3c0e0a1caa12" },
    "metadata": {}
  }
}
```

A mid-period revocation carries the same shape with `access_ends_at` before `expires_at`:

```json theme={null}
{
  "id": "9b3c1d20-4d5e-4f60-8a71-1c2d3e4f5a61",
  "type": "creator.experience_subscription.deactivated",
  "timestamp": "2026-06-24T11:07:02.480Z",
  "data": {
    "object": "experience_subscription",
    "id": "7f2a91c4-58d3-4b0e-9c11-6a4e8b2d0f37",
    "status": "expired",
    "end_reason": "chargeback",
    "access_ends_at": "2026-06-24T11:07:02.311Z",
    "cancel_at_period_end": false,
    "cancelled_at": null,
    "expires_at": "2026-07-09T08:39:33.139Z",
    "price": 999,
    "created_at": "2026-05-09T08:39:33.139Z",
    "experience": { "uuid": "3d8f0b52-77a1-4c9e-b6d4-2e5a1f9c8b03" },
    "app": { "uuid": "c41e7a68-9b25-4d13-8f7a-0b6c2d94e5f1" },
    "fan": { "uuid": "5939ae62-3eb5-4433-954f-f78c7b4282f3" },
    "creator": { "uuid": "f4b2a184-2b4a-4a04-9b70-3c0e0a1caa12" },
    "metadata": {}
  }
}
```

## Example: `creator.experience_subscription.activated`

The recovery case, the reason an app that revokes has to handle this topic.

```json theme={null}
{
  "id": "9b3c1d20-4d5e-4f60-8a71-1c2d3e4f5a62",
  "type": "creator.experience_subscription.activated",
  "timestamp": "2026-07-11T02:15:44.902Z",
  "data": {
    "object": "experience_subscription",
    "id": "7f2a91c4-58d3-4b0e-9c11-6a4e8b2d0f37",
    "status": "active",
    "activation_reason": "renewal_recovered",
    "cancel_at_period_end": false,
    "cancelled_at": null,
    "expires_at": "2026-08-09T08:39:33.139Z",
    "price": 999,
    "created_at": "2026-05-09T08:39:33.139Z",
    "experience": { "uuid": "3d8f0b52-77a1-4c9e-b6d4-2e5a1f9c8b03" },
    "app": { "uuid": "c41e7a68-9b25-4d13-8f7a-0b6c2d94e5f1" },
    "fan": { "uuid": "5939ae62-3eb5-4433-954f-f78c7b4282f3" },
    "creator": { "uuid": "f4b2a184-2b4a-4a04-9b70-3c0e0a1caa12" },
    "metadata": {}
  }
}
```

## Revoking access on subscription end

Gate the resource on one pair of handlers: revoke on `deactivated`, grant on `activated`. Verify the signature first, return `2xx` immediately, and do the revocation out of band, see [Delivery, Retries and Idempotency](/docs/webhooks/delivery-and-idempotency).

```ts theme={null}
// Fanvue delivers both topics to the same endpoint. Discriminate on `type`.
app.post("/fanvue/webhooks", async (req, res) => {
  const event = verifySignature(req); // see /webhooks/signature-verification
  res.sendStatus(200); // ack first, act after

  const sub = event.data;
  if (sub.object !== "experience_subscription") return;

  // Key on (experience, fan). `id` is reused when a fan lapses and resubscribes,
  // so it identifies the subscription, not one span of access.
  const member = { experienceUuid: sub.experience.uuid, fanUuid: sub.fan.uuid };

  if (event.type === "creator.experience_subscription.deactivated") {
    // access_ends_at, not expires_at: a refund, chargeback, ban or unpublish
    // revokes mid-period and leaves expires_at in the future.
    await removeFromGroup(member, {
      at: sub.access_ends_at,
      reason: sub.end_reason,
      // access_ends_at < expires_at means the fan lost time they had paid for.
      lostPaidTime: sub.expires_at !== null && sub.access_ends_at < sub.expires_at,
    });
    return;
  }

  if (event.type === "creator.experience_subscription.activated") {
    // Required counterpart. `renewal_recovered` follows a payment_failed
    // revocation you have already acted on; without this the fan stays out.
    await addToGroup(member);
  }
});
```

Two rules this handler encodes:

1. **Revoke on the event, not on a cancellation.** `cancel_at_period_end: true` on a `deactivated` event is context, it tells you the fan chose to leave rather than failed to pay. It is never itself the trigger.
2. **Always pair revoke with grant.** Revoking on `deactivated` without handling `activated` permanently locks out any fan whose renewal recovered after a declined charge.

<Note>
  **Access can also lapse silently at `expires_at`.** A subscription grants access only while it is not deleted **and** `expires_at` is in the future. The hourly expiry sweep defers a row when a renewal is still settling or an upstream lookup fails, so the fan loses access on the clock and no event is sent until a later sweep resolves the row. Treat `expires_at` as a deadline in its own right: expire your own cached grant at `expires_at` and let `activated` extend it, rather than waiting only for `deactivated`.
</Note>

## See also

* [Creator webhooks overview](/docs/creator/overview), the shared envelope and the full event table
* [Creator Subscriptions](/docs/creator/subscriptions), the separate profile-subscription lifecycle
* [Signature verification](/docs/webhooks/signature-verification)
* [Delivery, Retries and Idempotency](/docs/webhooks/delivery-and-idempotency)
* [Scopes](/docs/authentication/scopes)
