Skip to content
sanalyer API

Feed

A post is a short text, up to four images, or both, published to the accounts that follow its author. A post can instead hold one video, which becomes a post only after it is encoded and checked. A post with text can carry a poll instead of media, and a public post can also reach other accounts through Discover. New posts are not pushed over the Gateway; a client reads the home feed. Accounts that may read a post can like it, comment on it and reply to its comments. Likes, comments, replies and new followers reach the account they are aimed at as notifications. Posts and comments are reported through Report feed post and Report feed comment.

Every route here except Receive Bunny Stream webhook requires a user session. A bot or OAuth2 bearer credential is refused with 403 ACCESS_DENIED, and a limited account is refused with 403 ACCOUNT_LIMITED by every route that writes. While the feature_feed limit of the caller is 0, every route that requires a session returns 403 FEATURE_TEMPORARILY_DISABLED.

A post is marked public when its author’s profile privacy level reads as EVERYONE at the time it is created; the server sets the mark and ignores the public field of the request. Posts marked earlier keep the mark, including those an earlier client marked while the author was at ALL_GUILDS. While the author’s level reads as EVERYONE, any account with a user session can read a public post by its ID, like it, comment on it and vote in its poll without the profile rule, unless either account blocks the other or the author is disabled or deleted. Once the level reads as anything else, the post falls back to the profile rule on the next request. The public post of an account flagged as a spammer keeps the profile rule. List user posts and List post likers keep the profile rule for every post. On a public post of another account, List comments leaves out the comments of accounts that block the caller or that the caller blocks. An account that reaches a public post only through its public flag needs more to write there: see Create comment and Vote in poll.

A video post is uploaded to Bunny Stream rather than through Upload post media. It needs a feature_feed_video limit other than 0 for the caller and the instance’s Bunny Stream settings; without either, Start video post and Re-sign video upload return 403 FEATURE_TEMPORARILY_DISABLED.

  1. Start video post returns a video upload ticket. It takes one post of the day at once, and an account has at most 3 videos on their way.
  2. Upload the file with TUS to upload.endpoint, sending upload.signature as AuthorizationSignature, upload.expires as AuthorizationExpire, id as VideoId and library_id as LibraryId. The signature lasts 2 hours; Re-sign video upload signs the same upload again within 6 hours of its start.
  3. Bunny Stream encodes the video and reports each step to Receive Bunny Stream webhook. Once it is encoded, the video is checked for explicit content through Bunny’s content tagging and frames of the video.
  4. A clean video becomes a post with the text given at the start, public when the author’s level read as EVERYONE at the start, and reaches home feeds and Discover like any post. No route shows the video before that.

A video that fails to encode, is stored larger than 500 MB, is refused by the check or is still not a post after 72 hours is deleted, and its author receives a system DM unless the account is disabled or deleted. List pending videos shows the caller’s videos on their way. A published video that Bunny later tags as adult, or whose upload is replaced, loses its post.

FieldTypeDescription
id1snowflakeThe post ID
authorpartial user objectThe author of the post
content?stringThe plain text of the post
attachments2array[message attachment object]The images of the post (at most 4); empty for a video post
edited_at?ISO8601 timestampWhen the post was last edited
like_countintegerHow many accounts like the post
comment_countintegerHow many comments and replies the post has
likedbooleanWhether the caller likes the post
publicbooleanWhether the post is public
poll?poll objectThe poll of the post, when it has one
link_preview?3link preview objectThe previewed link of the post, when it has one
video?4video objectThe video of a video post
view_count?integerHow many times the post was seen, a minute or two behind; absent while view counts are off
view_token?stringTicket for Record post views, for the caller only, valid 12 hours
author_following?booleanWhether the caller follows the author; only on Discover pages, and never on the caller’s own posts

1 Its snowflake timestamp is when the post was published. It is the before cursor of Get home feed and List user posts

2 Each url and proxy_url is a signed attachment URL

3 Resolved in the background after the post is published, so a new post can arrive without it

4 Absent while the instance has no Bunny Stream settings, so a video post then shows only its text

The URLs are signed for the request that returned them and name only this video’s files. They stay the same within each hour and work until expires_at, at least 2 hours after the request (longer for a video longer than 90 minutes, at most about 7 hours); read the post again for new ones.

FieldTypeDescription
idstringThe Bunny Stream ID (GUID) of the video
width?integerThe width of the picture as shown, in pixels
height?integerThe height of the picture as shown, in pixels
duration?integerThe length in seconds
poster_urlstringThe signed URL of the poster image
hls_urlstringThe signed URL of the HLS playlist
mp4_url1?stringThe signed URL of an MP4 file for players without HLS
expires_atISO8601 timestampWhen the signed URLs stop working

1 The 720p file, or the largest one below it; null when Bunny keeps no MP4 of the video

A poll takes one vote per account. A vote cannot be changed or taken back, and the author of the post cannot vote. Which account chose which option is never shown.

FieldTypeDescription
optionsarray[string]The choices, in order
expires_atISO8601 timestampWhen voting ends
votes1?array[integer]The votes for each option, in the order of options
voted?integerThe index of the caller’s option, or null when the caller has not voted

1 null until the caller votes, unless the caller wrote the post or voting has ended

FieldTypeDescription
optionsarray[string]The choices, 2-4, each 1-80 characters; two that match ignoring case and outer spaces are refused
duration_hoursintegerHow many hours the poll takes votes (1-168)

A post previews at most one link: the first link of its text to a YouTube video, a Twitch or Kick channel, video or clip, or an invite of this instance. Other links stay plain text. The unfurled card of a YouTube, Twitch or Kick link has its media through the Media Proxy; when it cannot be unfurled, the card holds only the site name. An invite is never fetched; read it through Get invite.

FieldTypeDescription
kindstringyoutube, twitch, kick or invite
urlstringThe link as written in the post
invite_code?stringThe invite code for invite; null for the other kinds
embed?embed objectThe card of the link; null for invite
FieldTypeDescription
id1snowflakeThe comment ID
post_idsnowflakeThe post the comment belongs to
authorpartial user objectThe author of the comment
contentstringThe plain text of the comment
edited_at?ISO8601 timestampWhen the comment was last edited; comments cannot be edited yet, so it is null
parent_id?2?snowflakeThe top-level comment a reply answers; null for a top-level comment
reply_count?3integerHow many replies a top-level comment has

1 Its snowflake timestamp is when the comment was written. It is the before cursor of List comments and List replies

2 A reply is a comment object too. Replies go one level deep: a reply cannot be answered

3 Present on top-level comments only

The actions of one type aimed at one account fold into one notification per post and clock hour (per clock hour for follows). A later action of the same hour updates the notification in place under the same id.

FieldTypeDescription
id1snowflakeThe notification ID
type5stringpost_like, post_comment, comment_reply or follow
post_id?snowflakeThe post liked or commented on; null for follow
comment_id2?snowflakeThe newest comment for post_comment, the comment the newest reply answers for comment_reply, else null
actors3array[partial user object]Up to three of the accounts behind it, the most recent first
actor_count4integerHow many accounts are behind it
updated_atISO8601 timestampWhen the newest action folded into it happened

1 Its snowflake timestamp is when its first action was folded in. It is the before cursor of List notifications

2 The comment may have been deleted since; the post then still lists its other comments

3 Holds at least one account. Accounts the recipient blocks are left out, and a notification with no other account is left out entirely

4 Counts each account once while it stays among the three newest; an account that undoes and repeats a like after three others may count again. Undoing a like or deleting a comment does not lower it

5 comment_reply goes to the author of a comment that receives a reply, and only while the instance’s feature_feed_reply_notifications limit is not 0. A client ignores a type it does not know

The caller’s notification read cursor and unread count, sent in READY as feed_notification_state. It is the same on all of the caller’s devices.

FieldTypeDescription
last_read_at?ISO8601 timestampThe read cursor that Mark notifications read moves; null before the first read
unread_count1integerNotifications updated after last_read_at (0-10)
unread_seq2integerThe unread sequence this count includes
unread_epoch3integerWhen that sequence began, in Unix milliseconds; 0 before it exists

1 Counted over the newest 20 notifications of the newest 20 days and capped at 10, so a client shows 9+ from 10. A notification whose post was deleted counts until the notifications are read

2 FEED_NOTIFICATION_CREATE carries newer values; the rise over the last value a client saw is the number of newly unread notifications

3 The sequence lives for a day after its last change (a newly unread notification, a read or a recount) and then starts again from 0 under a later epoch. A value under a later epoch than a client’s counts from 0; one under an earlier epoch is a late event and changes no count

One of the caller’s video posts on its way.

FieldTypeDescription
idstringThe Bunny Stream ID (GUID) of the video
statestringuploading, processing (Bunny is encoding it), scanning (it is being checked) or published
created_atISO8601 timestampWhen the upload started
post_id?snowflakeThe post the video became; null until it is published
FieldTypeDescription
idstringThe Bunny Stream ID (GUID) of the video, sent as the VideoId TUS header
library_idstringThe Bunny Stream library, sent as the LibraryId TUS header
uploadTUS upload objectWhat the TUS upload is signed with
FieldTypeDescription
endpointstringThe TUS endpoint to upload the file to
signaturestringThe AuthorizationSignature TUS header
expiresintegerThe AuthorizationExpire TUS header, in Unix seconds; 2 hours after the ticket was issued
FieldTypeDescription
idsnowflakeThe ID of a post the caller saw
tokenstringThe view_token that came with the post (at most 64 characters)

What Get feed suggestions returns.

FieldTypeDescription
users1array[partial user object]Up to five popular accounts for the caller to follow
guilds2array[discovery guild object]Up to five popular Discovery communities the caller is not in

1 Empty for a caller who may not follow strangers or is not an adult by date of birth

2 Empty while the instance has Discovery disabled

POST/v1/feed/attachments

Plans the upload of up to four post images and returns 200 with attachments, an array of singlepart attachment upload objects in request order. Each object echoes the id of its request item. Upload each file with one PUT to its upload_url, then pass the upload_filename in Create post. A video is not uploaded here; it starts at Start video post.

A filename must name a PNG, JPEG, WebP or GIF image, and a declared size may not exceed 10 MiB or the caller’s max_attachment_file_size limit when that is lower. The instance must allow presigned uploads.

FieldTypeDescription
attachmentsarray[attachment upload request item object]The images to upload (1-4)
StatusBodyCondition
200object with attachmentsThe upload URLs were issued
4001error responseThe body is invalid, a filename is not an image, or a size is too large
4032error responsePresigned uploads are off and the request returns FEATURE_TEMPORARILY_DISABLED

1 A filename that is not a PNG, JPEG, WebP or GIF returns INVALID_FORM_BODY with the field code ATTACHMENT_MUST_BE_IMAGE; a size over the limit returns FILE_SIZE_TOO_LARGE

2 Also returned for an MP4 or WebM filename while the caller’s feature_feed_video limit is 0, as before video posts moved to Bunny Stream; once it is not 0, such a filename returns 400 with ATTACHMENT_MUST_BE_IMAGE

10 requests per 10 seconds for each authenticated user, on the feed:attachment:upload bucket.

POST/v1/feed/posts

Publishes a post and returns 200 with the post object. A post needs text, media, or both. A poll needs text, which is its question, and takes no media.

Each image is checked like a message attachment, and more strictly: the media type sniffed from the uploaded bytes must be PNG, JPEG, WebP or GIF, the file may not exceed 10 MiB, explicit media is refused, and a file the media service cannot read is refused rather than kept as a plain file. An upload key is consumed by the post that uses it. A video post is made through Start video post instead.

The post is public, and can reach Discover, when the caller’s profile privacy level reads as EVERYONE; public in the body is ignored. The first link of the text that can be previewed becomes the post’s link preview.

An account can publish max_feed_posts_per_day posts a feed day (50 by default). Followers see the first feed_fanout_max_posts_per_day posts of each day (10 by default) in their home feed; later posts appear only in List user posts.

FieldTypeDescription
content??stringThe plain text of the post (1-2,000 characters)
attachments?array[pre-uploaded attachment object]Images uploaded through Upload post media (at most 4)
public?booleanIgnored; the server sets whether the post is public
poll?poll input objectA poll; needs content as its question and no attachments
StatusBodyCondition
200post objectThe post was published
4001error responseThe post is empty, an attachment was refused, or the poll is invalid
4033error responseThe text, a poll option or an image is blocked and the request returns CONTENT_BLOCKED
4292error responseThe caller already published max_feed_posts_per_day posts this feed day

1 An empty post, or a poll without text, returns CANNOT_SEND_EMPTY_MESSAGE. An image returns INVALID_FORM_BODY with the field code ATTACHMENT_MUST_BE_IMAGE for another sniffed type and UPLOADED_ATTACHMENT_NOT_FOUND for an unknown or used upload key, MEDIA_METADATA_ERROR when it cannot be read, FILE_SIZE_TOO_LARGE over the size limit, and EXPLICIT_CONTENT_CANNOT_BE_SENT for explicit media. An MP4 or WebM filename returns ATTACHMENT_MUST_BE_IMAGE too while the caller’s feature_feed_video limit is not 0. A poll with attachments returns the field code INVALID_FORMAT on poll, and repeated options the same code on poll.options

2 Returned as RATE_LIMITED, with retry_after counting down to the start of the next feed day

3 An MP4 or WebM filename while the caller’s feature_feed_video limit is 0 returns FEATURE_TEMPORARILY_DISABLED

5 requests per 10 seconds for each authenticated user, on the feed:post:create bucket.

POST/v1/feed/videos

Starts a video post and returns 200 with a video upload ticket object. The text is checked like message content now; the post is published with it once the video passes processing.

The video takes one of the caller’s max_feed_posts_per_day posts at once, and a cancelled or refused video does not give it back. An account has at most 3 videos uploading, processing or being checked at a time.

FieldTypeDescription
filenamestringThe name of the file (1-255 characters)
file_size1integerThe size of the file in bytes (1-524,288,000)
content_typestringvideo/mp4, video/webm, video/quicktime or video/x-matroska
content??stringThe plain text of the post (1-2,000 characters)
public?booleanIgnored; the server sets whether the post is public

1 Bunny’s stored copy is measured again once it arrives, and a video over 500 MB is then deleted

StatusBodyCondition
200video upload ticket objectThe video was started
400error responseThe body is invalid and the request returns INVALID_FORM_BODY
4031error responseVideo posts are off for the caller and the request returns FEATURE_TEMPORARILY_DISABLED
4292error response3 videos are on their way, or the caller already has max_feed_posts_per_day posts this feed day
502error responseBunny Stream did not create the video and the request returns BAD_GATEWAY

1 Off means a feature_feed_video limit of 0 or an instance without Bunny Stream settings. A blocked text returns CONTENT_BLOCKED

2 Returned as RATE_LIMITED; retry_after is 60 seconds for the videos on their way, and counts down to the start of the next feed day for the daily limit

3 requests per minute for each authenticated user, on the feed:video:create bucket.

POST/v1/feed/videos/{video_id}/signature

Returns 200 with a new video upload ticket object for one of the caller’s videos that is still uploading, for a TUS upload whose signature ran out. A video can be re-signed within 6 hours of its start.

FieldTypeDescription
video_idstringThe Bunny Stream ID (GUID) of the video
StatusBodyCondition
200video upload ticket objectThe upload was signed again
403error responseVideo posts are off for the caller and the request returns FEATURE_TEMPORARILY_DISABLED
4041error responseThe caller has no such video still uploading and the request returns NOT_FOUND
409error responseThe video is being changed by another request and the request returns CONFLICT

1 Also returned 6 hours after the start

10 requests per minute for each authenticated user, on the feed:video:sign bucket.

DELETE/v1/feed/videos/{video_id}

Cancels one of the caller’s videos that is not a post yet, deletes it from Bunny Stream and returns 204 with an empty body. A published video goes with its post through Delete post.

FieldTypeDescription
video_idstringThe Bunny Stream ID (GUID) of the video
StatusBodyCondition
204emptyThe video was cancelled
404error responseThe caller has no such video on its way and the request returns NOT_FOUND
409error responseThe video is being changed by another request and the request returns CONFLICT

10 requests per 10 seconds for each authenticated user, on the feed:video:cancel bucket.

GET/v1/feed/videos

Returns up to 20 of the caller’s pending video objects, newest first: the videos uploading, processing or being checked, and those published in the last hour with their post_id. A video that was refused or cancelled leaves the list.

StatusBodyCondition
200array of pending video objectsThe list was returned, possibly empty

10 requests per 10 seconds for each authenticated user, on the feed:video:list bucket.

POST/v1/feed/videos/webhookUnauthenticated

Accepts a Bunny Stream video status change and returns 200 with a webhook received object. The instance’s Bunny Stream library sends it; clients never call it. The signature headers are the credential, and the status is read again from Bunny before anything changes.

  • A body over 4 KiB returns 400 INVALID_REQUEST, and so does a signed body that is not the expected JSON.
  • A signature that is missing, of another version or algorithm, or not made over the raw body with the library’s read-only API key returns 401 UNAUTHORIZED.
  • An event of another library or with an unknown status is acknowledged and ignored.
  • One status of one video is handled once a minute; a repeat within the minute is acknowledged and ignored.
FieldTypeDescription
X-BunnyStream-SignaturestringLowercase hex HMAC-SHA256 of the raw body, keyed with the library’s read-only API key
X-BunnyStream-Signature-VersionstringThe signature version, which must be v1
X-BunnyStream-Signature-AlgorithmstringThe signature algorithm, which must be hmac-sha256
FieldTypeDescription
VideoLibraryIdintegerThe Bunny Stream library of the video
VideoGuidstringThe Bunny Stream ID (GUID) of the video
StatusintegerThe Bunny Stream video status
StatusBodyCondition
200webhook received objectThe event was authenticated and queued, or acknowledged and ignored
400error responseThe body is too large or malformed and the request returns INVALID_REQUEST
401error responseThe signature did not verify and the request returns UNAUTHORIZED
503error responseThe instance has no Bunny Stream settings and the request returns SERVICE_UNAVAILABLE

60,000 requests per minute for each client IP address, or for the authenticated user when a credential happens to be supplied, on the feed:video:webhook bucket. The bucket is exempt from the global bucket.

GET/v1/feed/posts/{post_id}

Returns 200 with one post object.

FieldTypeDescription
post_idsnowflakeThe ID of the post
StatusBodyCondition
200post objectThe post was returned
403error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4041error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

1 Also returned when the caller and the author block each other

60 requests per 10 seconds for each authenticated user, on the feed:post:get bucket.

DELETE/v1/feed/posts/{post_id}

Deletes one of the caller’s posts with its media, likes, poll votes, comments and replies and returns 204 with an empty body. Home feeds drop the post on their next read. The video of a video post is deleted from Bunny Stream in the background.

FieldTypeDescription
post_idsnowflakeThe ID of the post
StatusBodyCondition
204emptyThe post was deleted
403error responseAnother account wrote the post and the request returns MISSING_PERMISSIONS
404error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

10 requests per 10 seconds for each authenticated user, on the feed:post:delete bucket.

GET/v1/feed/home

Returns one page of the posts of the accounts the caller follows and of the caller, as an array of post objects with the newest post first. The home feed covers the last 30 days.

One request reads a bounded part of the home feed, so a page can hold fewer posts than limit, or none, while older posts remain.

The pagination signal is in the response headers. X-Next-Before is the before value for the next page, and it is absent when no older posts are known to remain. Read the next page with X-Next-Before as before when it is present, and otherwise with the last post’s id. An empty page without X-Next-Before is the end of the home feed. A client that follows empty pages on its own should wait between the requests and stop after a few in a row, since each one counts against the rate limit below.

Posts of an account the caller stops following, or blocks, or is blocked by, leave the home feed.

An account the caller follows shows all its posts while the caller sees its full profile under Get user profile. One the caller sees only as a public profile (for example one followed through a public Discover post with Follow user and post_id, or from the feed suggestions) shows only its public posts here, from the whole 30-day window, and only while its profile privacy level reads as EVERYONE; one whose profile is limited or refused shows none. Those public posts are left out while the author is flagged as a spammer or for suspicious activity, and while the caller’s feature_feed_discover limit is 0. As on Discover, a post hidden after reports, or one with images or a video while the instance’s feature_feed_discover_media or feature_feed_discover_video limit is 0, is left out too, and so is every post between accounts that block each other. A narrower privacy level of the author reaches the home feed on its next page, and a wider one within a minute.

FieldTypeDescription
before?snowflakeThe cursor selecting posts published before this post ID
limit?integerThe maximum number of posts to return (1-50, default 25)
StatusBodyCondition
200array of post objectsThe page was returned, possibly empty; X-Next-Before says where the next page starts

20 requests per 10 seconds for each authenticated user, on the feed:home bucket.

GET/v1/feed/discover

Returns one page of Discover, the public posts of the whole instance that drew the most reactions lately, as an array of post objects with the best post first. While the caller’s feature_feed_discover limit is 0, the route returns 403 FEATURE_TEMPORARILY_DISABLED.

Discover ranks the public posts of the last 72 hours by their likes, comments and poll votes from established accounts, a newer reaction weighing more than an older one, and spreads out the posts of one author. An author qualifies with a verified email address, an account at least 24 hours old (7 days for a post with a video) and a profile privacy level that reads as EVERYONE, and not while flagged as a spammer or for suspicious activity. While the instance’s feature_feed_discover_media limit is 0, posts with images are left out, and Twitch and Kick previews lose their images. While its feature_feed_discover_video limit is 0, video posts are left out. Discover holds no signed video URL; each page signs the videos it returns for the caller. A post that draws enough reports leaves Discover but stays readable through its ID. Posts of accounts that block the caller, or that the caller blocks, are left out.

The list holds up to 1,000 posts and is rebuilt about every 30 seconds, so the counts on a page can be about a minute old; liked, author_following and the poll state are the caller’s own at the time of the request.

The pagination signal is in the response headers. X-Next-Cursor is the cursor value for the next page, and it is absent at the end of the list. A page read after a rebuild continues at the same position in the new list, so a post can repeat or be skipped; drop the posts already shown.

FieldTypeDescription
cursor?stringThe X-Next-Cursor value of the previous page
limit?integerThe maximum number of posts to return (1-50, default 20)
StatusBodyCondition
200array of post objectsThe page was returned, possibly empty; X-Next-Cursor says where the next page starts
403error responseThe caller’s feature_feed_discover limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED

20 requests per 10 seconds for each authenticated user, on the feed:discover bucket.

GET/v1/feed/suggestions

Returns a feed suggestions object with up to five popular accounts for the caller to follow and up to five popular Discovery communities for the caller to join. While the caller’s feature_feed_suggestions limit is 0, the route returns 403 FEATURE_TEMPORARILY_DISABLED.

The instance keeps a list of up to 100 accounts and a list of up to 100 communities, and rebuilds both about once a minute. The account list takes the accounts with the most followers, each with at least as many as the feed_suggestions_min_followers limit of the default rule (at least 1), that qualify: not a bot or system account; a verified email address, a claimed account at least 24 hours old and the date of birth of an adult; not banned, deleted or flagged as a spammer or for suspicious activity; a feature_feed limit other than 0; a profile privacy level that reads as EVERYONE; and at least half of its 100 newest followers with a verified email address and a claimed, unflagged account at least 72 hours old. The community list takes the approved Discovery listings with the most members whose NSFW level is 0 and that lack the INVITES_DISABLED guild feature. It is empty while the instance has Discovery disabled or no search backend.

Each request draws up to 20 entries of each list at random and returns the first five that suit the caller, so two requests seldom return the same entries; keep one answer while the view stays open. users leaves out the caller, the accounts the caller follows and the accounts that block the caller or that the caller blocks. It stays empty unless the caller has a verified email address, a claimed account at least 24 hours old and no spammer or suspicious activity flag, and is an adult by date of birth; an account without a date of birth receives no accounts. guilds leaves out the communities the caller is in and does not depend on the caller’s standing.

A new ban, spammer or suspicious activity flag, or a deletion removes an account from the list at once. Any other change to a listed account or community reaches the lists within about a minute and a half. When the lists have not been rebuilt for 10 minutes, both arrays are empty. A listed account can be followed through Follow user without post_id, and a listed community joined through Join discovery guild.

StatusBodyCondition
200feed suggestions objectThe suggestions were returned, possibly with empty arrays
403error responseThe caller’s feature_feed_suggestions limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED

10 requests per 10 seconds for each authenticated user, on the feed:suggestions bucket.

GET/v1/users/{user_id}/posts

Returns one page of an account’s posts, as an array of post objects with the newest post first. A caller that sees the full profile under Get user profile gets every post, reads the next page with the last post’s id as before, and stops at an empty page; no X-Next-Before is sent.

A caller that sees the account only as a public profile gets its public posts only, and spends the daily stranger allowance described under Get user profile. One request reads up to three pages of the account’s posts to fill the page, so a page can come back short or empty while older public posts remain. X-Next-Before is then the before value for the next page; when it is absent, read the next page with the last post’s id, and an empty page without it is the end.

FieldTypeDescription
user_idsnowflakeThe ID of the author
FieldTypeDescription
before?snowflakeThe cursor selecting posts published before this post ID
limit?integerThe maximum number of posts to return (1-50, default 25)
StatusBodyCondition
200array of post objectsThe page was returned, possibly empty; X-Next-Before says where a public profile’s next page starts
403error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
404error responseNo account has the ID and the request returns UNKNOWN_USER

30 requests per 10 seconds for each authenticated user, on the feed:user_posts bucket.

PUT/v1/feed/posts/{post_id}/like

Likes a post and returns 204 with an empty body. Liking a post the caller already likes changes nothing. The post’s like_count counts each account once, and liked is true in the caller’s post objects.

FieldTypeDescription
post_idsnowflakeThe ID of the post
StatusBodyCondition
204emptyThe caller likes the post
403error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4041error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

1 Also returned when the caller and the author block each other

30 requests per 10 seconds for each authenticated user, on the feed:post:like bucket.

DELETE/v1/feed/posts/{post_id}/like

Takes the caller’s like off a post and returns 204 with an empty body. Without a like it changes nothing.

FieldTypeDescription
post_idsnowflakeThe ID of the post
StatusBodyCondition
204emptyThe caller does not like the post
403error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4041error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

1 Also returned when the caller and the author block each other

30 requests per 10 seconds for each authenticated user, on the feed:post:unlike bucket.

PUT/v1/feed/posts/{post_id}/poll/vote

Casts the caller’s vote for one option of a post’s poll and returns 200 with the post object, whose poll now shows the votes. An account votes once and cannot change or take back its vote, and the author of the post cannot vote. A vote also counts toward the post’s place in Discover, with a quarter of a like’s weight. On a public post, a caller who could not read the author’s posts without the public flag needs a verified email address to vote.

FieldTypeDescription
post_idsnowflakeThe ID of the post
FieldTypeDescription
optionintegerThe index of the chosen option in options (0-3)
StatusBodyCondition
200post objectThe vote was cast
4001error responseThe body is invalid, the post has no poll, or it has no option of the index
4032error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4043error responseNo post has the ID and the request returns UNKNOWN_FEED_POST
409error responseThe caller already voted or voting has ended, and the request returns CONFLICT

1 A post without a poll or an index past its options returns INVALID_FORM_BODY with the field code INVALID_FORMAT

2 The author of the post receives MISSING_PERMISSIONS. On a public post, a caller without a verified email address who could not read the author’s posts otherwise receives EMAIL_VERIFICATION_REQUIRED

3 Also returned when the caller and the author block each other

10 requests per 10 seconds for each authenticated user, on the feed:poll:vote bucket.

POST/v1/feed/views

Counts the posts the caller saw and returns 204 with an empty body. Each view carries the view_token of a post object the caller received. A ticket of another account, a forged or an expired ticket is dropped without an error, and a post named twice in a batch counts once. The caller passes one batch a minute, so an account adds at most one view to a post a minute. The views reach view_count a minute or two later. While the instance’s feature_feed_views limit is 0, the route returns 403 and post objects carry neither view_count nor view_token.

FieldTypeDescription
viewsarray[view input object]Posts seen since the last batch (1-100)
StatusBodyCondition
204emptyThe batch was taken; views with an invalid ticket were dropped
400error responseThe body is invalid or holds more than 100 views, and the request returns INVALID_FORM_BODY
403error responseThe instance’s feature_feed_views limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED
4291error responseThe caller’s batch of this minute was already taken, or the bucket is spent, and the request returns RATE_LIMITED

1 For the minute, retry_after counts down to its end. A batch in which no ticket is valid does not take the minute

6 requests per 60 seconds for each authenticated user, on the feed:views bucket, outside the global limit; one batch a minute passes.

GET/v1/feed/posts/{post_id}/likes

Returns one page of the accounts that like a post, as an array of partial user objects by ascending user ID. Read the next page with the last account’s id as after, and stop at a page shorter than limit.

FieldTypeDescription
post_idsnowflakeThe ID of the post
FieldTypeDescription
after?snowflakeThe cursor selecting accounts with a user ID above this one
limit?integerThe maximum number of accounts to return (1-100, default 50)
StatusBodyCondition
200array of partial user objectsThe page was returned, possibly empty
403error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4041error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

1 Also returned when the caller and the author block each other

20 requests per 10 seconds for each authenticated user, on the feed:post:likers bucket.

GET/v1/feed/posts/{post_id}/comments

Returns one page of the top-level comments on a post, as an array of comment objects with the newest comment first. Each carries its reply_count; read its replies through List replies. Read the next page with the last comment’s id as before, and stop at a page shorter than limit.

FieldTypeDescription
post_idsnowflakeThe ID of the post
FieldTypeDescription
before?snowflakeThe cursor selecting comments written before this comment ID
limit?integerThe maximum number of comments to return (1-50, default 25)
StatusBodyCondition
200array of comment objectsThe page was returned, possibly empty
403error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4041error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

1 Also returned when the caller and the author block each other

40 requests per 10 seconds for each authenticated user, on the feed:comments:list bucket.

POST/v1/feed/posts/{post_id}/comments

Adds a comment to a post and returns 200 with the comment object. The text is checked like message content. A post takes up to max_comments_per_post comments and replies together (10,000 by default); deleting a comment frees its place.

On a public post of another account, the caller writes at most 20 comments and replies together while the post is in its first 72 hours (72 hours from the caller’s first comment after that). A caller who could not read the author’s posts without the public flag also needs a verified email address, a claimed account and an account at least 24 hours old.

FieldTypeDescription
post_idsnowflakeThe ID of the post
FieldTypeDescription
contentstringThe plain text of the comment (1-1,000 characters)
StatusBodyCondition
200comment objectThe comment was added
4001error responseThe body is invalid or the post is full
4033error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4042error responseNo post has the ID and the request returns UNKNOWN_FEED_POST
4294error responseThe caller already wrote 20 comments and replies on this public post and the request returns RATE_LIMITED

1 A post with max_comments_per_post comments returns MAX_FEED_COMMENTS, with the limit in max_comments_per_post

2 Also returned when the caller and the author block each other

3 A blocked text returns CONTENT_BLOCKED. On a public post, a caller without a verified email address who could not read the author’s posts otherwise receives EMAIL_VERIFICATION_REQUIRED, and a claimed, verified account younger than 24 hours MISSING_ACCESS

4 retry_after is the time until the count resets

5 requests per 10 seconds for each authenticated user, on the feed:comment:create bucket, and 500 requests a day on the feed:comment:create:daily bucket.

DELETE/v1/feed/posts/{post_id}/comments/{comment_id}

Deletes a top-level comment with its replies and returns 204 with an empty body. The post’s comment_count drops by one plus the number of replies. The comment’s author and the post’s author may delete it, whatever blocks lie between them.

FieldTypeDescription
post_idsnowflakeThe ID of the post
comment_idsnowflakeThe ID of the comment
StatusBodyCondition
204emptyThe comment was deleted
403error responseThe caller wrote neither the comment nor the post and the request returns MISSING_PERMISSIONS
4041error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

1 A post with no comment of the ID returns UNKNOWN_FEED_COMMENT

10 requests per 10 seconds for each authenticated user, on the feed:comment:delete bucket.

GET/v1/feed/posts/{post_id}/comments/{comment_id}/replies

Returns one page of the replies to a top-level comment, as an array of comment objects with the newest reply first. Read the next page with the last reply’s id as before, and stop at a page shorter than limit. On a public post of another account, replies of accounts that block the caller or that the caller blocks are left out, as in List comments.

FieldTypeDescription
post_idsnowflakeThe ID of the post
comment_idsnowflakeThe ID of the top-level comment
FieldTypeDescription
before?snowflakeThe cursor selecting replies written before this reply ID
limit?integerThe maximum number of replies to return (1-50, default 25)
StatusBodyCondition
200array of comment objectsThe page was returned, possibly empty
403error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4041error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

1 Also returned when the caller and the author block each other. A post with no top-level comment of the ID returns UNKNOWN_FEED_COMMENT, and so does a reply ID

40 requests per 10 seconds for each authenticated user, on the feed:comments:list bucket, shared with List comments.

POST/v1/feed/posts/{post_id}/comments/{comment_id}/replies

Adds a reply to a top-level comment and returns 200 with the comment object of the reply. Replies go one level deep, so a reply cannot be answered. A reply follows the rules of Create comment: the text check, the access rules on a public post, the allowance of 20 comments and replies on another account’s public post, and max_comments_per_post, which counts comments and replies together. A reply raises the post’s comment_count and the comment’s reply_count by one.

The comment’s author receives a comment_reply notification while the instance’s feature_feed_reply_notifications limit is not 0.

FieldTypeDescription
post_idsnowflakeThe ID of the post
comment_idsnowflakeThe ID of the top-level comment
FieldTypeDescription
contentstringThe plain text of the reply (1-1,000 characters)
StatusBodyCondition
200comment objectThe reply was added
4001error responseThe body is invalid or the post is full
4033error responseThe caller may not read the author’s posts and the request returns MISSING_ACCESS
4042error responseNo post has the ID and the request returns UNKNOWN_FEED_POST
4294error responseThe caller already wrote 20 comments and replies on this public post and the request returns RATE_LIMITED

1 A post with max_comments_per_post comments and replies returns MAX_FEED_COMMENTS, with the limit in max_comments_per_post

2 Also returned when the caller and the author block each other. A post with no top-level comment of the ID returns UNKNOWN_FEED_COMMENT, and so does a reply ID

3 As for Create comment

4 retry_after is the time until the count resets

5 requests per 10 seconds for each authenticated user, on the feed:comment:create bucket, and 500 requests a day on the feed:comment:create:daily bucket, both shared with Create comment.

DELETE/v1/feed/posts/{post_id}/comments/{comment_id}/replies/{reply_id}

Deletes a reply and returns 204 with an empty body. The post’s comment_count and the comment’s reply_count drop by one. The reply’s author and the post’s author may delete it, whatever blocks lie between them.

FieldTypeDescription
post_idsnowflakeThe ID of the post
comment_idsnowflakeThe ID of the top-level comment the reply answers
reply_idsnowflakeThe ID of the reply
StatusBodyCondition
204emptyThe reply was deleted
403error responseThe caller wrote neither the reply nor the post and the request returns MISSING_PERMISSIONS
4041error responseNo post has the ID and the request returns UNKNOWN_FEED_POST

1 A comment with no reply of the ID returns UNKNOWN_FEED_COMMENT

10 requests per 10 seconds for each authenticated user, on the feed:comment:delete bucket, shared with Delete comment.

GET/v1/feed/notifications

Returns one page of the likes, comments, replies and follows aimed at the caller, as an array of notification objects with the newest id first. Notifications are kept for 90 days. Liking or commenting on your own post, or replying to your own comment, raises none.

A page can hold fewer notifications than limit while more remain. Read the next page with the last notification’s id as before, and stop at an empty page. Notifications of a deleted post are left out.

New and updated notifications also arrive as FEED_NOTIFICATION_CREATE on the caller’s sessions, at most one a minute; an update held back by that limit follows when the minute ends.

FieldTypeDescription
before?snowflakeThe cursor selecting notifications with an ID below this notification ID
limit?integerThe maximum number of notifications to return (1-50, default 25)
StatusBodyCondition
200array of notification objectsThe page was returned, possibly empty

20 requests per 10 seconds for each authenticated user, on the feed:notifications bucket.

POST/v1/feed/notifications/ack

Moves the caller’s notification read cursor to read_at and marks the notifications counted up to unread_seq read. The cursor only moves forward and never past the server’s time. A notification counted after the sequence the session sent stays unread, so a push that crossed the request is not lost. All of the caller’s sessions, this one included, receive FEED_NOTIFICATION_ACK with the response body.

FieldTypeDescription
read_at1ISO8601 timestampWhen the caller saw their notifications
unread_seqintegerThe newest unread sequence the session has seen (READY, pushes and acks), 0 or more
unread_epochintegerThe epoch of that sequence, 0 or more

1 A server time: the newest updated_at shown or the time the list was served, never the device clock

FieldTypeDescription
last_read_atISO8601 timestampThe read cursor after the request
unread_seq1integerThe unread sequence the read covers
unread_epochintegerThe epoch of the server’s current sequence

1 The greater of the sequence an earlier read covered and the lower of unread_seq and the server’s current sequence; unread_seq counts as 0 when unread_epoch is not the current epoch

StatusBodyCondition
200response bodyThe read was taken; a cursor that would move back stays where it was
400error responseA field is missing, read_at is not ISO8601 or a number is negative; returns INVALID_FORM_BODY
403error responseThe caller’s feature_feed limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED

10 requests per 10 seconds for each authenticated user, on the feed:notifications:ack bucket.

Deleting a post deletes its likes, poll votes, comments and replies, and its video from Bunny Stream. Deleting an account deletes its posts and their media, its videos that are not posts yet, its likes, poll votes, comments and replies, and its notifications, and the posts it liked, voted in or commented on stop counting them. Its data harvest lists in feed.json its posts, each with its text, timestamps, whether it is public, its poll and its media URLs, its comments and replies with their post and text (a reply with the parent_id of its comment), the posts it likes, its poll votes, and its notifications of the last 90 days with the IDs of their accounts. A video is listed by the URL of its HLS playlist, signed for the 7 days the archive can be downloaded, or null while the instance has no Bunny Stream settings.