Available Fansly webhook events
A list of all available Fansly webhook events that you can subscribe to.
Looking for a specific event?
Please reach out to us, we can add it!
All Fansly events are prefixed with fansly. so they never collide with the OnlyFans 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. If you don't see them when subscribing to a webhook, 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:
{
"event": "fansly.<category>.<name>",
"account_id": "fansly_acct_123",
"payload": { }
}Within payload, Fansly's own conventions apply:
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.
- 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.
createdAtis seconds onfansly.posts.created, but milliseconds onfansly.subscriptions.new). Check the magnitude before parsing. typefields 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 / subscriptions.renewed | fansly.subscriptions.new / fansly.subscriptions.renewed — the same split |
subscriptions.expired | fansly.subscriptions.expired. 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 | fansly.media.purchased |
users.online / users.offline | Not available. Fansly exposes no fan presence signal — poll lastSeenAt on Get User Details instead |
chat_queue.updated / chat_queue.finished | Not available |
accounts.reconnected, accounts.session_expired | Folded into fansly.accounts.connected |
accounts.disconnected | fansly.accounts.disconnected |
accounts.otp_code_required | fansly.accounts.otp_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.
{
"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 or with the Disconnect Account endpoint.
Every request for that fansly_acct_ ID from this point on returns a 404, so stop polling it when this arrives.
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.
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.
{
"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"
}
}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
instead, and the account stays connected until you disconnect it.
fansly.accounts.authentication_failed
An account login attempt failed. Reconnect with our /authenticate endpoint or manually in our dashboard.
{
"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 and 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 inmasked_email. This is what a login from a new IP gets.0/"app"— a code from the creator's authenticator app.masked_emailisnull, because there is no destination to show.
You submit it the same way either way, with Submit 2FA. 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.
{
"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:
{
"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.
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
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.
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,
and authentication_progress on
List Accounts. Check there if your endpoint was
down when the challenge was raised, rather than waiting for a redelivery.
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_idis who asked,request_idthe request itself.consent_sign_expired— a consent form you rely on has lapsed, which blocks the content it covers.co_performer_idandnicknameidentify the other performer.
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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
High volume
This fires on every hide and unhide across your inbox. Only subscribe if you mirror conversation state; otherwise ignore it.
{
"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.
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.
{
"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.
{
"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, 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 fires alongside this one and carries the full post body.
{
"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.
{
"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 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.
{
"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
}
}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.
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.
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.
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.
{
"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.
{
"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.
{
"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.
High volume
Popular accounts can receive these continuously. Only subscribe if you actually process them, and make sure your endpoint can keep up.
{
"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 instead.
price and renewPrice are in thousandths of a dollar, and the timestamps are epoch milliseconds.
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 as well to see
the same volume.
A first subscription is recognisable on its own terms: subscriptionStreak and subscriptionTotalDays are both 0, and createdAt is the moment the event fires.
{
"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, with historyId freshly minted for this cycle, version incremented, and endsAt pushed out by another billingCycle.
{
"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
}
}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.
fansly.subscriptions.expired
A fan's subscription expired. correlationId is the subscription, correlationGroupId is the fan.
{
"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.
{
"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.
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.
{
"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 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 |
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.
{
"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 if you need something displayable.
{
"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.
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.
{
"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 for that.
High volume
A busy account changes balance constantly. Only subscribe if you display or reconcile a live balance.
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.
{
"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.
{
"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.
{
"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.
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
count a withdrawal as income. It is delivered here and only here.
amount is in thousandths of a dollar. originWalletId is the wallet debited, and destinationWalletId is null because the money leaves Fansly entirely.
{
"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 instead.
amount is the gross amount, destinationTax the platform cut, and destinationAmount what you actually receive. All three are in thousandths of a dollar.
{
"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.
{
"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.
{
"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.
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.
{
"event": "fansly.users.typing",
"account_id": "fansly_acct_123",
"payload": {
"accountId": "200000000000000002",
"groupId": "100000000000000001",
"lastAnnounce": 1767229990129
}
}No presence events
Fansly does not expose fan online/offline presence, so there is no equivalent of the OnlyFans users.online and users.offline events.
Poll the lastSeenAt field on Get User Details instead — see the FAQ for the caveats.