> ## Documentation Index
> Fetch the complete documentation index at: https://docs.onlyfansapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Available Fansly webhook events (/webhooks/fansly-events)

<Callout title="Looking for a specific event?">
  Please reach out to us, we can add it!
</Callout>

All Fansly events are prefixed with `fansly.` so they never collide with the [OnlyFans events](/webhooks/available-events). Delivery is driven by our Fansly websocket relay rather than polling, so events arrive in near real-time.

Fansly events only appear in the event picker once your team has a [connected Fansly account](/api-reference/fansly/connect-fansly-account/start-authentication). If you don't see them when [subscribing to a webhook](/webhooks/subscribing-to-webhooks), connect an account first.

## Payload conventions

Fansly events use the same envelope as the OnlyFans events: an `event` name, the `account_id` of the connected account, and a platform-native `payload`:

```json title="Envelope"
{
  "event": "fansly.<category>.<name>",
  "account_id": "fansly_acct_123",
  "payload": { }
}
```

Within `payload`, Fansly's own conventions apply:

<Callout type="warn" title="Money is in thousandths of a dollar">
  Unlike the OnlyFans events, Fansly amounts are **not** dollars and **not** cents. `15000` means `$15.00`. Divide by `1000` before displaying or storing an amount.
</Callout>

* **IDs are snowflake strings**, not integers. Keep them as strings, because they exceed the safe integer range in JavaScript and other 53-bit float languages.
* **Timestamps are Unix epoch**, in seconds *or* milliseconds depending on the upstream field (e.g. `createdAt` is seconds on `fansly.posts.created`, but milliseconds on `fansly.subscriptions.new`). Check the magnitude before parsing.
* **`type` fields are Fansly enums**, not strings. Their meaning is specific to each event.

## Coming from the OnlyFans events?

The catalogs are not identical. If you're porting an existing OnlyFans integration:

| OnlyFans event                                                                                                                                  | Fansly equivalent                                                                                                                                                                                                  |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`subscriptions.new`](/webhooks/available-events#subscriptionsnew) / [`subscriptions.renewed`](/webhooks/available-events#subscriptionsrenewed) | [`fansly.subscriptions.new`](#fanslysubscriptionsnew) / [`fansly.subscriptions.renewed`](#fanslysubscriptionsrenewed) — the same split                                                                             |
| [`subscriptions.expired`](/webhooks/available-events#subscriptionsexpired)                                                                      | [`fansly.subscriptions.expired`](#fanslysubscriptionsexpired). Fansly pushes this one; OnlyFans has no expiry signal, so the OnlyFans event is derived and arrives within about 15 minutes rather than immediately |
| [`messages.ppv.unlocked`](/webhooks/available-events#messagesppvunlocked)                                                                       | [`fansly.media.purchased`](#fanslymediapurchased)                                                                                                                                                                  |
| [`users.online`](/webhooks/available-events#usersonline) / [`users.offline`](/webhooks/available-events#usersoffline)                           | Not available. Fansly exposes no fan presence signal — poll `lastSeenAt` on [Get User Details](/api-reference/fansly/users/get-user-details) instead                                                               |
| `chat_queue.updated` / `chat_queue.finished`                                                                                                    | Not available                                                                                                                                                                                                      |
| `accounts.reconnected`, `accounts.session_expired`                                                                                              | Folded into [`fansly.accounts.connected`](#fanslyaccountsconnected)                                                                                                                                                |
| [`accounts.disconnected`](/webhooks/available-events#accountsdisconnected)                                                                      | [`fansly.accounts.disconnected`](#fanslyaccountsdisconnected)                                                                                                                                                      |
| [`accounts.otp_code_required`](/webhooks/available-events#accountsotp_code_required)                                                            | [`fansly.accounts.otp_code_required`](#fanslyaccountsotp_code_required), for both the emailed and authenticator-app code                                                                                           |
| `accounts.face_otp_required`                                                                                                                    | Not available. Fansly has no selfie or liveness step                                                                                                                                                               |
| (none)                                                                                                                                          | Fansly adds reactions, read receipts, wall post lifecycle, post comments, media likes, followers, payouts, story purchases, wallet balances, blocks, streams and profile changes                                   |

## Accounts

### `fansly.accounts.connected`

A Fansly account was connected or re-authenticated. Fires for all of your Fansly accounts.

```json title="Example payload"
{
    "event": "fansly.accounts.connected",
    "account_id": "fansly_acct_123",
    "payload": {
        "status": "authenticated",
        "is_session_active": true,
        "last_authenticated_at": "2026-01-01T02:33:20+00:00",
        "last_error": null,
        "fansly_user": {
            "id": "100000000000000001",
            "username": "example_creator",
            "display_name": "Example Creator"
        }
    }
}
```

### `fansly.accounts.disconnected`

A Fansly account was disconnected, either from the [dashboard](https://app.onlyfansapi.com) or with the [Disconnect Account](/api-reference/fansly/account/disconnect-account) endpoint.

Every request for that `fansly_acct_` ID from this point on returns a `404`, so stop polling it when this arrives.

<Callout type="warn" title="Reconnecting gives you a new fansly_acct_ ID">
  The disconnected `fansly_acct_` ID is never reused. If the creator reconnects
  later, they come back as a new account, so key your own records on
  `fansly_user.id` — that snowflake is the creator's Fansly identity and we
  re-link the same one on reconnect.
</Callout>

Unlike the two events above, the payload carries no `status`, `is_session_active` or `last_error`: those describe a live session, and the account no longer has one.

```json title="Example payload"
{
    "event": "fansly.accounts.disconnected",
    "account_id": "fansly_acct_123",
    "payload": {
        "account_id": "fansly_acct_123",
        "fansly_user": {
            "id": "100000000000000001",
            "username": "example_creator",
            "display_name": "Example Creator"
        },
        "disconnected_at": "2026-01-01T02:33:20+00:00"
    }
}
```

<Callout title="Not the same as a broken session">
  This event only fires when the account is deliberately disconnected. A login
  that breaks on its own gives you
  [`fansly.accounts.authentication_failed`](#fanslyaccountsauthentication_failed)
  instead, and the account stays connected until you disconnect it.
</Callout>

### `fansly.accounts.authentication_failed`

An account login attempt failed. Reconnect with our [/authenticate](/api-reference/fansly/connect-fansly-account/start-authentication) endpoint or manually in our [dashboard](https://app.onlyfansapi.com).

```json title="Example payload"
{
    "event": "fansly.accounts.authentication_failed",
    "account_id": "fansly_acct_123",
    "payload": {
        "status": "auth-failed",
        "is_session_active": false,
        "last_authenticated_at": null,
        "last_error": "Fansly login failed: invalid credentials",
        "fansly_user": null
    }
}
```

### `fansly.accounts.otp_code_required`

A connect or re-authenticate attempt stopped on Fansly's two-factor challenge: the creator has to give you a code before we can finish the login. Fires for both [Start Authentication](/api-reference/fansly/connect-fansly-account/start-authentication) and [Re-authenticate Account](/api-reference/fansly/connect-fansly-account/re-authenticate-account).

One event covers both code sources. `twofa_type` — and its string alias `twofa_method` — tells you where the creator gets the code:

* **`2` / `"email"`** — a code Fansly emailed to the account's own address, masked in `masked_email`. This is what a login from a new IP gets.
* **`0` / `"app"`** — a code from the creator's authenticator app. `masked_email` is `null`, because there is no destination to show.

You submit it the same way either way, with [Submit 2FA](/api-reference/fansly/connect-fansly-account/submit2fa). The challenge is only valid until `expires_at` (15 minutes from when it was raised); after that, start a fresh attempt to get a new code.

```json title="Example payload (emailed code)"
{
    "event": "fansly.accounts.otp_code_required",
    "account_id": "fansly_acct_123",
    "payload": {
        "status": "challenge-email",
        "is_session_active": false,
        "last_authenticated_at": null,
        "last_error": null,
        "fansly_user": null,
        "twofa_type": 2,
        "twofa_method": "email",
        "masked_email": "e*****@e*****.com",
        "expires_at": "2026-01-01T02:48:20.000000Z"
    }
}
```

For the authenticator-app challenge, `status` is `challenge-app` and there is no masked destination:

```json title="Example payload (authenticator app)"
{
    "event": "fansly.accounts.otp_code_required",
    "account_id": "fansly_acct_123",
    "payload": {
        "status": "challenge-app",
        "is_session_active": false,
        "last_authenticated_at": null,
        "last_error": null,
        "fansly_user": null,
        "twofa_type": 0,
        "twofa_method": "app",
        "masked_email": null,
        "expires_at": "2026-01-01T02:48:20.000000Z"
    }
}
```

The `fansly_user` block and `last_authenticated_at` are `null` while a first connection is still being set up, as above. On a re-authentication of an account you already know, both are filled in with the identity and the previous session's timestamp.

<Callout title="A rejected code does not re-send this event">
  If the wrong code is submitted, the challenge stays pending and you get no second event —
  [Poll Authentication Status](/api-reference/fansly/connect-fansly-account/poll-authentication-status)
  keeps returning `needs_code: true`, with the reason in `last_error`. Ask the creator for the
  code again and submit it. A new event fires only when a fresh attempt is started.
</Callout>

<Callout type="info" title="The account stays the source of truth">
  The same challenge is always readable from the account itself: `needs_code`, `twofa_type` and
  `masked_email` on
  [Poll Authentication Status](/api-reference/fansly/connect-fansly-account/poll-authentication-status),
  and `authentication_progress` on
  [List Accounts](/api-reference/fansly/account/list-accounts). Check there if your endpoint was
  down when the challenge was raised, rather than waiting for a redelivery.
</Callout>

### `fansly.accounts.alert`

Fansly raised an alert against the connected account. `metadata.reason` says which — switch on it, because the rest of the block differs per reason.

Every alert we have observed so far belongs to the co-performer consent workflow:

* **`consent_request_received`** — someone asked you to confirm you appear in their content. `requester_account_id` is who asked, `request_id` the request itself.
* **`consent_sign_expired`** — a consent form you rely on has lapsed, which blocks the content it covers. `co_performer_id` and `nickname` identify the other performer.

<Callout type="info" title="Fansly ships metadata as a string">
  On the wire this field is a JSON-encoded string. We decode it for you, so
  `metadata` arrives as a real object. If it ever isn't decodable we forward it
  untouched, so guard with a type check before indexing into it.
</Callout>

```json title="Example payload"
{
    "event": "fansly.accounts.alert",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "800000000000000008",
        "accountId": "100000000000000001",
        "type": 24001,
        "correlationId": "300000000000000003",
        "correlationGroupId": null,
        "lookups": [
            {
                "lookupId": "300000000000000003"
            }
        ],
        "metadata": {
            "reason": "consent_sign_expired",
            "co_performer_id": "300000000000000003",
            "nickname": "Example Co-performer",
            "feedback_note": ""
        }
    }
}
```

### `fansly.accounts.updated`

The connected account's own record changed on Fansly — username, display name, email, or its `flags` bitfield. `modelFlagChange` marks a change to the account's model status.

`version` increments on every change, so use it to order events that arrive out of sequence.

```json title="Example payload"
{
    "event": "fansly.accounts.updated",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "100000000000000001",
        "email": "creator@example.com",
        "username": "examplecreator",
        "displayName": "Example Creator",
        "flags": 18,
        "version": 14,
        "createdAt": 1767230000000,
        "modelFlagChange": false
    }
}
```

## Messages & chats

### `fansly.messages.received`

A message was received from a fan. Includes the resolved sender identity in `senderData`.

```json title="Example payload"
{
    "event": "fansly.messages.received",
    "account_id": "fansly_acct_123",
    "payload": {
        "type": 1,
        "attachments": [],
        "content": "hey! loved your latest post",
        "groupId": "100000000000000001",
        "senderId": "200000000000000002",
        "inReplyTo": "",
        "interactions": [
            {
                "groupId": "100000000000000001",
                "userId": "300000000000000003",
                "readAt": 0,
                "deliveredAt": 0,
                "messageId": "400000000000000004"
            }
        ],
        "id": "400000000000000004",
        "createdAt": 1767230000.045,
        "embeds": [],
        "direction": "incoming",
        "senderData": {
            "available": true,
            "id": "200000000000000002",
            "username": "example_fan",
            "display_name": "Example Fan",
            "avatar": "https://cdn.fansly.example/avatars/200000000000000002.jpg"
        }
    }
}
```

### `fansly.messages.sent`

A message was sent from one of your Fansly accounts, from our API, the Fansly app, or any other tool.

```json title="Example payload"
{
    "event": "fansly.messages.sent",
    "account_id": "fansly_acct_123",
    "payload": {
        "type": 1,
        "attachments": [],
        "content": "thank you so much! 💕",
        "groupId": "100000000000000001",
        "senderId": "300000000000000003",
        "inReplyTo": "",
        "interactions": [
            {
                "groupId": "100000000000000001",
                "userId": "200000000000000002",
                "readAt": 0,
                "deliveredAt": 0,
                "messageId": "500000000000000005"
            }
        ],
        "id": "500000000000000005",
        "createdAt": 1767231000.421,
        "embeds": [],
        "direction": "outgoing"
    }
}
```

### `fansly.messages.deleted`

A chat message was deleted. The payload carries the last known message body, so you can reconcile without a lookup.

```json title="Example payload"
{
    "event": "fansly.messages.deleted",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "400000000000000004",
        "type": 1,
        "dataVersion": 1,
        "content": "hey! loved your latest post",
        "groupId": "100000000000000001",
        "senderId": "200000000000000002",
        "correlationId": "0",
        "inReplyTo": null,
        "inReplyToRoot": null,
        "createdAt": 1767230000,
        "attachments": [],
        "embeds": [],
        "interactions": [
            {
                "userId": "300000000000000003",
                "readAt": 0,
                "deliveredAt": 1767230004000
            }
        ],
        "likes": [],
        "deletedAt": 1767230009,
        "direction": "incoming"
    }
}
```

### `fansly.messages.read`

A read receipt for a conversation. `messageIds` lists the messages marked as read by `userId`.

```json title="Example payload"
{
    "event": "fansly.messages.read",
    "account_id": "fansly_acct_123",
    "payload": {
        "groupId": "100000000000000001",
        "messageIds": [
            "400000000000000004"
        ],
        "userId": "200000000000000002",
        "userReadReceiptsEnabled": true,
        "type": 1,
        "recipients": [
            {
                "userId": "300000000000000003",
                "readReceiptsEnabled": true
            },
            {
                "userId": "200000000000000002",
                "readReceiptsEnabled": true
            }
        ]
    }
}
```

### `fansly.messages.reaction_added`

A fan reacted to a chat message. `type` is the emoji reaction id.

```json title="Example payload"
{
    "event": "fansly.messages.reaction_added",
    "account_id": "fansly_acct_123",
    "payload": {
        "accountId": "200000000000000002",
        "messageId": "400000000000000004",
        "type": 5,
        "groupId": "100000000000000001",
        "id": "410000000000000041"
    }
}
```

### `fansly.messages.reaction_removed`

A reaction was removed from a chat message.

```json title="Example payload"
{
    "event": "fansly.messages.reaction_removed",
    "account_id": "fansly_acct_123",
    "payload": {
        "accountId": "200000000000000002",
        "messageId": "400000000000000004",
        "type": 5,
        "groupId": "100000000000000001",
        "id": "410000000000000042"
    }
}
```

### `fansly.chats.visibility_updated`

A conversation was hidden or unhidden. `userSettings.hidden` is the resulting state: `1` hidden, `0` visible.

<Callout type="warn" title="High volume">
  This fires on every hide and unhide across your inbox. Only subscribe if you mirror conversation state; otherwise ignore it.
</Callout>

```json title="Example payload"
{
    "event": "fansly.chats.visibility_updated",
    "account_id": "fansly_acct_123",
    "payload": {
        "groupId": "100000000000000001",
        "userId": "200000000000000002",
        "userSettings": {
            "hidden": 0
        }
    }
}
```

## Posts

### `fansly.posts.created`

You published a new wall post.

<Callout type="info" title="One event per post, not per wall">
  Fansly emits a separate frame for every wall a post lands on. We collapse
  them, so a post cross-posted to several walls arrives once, carrying the
  first wall we saw it on. Adding an existing post to a further wall more than
  24 hours later does deliver again.
</Callout>

```json title="Example payload"
{
    "event": "fansly.posts.created",
    "account_id": "fansly_acct_123",
    "payload": {
        "postId": "500000000000000005",
        "wallId": "200000000000000010",
        "wall": {
            "id": "200000000000000010",
            "accountId": "100000000000000001",
            "pos": 0,
            "name": "Posts",
            "description": "",
            "private": 0,
            "metadata": ""
        },
        "post": {
            "id": "500000000000000005",
            "accountId": "100000000000000001",
            "content": "New set just dropped 🔥 #newpost",
            "fypFlags": 0,
            "inReplyTo": null,
            "inReplyToRoot": null,
            "createdAt": 1767230000,
            "expiresAt": null,
            "attachments": [
                {
                    "postId": "500000000000000005",
                    "pos": 0,
                    "contentType": 1,
                    "contentId": "600000000000000006",
                    "deletedAt": null
                }
            ]
        },
        "metadata": {
            "recentPost": true
        }
    }
}
```

### `fansly.posts.updated`

You edited a wall post.

```json title="Example payload"
{
    "event": "fansly.posts.updated",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "500000000000000005",
        "accountId": "100000000000000001",
        "content": "New set just dropped 🔥 (now with more pics) #newpost",
        "fypFlags": 0,
        "inReplyTo": null,
        "inReplyToRoot": null,
        "createdAt": 1767230000,
        "expiresAt": 0,
        "attachments": [
            {
                "contentType": 1,
                "contentId": "600000000000000006"
            }
        ],
        "likeCount": 12,
        "postReplyPermissionFlags": [],
        "wallIds": [
            "200000000000000010"
        ]
    }
}
```

### `fansly.posts.deleted`

A wall post was deleted. Like [`fansly.posts.created`](#fanslypostscreated), a post that was on several walls arrives once rather than once per wall.

For a post that was removed because it hit its own expiry, [`fansly.posts.expired`](#fanslypostsexpired) fires alongside this one and carries the full post body.

```json title="Example payload"
{
    "event": "fansly.posts.deleted",
    "account_id": "fansly_acct_123",
    "payload": {
        "wallId": "200000000000000010",
        "postId": "500000000000000005",
        "wall": {
            "id": "200000000000000010",
            "accountId": "100000000000000001",
            "pos": 0,
            "name": "Posts",
            "description": "",
            "private": 1,
            "metadata": ""
        }
    }
}
```

### `fansly.posts.pinned`

A wall post was pinned.

```json title="Example payload"
{
    "event": "fansly.posts.pinned",
    "account_id": "fansly_acct_123",
    "payload": {
        "postId": "500000000000000005",
        "accountId": "100000000000000001",
        "pos": 0,
        "wallId": "200000000000000010"
    }
}
```

### `fansly.posts.expired`

A post was removed because it reached its own `expiresAt`. Where [`fansly.posts.deleted`](#fanslypostsdeleted) only identifies the post and the wall, this carries the whole post — content, attachments and all — so you can archive it as it goes.

Both events fire for an expiring post. Subscribe to whichever suits you, or to both if you want the body and the wall.

```json title="Example payload"
{
    "event": "fansly.posts.expired",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "500000000000000005",
        "accountId": "100000000000000001",
        "content": "Only up for 3 days 👀 #example",
        "fypFlags": 2,
        "inReplyTo": null,
        "inReplyToRoot": null,
        "createdAt": 1767230000,
        "expiresAt": 1767489200000,
        "attachments": [
            {
                "postId": "500000000000000005",
                "pos": 0,
                "contentType": 1,
                "contentId": "600000000000000006",
                "deletedAt": null
            }
        ],
        "delitionType": 1
    }
}
```

<Callout type="info" title="createdAt and expiresAt use different units">
  `createdAt` is epoch **seconds** here, while `expiresAt` is epoch
  **milliseconds** — Fansly's own inconsistency, forwarded as-is. Check the
  magnitude before parsing, as noted in the [payload
  conventions](#payload-conventions).
</Callout>

### `fansly.posts.commented`

Someone commented on, replied to, or quoted one of your posts. `kind` is `reply` for a comment or a reply to one, and `quote` when your post was quoted by another post.

A comment on Fansly is itself a post, and the notification behind this event carries only IDs — so we fetch the comment and its author for you and deliver them under `comment`.

<Callout type="warn" title="Check comment.available first">
  Enrichment is best-effort: if the lookup fails, or the comment is already
  gone by the time we fetch it, the event is still delivered with
  `comment.available: false` and an `error` block instead of the body. Always
  branch on `available` before reading `content` or `author`.
</Callout>

`comment.in_reply_to` is what was replied to directly and `comment.in_reply_to_root` is the post the thread hangs off. They match for a top-level comment and differ when a fan replies to another comment.

```json title="Example payload"
{
    "event": "fansly.posts.commented",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "800000000000000008",
        "accountId": "100000000000000001",
        "type": 1004,
        "correlationId": "500000000000000006",
        "correlationGroupId": "500000000000000005",
        "kind": "reply",
        "post_id": "500000000000000005",
        "comment": {
            "available": true,
            "id": "500000000000000006",
            "content": "love this 😍",
            "created_at": 1767230000,
            "in_reply_to": "500000000000000005",
            "in_reply_to_root": "500000000000000005",
            "quoted_post_id": null,
            "attachments": [],
            "author": {
                "available": true,
                "id": "200000000000000002",
                "username": "example_fan",
                "display_name": "Example Fan",
                "avatar": "https://cdn.fansly.example/a.jpg"
            }
        }
    }
}
```

### `fansly.posts.liked`

A fan liked one of your posts. `correlationId` is the post, `correlationGroupId` is the fan who liked it.

```json title="Example payload"
{
    "event": "fansly.posts.liked",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "920000000000000021",
        "type": 1002,
        "accountId": "100000000000000001",
        "correlationId": "700000000000000070",
        "correlationGroupId": "200000000000000002",
        "lookups": [
            {
                "lookupId": "930000000000000030"
            }
        ]
    }
}
```

## Media

### `fansly.media.purchased`

A fan purchased a media item. This is the Fansly equivalent of an unlocked PPV. Bundles fire one event per item, so a single checkout can produce several events. `orderMetadata.accountMediaPrice` is in thousandths of a dollar.

```json title="Example payload"
{
    "event": "fansly.media.purchased",
    "account_id": "fansly_acct_123",
    "payload": {
        "orderId": "940000000000000040",
        "accountMediaId": "800000000000000080",
        "correlationAccountId": "100000000000000001",
        "accountId": "200000000000000002",
        "type": 1,
        "orderMetadata": {
            "accountMediaPrice": 15000
        }
    }
}
```

### `fansly.media.liked`

A fan liked a media item. `correlationId` is the media, `correlationGroupId` is the fan.

<Callout type="warn" title="High volume">
  Popular accounts can receive these continuously. Only subscribe if you actually process them, and make sure your endpoint can keep up.
</Callout>

```json title="Example payload"
{
    "event": "fansly.media.liked",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "920000000000000022",
        "type": 2002,
        "accountId": "100000000000000001",
        "correlationId": "800000000000000080",
        "correlationGroupId": "200000000000000002",
        "lookups": [
            {
                "lookupId": "930000000000000031"
            }
        ],
        "metadata": "{\"likeId\":\"930000000000000031\",\"accountMediaAccess\":true}"
    }
}
```

## Subscriptions

### `fansly.subscriptions.new`

A fan subscribed to you for the first time. Rebills of an existing subscription arrive as [`fansly.subscriptions.renewed`](#fanslysubscriptionsrenewed) instead.

`price` and `renewPrice` are in thousandths of a dollar, and the timestamps are epoch milliseconds.

<Callout type="warn" title="This changed in August 2026">
  This event used to fire for renewals too. If your integration predates that
  change and counts new subscribers from this event, it was over-counting, and
  it now needs to consume
  [`fansly.subscriptions.renewed`](#fanslysubscriptionsrenewed) as well to see
  the same volume.
</Callout>

A first subscription is recognisable on its own terms: `subscriptionStreak` and `subscriptionTotalDays` are both `0`, and `createdAt` is the moment the event fires.

```json title="Example payload"
{
    "event": "fansly.subscriptions.new",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "500000000000000005",
        "historyId": "510000000000000005",
        "accountId": "100000000000000001",
        "subscriberId": "200000000000000002",
        "subscriptionTierId": "600000000000000006",
        "subscriptionTierName": "Example Tier",
        "subscriptionTierColor": "#46A7F8",
        "planId": "610000000000000006",
        "promoId": "0",
        "giftCodeId": null,
        "paymentMethodId": "0",
        "status": 3,
        "price": 9990,
        "renewPrice": 9990,
        "renewCorrelationId": "510000000000000005",
        "autoRenew": 1,
        "version": 2,
        "billingCycle": 30,
        "duration": 30,
        "renewDate": 1767230000000,
        "createdAt": 1767230000000,
        "endsAt": 1769908400000,
        "promoDuration": null,
        "promoStartsAt": null,
        "promoEndsAt": null,
        "updatedAt": 1767230000998,
        "subscriptionStreak": 0,
        "subscriptionTotalDays": 0
    }
}
```

### `fansly.subscriptions.renewed`

An existing subscription rebilled. `subscriptionStreak` counts the consecutive billing cycles, so `5` is the fan's fifth month, and `subscriptionTotalDays` is how long they have been subscribed in total.

The payload is the same shape as [`fansly.subscriptions.new`](#fanslysubscriptionsnew), with `historyId` freshly minted for this cycle, `version` incremented, and `endsAt` pushed out by another `billingCycle`.

```json title="Example payload"
{
    "event": "fansly.subscriptions.renewed",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "500000000000000005",
        "historyId": "520000000000000005",
        "accountId": "100000000000000001",
        "subscriberId": "200000000000000002",
        "subscriptionTierId": "600000000000000006",
        "subscriptionTierName": "Example Tier",
        "subscriptionTierColor": "#FF69B4",
        "planId": "610000000000000006",
        "promoId": null,
        "giftCodeId": null,
        "paymentMethodId": "0",
        "status": 3,
        "price": 5550,
        "renewPrice": 5550,
        "renewCorrelationId": "510000000000000005",
        "autoRenew": 1,
        "version": 21,
        "billingCycle": 30,
        "duration": 30,
        "renewDate": 1754390000000,
        "createdAt": 1754390000000,
        "endsAt": 1770290000000,
        "promoDuration": null,
        "promoStartsAt": null,
        "promoEndsAt": null,
        "updatedAt": 1767611600000,
        "subscriptionStreak": 5,
        "subscriptionTotalDays": 150
    }
}
```

<Callout type="info" title="A brief lapse before each rebill is normal">
  Fansly marks the subscription expired for a moment before the renewal lands.
  We don't deliver that intermediate state, so a rebill normally reaches you as
  this event alone rather than as an expiry followed by a new subscription.
</Callout>

### `fansly.subscriptions.expired`

A fan's subscription expired. `correlationId` is the subscription, `correlationGroupId` is the fan.

```json title="Example payload"
{
    "event": "fansly.subscriptions.expired",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "920000000000000020",
        "type": 15007,
        "accountId": "100000000000000001",
        "correlationId": "500000000000000005",
        "correlationGroupId": "200000000000000002"
    }
}
```

## Followers

### `fansly.followers.new`

A fan followed your account.

```json title="Example payload"
{
    "event": "fansly.followers.new",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "910000000000000010",
        "followerId": "200000000000000002",
        "accountId": "100000000000000001",
        "accountSortOrder": 1
    }
}
```

### `fansly.followers.removed`

A fan unfollowed your account.

<Callout type="info">
  `createdAt` is the date of the **original follow**, not the unfollow. Use your own receive time if you need to know when the unfollow happened.
</Callout>

```json title="Example payload"
{
    "event": "fansly.followers.removed",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "910000000000000010",
        "followerId": "200000000000000002",
        "accountId": "100000000000000001",
        "createdAt": 1764547200000
    }
}
```

## Fans

### `fansly.fans.ignore_updated`

You blocked, muted, or cleared either for a Fansly user — from our [Block User](/api-reference/fansly/users/block-user) endpoint, the Fansly app, or anywhere else. `ignoredId` is the user it was applied to.

`state` is the decoded result of `ignoreFlags`, so you don't have to know the numbers:

| `ignoreFlags` | `state`   | Meaning              |
| ------------- | --------- | -------------------- |
| `0`           | `none`    | Unblocked or unmuted |
| `1`           | `muted`   | Muted                |
| `2`           | `blocked` | Blocked              |

<Callout type="info" title="A cleared flag doesn't say what it undid">
  One frame carries only the resulting state, with no record of what came
  before, so `state: "none"` means "no longer blocked *or* muted" without
  distinguishing the two. Track the previous state on your side if you need to
  tell an unblock from an unmute.
</Callout>

```json title="Example payload"
{
    "event": "fansly.fans.ignore_updated",
    "account_id": "fansly_acct_123",
    "payload": {
        "accountId": "100000000000000001",
        "ignoredId": "200000000000000002",
        "ignoreFlags": 2,
        "state": "blocked"
    }
}
```

## Profiles

### `fansly.profiles.updated`

Your public profile changed — avatar, banner, bio, location, socials or badges. `avatarId` and `bannerId` are media IDs rather than URLs; read the resolved avatar from [Get User Details](/api-reference/fansly/users/get-user-details) if you need something displayable.

```json title="Example payload"
{
    "event": "fansly.profiles.updated",
    "account_id": "fansly_acct_123",
    "payload": {
        "accountId": "100000000000000001",
        "avatarId": "700000000000000007",
        "bannerId": "700000000000000008",
        "about": "Example bio 🌸",
        "location": "Example City",
        "socials": [],
        "badges": []
    }
}
```

## Streams

### `fansly.streams.updated`

A live stream changed, or its access permissions did. Both arrive as this one event.

<Callout type="info" title="status is forwarded raw">
  We pass Fansly's `status` through without interpreting it, because we have
  not yet seen enough of its values in production to map them to
  started/ended reliably. Compare `version` between events to order them, and
  treat `status` as opaque unless you have verified a value yourself.
</Callout>

```json title="Example payload"
{
    "event": "fansly.streams.updated",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "400000000000000005",
        "channelId": "400000000000000004",
        "accountId": "100000000000000001",
        "title": "",
        "status": 1,
        "viewerCount": 0,
        "version": 1938,
        "createdAt": 1767230000000,
        "updatedAt": null,
        "lastFetchedAt": 1767230000829
    }
}
```

## Wallets

### `fansly.wallets.updated`

Your earnings wallet balance changed. Fires on every earning and every payout, so it is a running mirror of your balance rather than a record of what caused the change — pair it with [`fansly.transactions.new`](#fanslytransactionsnew) for that.

<Callout type="warn" title="High volume">
  A busy account changes balance constantly. Only subscribe if you display or reconcile a live balance.
</Callout>

Both `balance` and `balance64` are forwarded exactly as Fansly sends them, in thousandths of a dollar. They do not always agree; `walletVersion` increments on every change and orders the events.

```json title="Example payload"
{
    "event": "fansly.wallets.updated",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "900000000000000009",
        "balance": 4548002,
        "balance64": 4530402,
        "accountId": "100000000000000001",
        "type": 2,
        "flags": 0,
        "walletVersion": 2372,
        "updatedAt": 1767230000112
    }
}
```

## Payouts

### `fansly.payouts.created`

A payout was requested. `amount` is in thousandths of a dollar, so `5000000` is `$5,000.00`.

```json title="Example payload"
{
    "event": "fansly.payouts.created",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "950000000000000050",
        "accountId": "100000000000000001",
        "amount": 5000000,
        "payoutMethodId": "960000000000000060",
        "status": 1,
        "version": 0
    }
}
```

### `fansly.payouts.updated`

A payout request changed status. Compare `version` to detect ordering if events arrive out of sequence.

```json title="Example payload"
{
    "event": "fansly.payouts.updated",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "950000000000000050",
        "accountId": "100000000000000001",
        "amount": 5000000,
        "payoutMethodId": "960000000000000060",
        "status": 2,
        "version": 1
    }
}
```

### `fansly.payouts.debited`

Money actually left your earnings wallet for a payout. Where the two events above track the payout *request* and its status, this is the wallet movement itself.

<Callout type="info" title="Deliberately kept out of fansly.transactions.new">
  A payout is an outflow, so including it in the earnings feed would let any
  integration that sums [`fansly.transactions.new`](#fanslytransactionsnew)
  count a withdrawal as income. It is delivered here and only here.
</Callout>

`amount` is in thousandths of a dollar. `originWalletId` is the wallet debited, and `destinationWalletId` is `null` because the money leaves Fansly entirely.

```json title="Example payload"
{
    "event": "fansly.payouts.debited",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "800000000000000008",
        "type": 16012,
        "originWalletId": "900000000000000009",
        "destinationWalletId": null,
        "status": 2,
        "amount": 15200000,
        "createdAt": 1767230000000
    }
}
```

## Transactions & Tips

### `fansly.transactions.new`

A new earning: a subscription, tip, PPV purchase, and so on. Payouts are excluded — they arrive as [`fansly.payouts.debited`](#fanslypayoutsdebited) instead.

`amount` is the gross amount, `destinationTax` the platform cut, and `destinationAmount` what you actually receive. All three are in thousandths of a dollar.

```json title="Example payload"
{
    "event": "fansly.transactions.new",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "700000000000000007",
        "type": 2110,
        "correlationId": "800000000000000008",
        "walletId": "900000000000000009",
        "destinationWalletId": "900000000000000009",
        "accountId": "200000000000000002",
        "amount": 15000,
        "destinationTax": 2000,
        "destinationAmount": 12000,
        "senderId": "200000000000000002",
        "receiverId": "100000000000000001",
        "status": 1,
        "createdAt": 1767230000000,
        "updatedAt": 1767230000000
    }
}
```

### `fansly.tips.received`

You received a tip from a fan. This is the same transaction shape as above with a `type` in the tip range, so a tip that you subscribe to both events for will arrive twice: once as `fansly.tips.received` and once as `fansly.transactions.new`.

```json title="Example payload"
{
    "event": "fansly.tips.received",
    "account_id": "fansly_acct_123",
    "payload": {
        "id": "700000000000000007",
        "type": 7101,
        "correlationId": "800000000000000008",
        "walletId": "900000000000000009",
        "destinationWalletId": "900000000000000009",
        "accountId": "200000000000000002",
        "amount": 5000,
        "destinationTax": 2000,
        "destinationAmount": 4000,
        "senderId": "200000000000000002",
        "receiverId": "100000000000000001",
        "status": 1,
        "createdAt": 1767230000000,
        "updatedAt": 1767230000000
    }
}
```

## Stories

### `fansly.stories.purchased`

A fan purchased one of your paid stories. This event is keyed on `transactionId`. There is no `orderId` like on [`fansly.media.purchased`](#fanslymediapurchased).

```json title="Example payload"
{
    "event": "fansly.stories.purchased",
    "account_id": "fansly_acct_123",
    "payload": {
        "accountId": "200000000000000002",
        "storyId": "810000000000000081",
        "transactionId": "820000000000000082",
        "price": 15000,
        "type": 1,
        "correlationAccountId": "100000000000000001"
    }
}
```

## Users

### `fansly.users.typing`

A fan is typing in a conversation.

<Callout type="warn" title="High volume">
  Typing indicators fire repeatedly for a single message being composed. Debounce on your side, and don't subscribe unless you're rendering a live inbox.
</Callout>

```json title="Example payload"
{
    "event": "fansly.users.typing",
    "account_id": "fansly_acct_123",
    "payload": {
        "accountId": "200000000000000002",
        "groupId": "100000000000000001",
        "lastAnnounce": 1767229990129
    }
}
```

<Callout type="info" title="No presence events">
  Fansly does not expose fan online/offline presence, so there is no equivalent of the OnlyFans [`users.online`](/webhooks/available-events#usersonline) and [`users.offline`](/webhooks/available-events#usersoffline) events.

  Poll the `lastSeenAt` field on [Get User Details](/api-reference/fansly/users/get-user-details) instead — [see the FAQ](/faq#is-there-a-fansly-webhook-for-when-a-fan-comes-online) for the caveats.
</Callout>