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 and the page of the category it is filed under. 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.
Public posts
Section titled “Public posts”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.
Video posts
Section titled “Video posts”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.
- 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.
- Upload the file with TUS to
upload.endpoint, sendingupload.signatureasAuthorizationSignature,upload.expiresasAuthorizationExpire,idasVideoIdandlibrary_idasLibraryId. The signature lasts 2 hours; Re-sign video upload signs the same upload again within 6 hours of its start. - 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.
- A clean video becomes a post with the text given at the start, public when the author’s level read as
EVERYONEat 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.
Post object
Section titled “Post object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id1 | snowflake | The post ID |
| author | partial user object | The author of the post |
| content | ?string | The plain text of the post |
| attachments2 | array[message attachment object] | The images of the post (at most 4); empty for a video post |
| edited_at | ?ISO8601 timestamp | When the post was last edited |
| like_count | integer | How many accounts like the post |
| comment_count | integer | How many comments and replies the post has |
| liked | boolean | Whether the caller likes the post |
| public | boolean | Whether the post is public |
| poll? | poll object | The poll of the post, when it has one |
| link_preview?3 | link preview object | The previewed link of the post, when it has one |
| video?4 | video object | The video of a video post |
| view_count? | integer | How many times the post was seen, a minute or two behind; absent while view counts are off |
| view_token? | string | Ticket for Record post views, for the caller only, valid 12 hours |
| author_following? | boolean | Whether the caller follows the author; only on Discover, category and topic pages, and never on the caller’s own posts |
| category?5 | ?post category object | The category the post is filed under; absent when it has none |
| category_listed? | boolean | Only in the answer to Create post for a post filed under a category: whether that category’s page lists it |
| topic?6 | ?post topic object | The topic the post is filed under; absent when it has none |
| topic_listed? | boolean | Only in the answer to Create post for a post filed under a topic: whether that topic’s page lists it |
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
5 A category turned off later still names the post’s category; null only when the category is no longer known
6 An ended topic still names the post’s topic; null once an admin removed the topic
Video object
Section titled “Video object”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.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | string | The Bunny Stream ID (GUID) of the video |
| width | ?integer | The width of the picture as shown, in pixels |
| height | ?integer | The height of the picture as shown, in pixels |
| duration | ?integer | The length in seconds |
| poster_url | string | The signed URL of the poster image |
| hls_url | string | The signed URL of the HLS playlist |
| mp4_url1 | ?string | The signed URL of an MP4 file for players without HLS |
| expires_at | ISO8601 timestamp | When the signed URLs stop working |
1 The 720p file, or the largest one below it; null when Bunny keeps no MP4 of the video
Poll object
Section titled “Poll object”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.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| options | array[string] | The choices, in order |
| expires_at | ISO8601 timestamp | When voting ends |
| votes1 | ?array[integer] | The votes for each option, in the order of options |
| voted | ?integer | The 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
Poll input object
Section titled “Poll input object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| options | array[string] | The choices, 2-4, each 1-80 characters; two that match ignoring case and outer spaces are refused |
| duration_hours | integer | How many hours the poll takes votes (1-168) |
Link preview object
Section titled “Link preview object”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.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| kind | string | youtube, twitch, kick or invite |
| url | string | The link as written in the post |
| invite_code | ?string | The invite code for invite; null for the other kinds |
| embed | ?embed object | The card of the link; null for invite |
Comment object
Section titled “Comment object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id1 | snowflake | The comment ID |
| post_id | snowflake | The post the comment belongs to |
| author | partial user object | The author of the comment |
| content | string | The plain text of the comment |
| edited_at | ?ISO8601 timestamp | When the comment was last edited; comments cannot be edited yet, so it is null |
| parent_id?2 | ?snowflake | The top-level comment a reply answers; null for a top-level comment |
| reply_count?3 | integer | How many replies a top-level comment has |
| like_count? | integer | How many accounts like the comment or reply |
| liked? | boolean | Whether the caller likes the comment or reply |
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
Notification object
Section titled “Notification object”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.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id1 | snowflake | The notification ID |
| type5 | string | post_like, post_comment, comment_reply, comment_like or follow |
| post_id | ?snowflake | The post liked or commented on; null for follow |
| comment_id2 | ?snowflake | The newest comment for post_comment, the comment the newest reply answers for comment_reply, else null |
| actors3 | array[partial user object] | Up to three of the accounts behind it, the most recent first |
| actor_count4 | integer | How many accounts are behind it |
| updated_at | ISO8601 timestamp | When 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. comment_like goes to the author of a liked comment or reply, per post, and only while feature_feed_comment_like_notifications is not 0. A client ignores a type it does not know
Notification state object
Section titled “Notification state object”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.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| last_read_at | ?ISO8601 timestamp | The read cursor that Mark notifications read moves; null before the first read |
| unread_count1 | integer | Notifications updated after last_read_at (0-10) |
| unread_seq2 | integer | The unread sequence this count includes |
| unread_epoch3 | integer | When 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
Pending video object
Section titled “Pending video object”One of the caller’s video posts on its way.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | string | The Bunny Stream ID (GUID) of the video |
| state | string | uploading, processing (Bunny is encoding it), scanning (it is being checked) or published |
| created_at | ISO8601 timestamp | When the upload started |
| post_id | ?snowflake | The post the video became; null until it is published |
Video upload ticket object
Section titled “Video upload ticket object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | string | The Bunny Stream ID (GUID) of the video, sent as the VideoId TUS header |
| library_id | string | The Bunny Stream library, sent as the LibraryId TUS header |
| upload | TUS upload object | What the TUS upload is signed with |
TUS upload object
Section titled “TUS upload object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| endpoint | string | The TUS endpoint to upload the file to |
| signature | string | The AuthorizationSignature TUS header |
| expires | integer | The AuthorizationExpire TUS header, in Unix seconds; 2 hours after the ticket was issued |
View input object
Section titled “View input object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of a post the caller saw |
| token | string | The view_token that came with the post (at most 64 characters) |
| count? | integer | Views since the last batch, one per 3 seconds on screen; the server takes 1-20, and 1 when absent |
Feed categories
Section titled “Feed categories”The instance’s admins curate a short list of feed categories, each with a name, an emoji and the countries it is shown in. A post can be filed under one category when it is created, and a public post filed under one is listed on that category’s page. The caller’s country comes from the address of the request, never from the client, so a category shown only in one country can still be opened and posted to from anywhere; the country only decides what List feed categories offers. Members cannot rename categories, and categories are never deleted; an admin turns one off instead.
A post filed under a category is refused with 400 INVALID_FORM_BODY and the field code FEED_CATEGORY_PHRASE_BLOCKED on category_id when its text, a poll option or an image description or title holds a phrase the admins refuse in categories; the same post without the category can be published. A phrase matches from the start of a word, with Turkish letters folded to ASCII and case ignored; a phrase of five letters or more also matches when punctuation splits it inside a word (erd.oğan). The general content filter is checked first and answers 403 CONTENT_BLOCKED on its own. Posts without a category are not checked against that list. A video post is checked again when it is published: if its text holds a refused phrase by then, it is published without being listed on the category’s page.
While the caller’s feature_feed_categories limit is 0, the category routes return 403 FEATURE_TEMPORARILY_DISABLED and a category_id in a new post is ignored.
Category object
Section titled “Category object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the category |
| name | string | The name the admins gave the category, in the language of its countries |
| emoji | string | The emoji shown before the name |
Post category object
Section titled “Post category object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the category |
| name | string | The name of the category |
| emoji | string | The emoji shown before the name |
Feed topics
Section titled “Feed topics”The instance’s admins publish a few short lived topics a day, such as a match or a game patch, and members post under them. A system job proposes topics from fixtures, game and sports news headlines and the terms rising in the feed; the admins publish proposals by hand, while fixtures of the chosen leagues can publish on their own. Each topic belongs to one category, which gives it its countries and its refused phrases, and a post filed under a topic is filed under its category too. A topic takes posts while it is live, at most 72 hours from when it was published, and stays readable once it ends; an admin can remove one, which hides it and drops its name from posts. New topics are not pushed over the Gateway; List feed topics shows a new one within 30 seconds.
A post filed under a topic is refused with 400 INVALID_FORM_BODY on topic_id: with the field code INVALID_FORMAT when the topic is unknown, not live or its category is off, and with FEED_CATEGORY_PHRASE_BLOCKED when a text holds a phrase the admins refuse in categories, as for categories. While the caller’s feature_feed_topics limit is 0, the topic routes return 403 FEATURE_TEMPORARILY_DISABLED and a topic_id in a new post is ignored.
Topic object
Section titled “Topic object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the topic |
| title | string | The title (1-60 characters) |
| description | ?string | One sentence about the topic (1-140 characters) |
| image1 | ?string | The hash of the topic image |
| category | ?post category object | The category the topic and its posts are filed under; null when the category is no longer known |
| starts_at | ISO8601 timestamp | When the topic started taking posts |
| ends_at | ISO8601 timestamp | When the topic stops taking posts |
| status | string | live while it takes posts, ended after |
| post_count2 | integer | How many posts its page lists |
1 Served by the media proxy at branding/feed-topic-{topic_id}/{image}.webp
2 For display only; it can start again from 0 if the instance loses its cache
Post topic object
Section titled “Post topic object”Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the topic |
| title | string | The title of the topic |
Feed suggestions object
Section titled “Feed suggestions object”What Get feed suggestions returns.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| users1 | array[partial user object] | Up to five popular accounts for the caller to follow |
| guilds2 | array[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
Upload post media
Section titled “Upload post media”POST/v1/feed/attachmentsPlans 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.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| attachments | array[attachment upload request item object] | The images to upload (1-4) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | object with attachments | The upload URLs were issued |
| 4001 | error response | The body is invalid, a filename is not an image, or a size is too large |
| 4032 | error response | Presigned 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
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:attachment:upload bucket.
Create post
Section titled “Create post”POST/v1/feed/postsPublishes 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.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| content? | ?string | The 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? | boolean | Ignored; the server sets whether the post is public |
| poll? | poll input object | A poll; needs content as its question and no attachments |
| category_id?4 | ?snowflake | A category from List feed categories to file the post under |
| topic_id?5 | ?snowflake | A live topic from List feed topics to file the post under |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | post object | The post was published |
| 4001 | error response | The post is empty, an attachment or the poll is invalid, or the category or the topic refuses it |
| 4033 | error response | The text, a poll option or an image is blocked: CONTENT_BLOCKED |
| 4292 | error response | The 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
4 An unknown or disabled category returns INVALID_FORM_BODY with the field code INVALID_FORMAT on category_id, and a refused phrase the field code FEED_CATEGORY_PHRASE_BLOCKED on category_id. The post is listed on the category’s page only when it is public and its author qualifies as for Discover; any other post keeps the category on its object only, and the answer’s category_listed says which. Ignored while the caller’s feature_feed_categories limit is 0
5 The post is filed under the topic’s category too, and category_id is then ignored. A topic that is unknown, not live or whose category is off returns INVALID_FORM_BODY with the field code INVALID_FORMAT on topic_id, and a refused phrase FEED_CATEGORY_PHRASE_BLOCKED on topic_id. The topic’s page lists the post on the same terms as a category page, and the answer’s topic_listed says whether it does. Ignored while the caller’s feature_feed_topics limit is 0
Rate limit
Section titled “Rate limit”5 requests per 10 seconds for each authenticated user, on the feed:post:create bucket.
Start video post
Section titled “Start video post”POST/v1/feed/videosStarts 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.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| filename | string | The name of the file (1-255 characters) |
| file_size1 | integer | The size of the file in bytes (1-524,288,000) |
| content_type | string | video/mp4, video/webm, video/quicktime or video/x-matroska |
| content? | ?string | The plain text of the post (1-2,000 characters) |
| public? | boolean | Ignored; the server sets whether the post is public |
| category_id?2 | ?snowflake | A category from List feed categories the post is filed under once it is published |
| topic_id?3 | ?snowflake | A live topic from List feed topics the post is filed under once it is published |
1 Bunny’s stored copy is measured again once it arrives, and a video over 500 MB is then deleted
2 Checked as in Create post when the upload starts: an unknown or disabled category returns 400 INVALID_FORM_BODY with INVALID_FORMAT on category_id, and a refused phrase in the text the field code FEED_CATEGORY_PHRASE_BLOCKED on category_id
3 Checked as in Create post when the upload starts; the post is listed on the topic’s page when it is published, even if the topic ended in the meantime
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | video upload ticket object | The video was started |
| 400 | error response | The body is invalid and the request returns INVALID_FORM_BODY |
| 4031 | error response | Video posts are off for the caller and the request returns FEATURE_TEMPORARILY_DISABLED |
| 4292 | error response | 3 videos are on their way, or the caller already has max_feed_posts_per_day posts this feed day |
| 502 | error response | Bunny 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
Rate limit
Section titled “Rate limit”3 requests per minute for each authenticated user, on the feed:video:create bucket.
Re-sign video upload
Section titled “Re-sign video upload”POST/v1/feed/videos/{video_id}/signatureReturns 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| video_id | string | The Bunny Stream ID (GUID) of the video |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | video upload ticket object | The upload was signed again |
| 403 | error response | Video posts are off for the caller and the request returns FEATURE_TEMPORARILY_DISABLED |
| 4041 | error response | The caller has no such video still uploading and the request returns NOT_FOUND |
| 409 | error response | The video is being changed by another request and the request returns CONFLICT |
1 Also returned 6 hours after the start
Rate limit
Section titled “Rate limit”10 requests per minute for each authenticated user, on the feed:video:sign bucket.
Cancel video post
Section titled “Cancel video post”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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| video_id | string | The Bunny Stream ID (GUID) of the video |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The video was cancelled |
| 404 | error response | The caller has no such video on its way and the request returns NOT_FOUND |
| 409 | error response | The video is being changed by another request and the request returns CONFLICT |
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:video:cancel bucket.
List pending videos
Section titled “List pending videos”GET/v1/feed/videosReturns 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.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of pending video objects | The list was returned, possibly empty |
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:video:list bucket.
Receive Bunny Stream webhook
Section titled “Receive Bunny Stream webhook”POST/v1/feed/videos/webhookAccepts 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.
Limitations
Section titled “Limitations”- 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.
Request headers
Section titled “Request headers”| Field | Type | Description |
|---|---|---|
| X-BunnyStream-Signature | string | Lowercase hex HMAC-SHA256 of the raw body, keyed with the library’s read-only API key |
| X-BunnyStream-Signature-Version | string | The signature version, which must be v1 |
| X-BunnyStream-Signature-Algorithm | string | The signature algorithm, which must be hmac-sha256 |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| VideoLibraryId | integer | The Bunny Stream library of the video |
| VideoGuid | string | The Bunny Stream ID (GUID) of the video |
| Status | integer | The Bunny Stream video status |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | webhook received object | The event was authenticated and queued, or acknowledged and ignored |
| 400 | error response | The body is too large or malformed and the request returns INVALID_REQUEST |
| 401 | error response | The signature did not verify and the request returns UNAUTHORIZED |
| 503 | error response | The instance has no Bunny Stream settings and the request returns SERVICE_UNAVAILABLE |
Rate limit
Section titled “Rate limit”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 post
Section titled “Get post”GET/v1/feed/posts/{post_id}Returns 200 with one post object.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | post object | The post was returned |
| 403 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4041 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
1 Also returned when the caller and the author block each other
Rate limit
Section titled “Rate limit”60 requests per 10 seconds for each authenticated user, on the feed:post:get bucket.
Delete post
Section titled “Delete post”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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The post was deleted |
| 403 | error response | Another account wrote the post and the request returns MISSING_PERMISSIONS |
| 404 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:post:delete bucket.
Get home feed
Section titled “Get home feed”GET/v1/feed/homeReturns 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.
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| before? | snowflake | The cursor selecting posts published before this post ID |
| limit? | integer | The maximum number of posts to return (1-50, default 25) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of post objects | The page was returned, possibly empty; X-Next-Before says where the next page starts |
Rate limit
Section titled “Rate limit”20 requests per 10 seconds for each authenticated user, on the feed:home bucket.
Get Discover feed
Section titled “Get Discover feed”GET/v1/feed/discoverReturns 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. A post whose text, poll or link preview holds an invite link of this instance, a Discord invite or a link through a known link shortener never enters Discover; it stays in its author’s followers’ home feeds and on the author’s profile. 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.
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| cursor? | string | The X-Next-Cursor value of the previous page |
| limit? | integer | The maximum number of posts to return (1-50, default 20) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of post objects | The page was returned, possibly empty; X-Next-Cursor says where the next page starts |
| 403 | error response | The caller’s feature_feed_discover limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
Rate limit
Section titled “Rate limit”20 requests per 10 seconds for each authenticated user, on the feed:discover bucket.
List feed categories
Section titled “List feed categories”GET/v1/feed/categoriesReturns the enabled feed categories shown in the caller’s country, as an array of category objects ordered by the position the admins set. A category lists the countries it is shown in, or none to be shown everywhere. The country comes from the request address; when it is unknown, only the categories shown everywhere are returned.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of category objects | The categories were returned, possibly none |
| 403 | error response | The caller’s feature_feed_categories limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:categories bucket.
List category posts
Section titled “List category posts”GET/v1/feed/categories/{category_id}/postsReturns one page of the public posts filed under a category, as an array of post objects with the newest post first. The page covers the last 30 days.
A post enters the page when it is published, public, without an invite link, and its author qualifies as for Discover. Every page applies Discover’s rule again: a post that was deleted, hidden after reports, left out by the instance’s media limits, or whose author is no longer public or is flagged as a spammer or for suspicious activity, is left out, and so is every post between accounts that block each other. The newest page is shared by every reader and can be up to 5 seconds old.
The pagination signal is in the response headers, as in Get home feed. X-Next-Before is the before value for the next page; a page can hold fewer posts than limit, or none, while it is present. An empty page without it is the end. An unknown or disabled category returns an empty page without the header.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| category_id | snowflake | The ID of the category |
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| before? | snowflake | The cursor selecting posts published before this post ID |
| limit? | integer | The maximum number of posts to return (1-50, default 25) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of post objects | The page was returned, possibly empty; X-Next-Before says where the next page starts |
| 403 | error response | The caller’s feature_feed_categories limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
Rate limit
Section titled “Rate limit”20 requests per 10 seconds for each authenticated user, on the feed:category:posts bucket.
List feed topics
Section titled “List feed topics”GET/v1/feed/topicsReturns the live feed topics of the categories shown in the caller’s country, as an array of topic objects with the newest first. The country comes from the request address; when it is unknown, only the topics of categories shown everywhere are returned. A topic published elsewhere appears within 30 seconds.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of topic objects | The topics were returned, possibly none |
| 403 | error response | The caller’s feature_feed_topics limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:topics bucket.
Get feed topic
Section titled “Get feed topic”GET/v1/feed/topics/{topic_id}Returns a live or ended topic as a topic object. An ended topic is read only.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| topic_id | snowflake | The ID of the topic |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | topic object | The topic was returned |
| 403 | error response | The caller’s feature_feed_topics limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
| 404 | error response | The topic is unknown, removed or not started yet, and the request returns NOT_FOUND |
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:topics bucket.
List topic posts
Section titled “List topic posts”GET/v1/feed/topics/{topic_id}/postsReturns one page of the public posts filed under a topic, as an array of post objects. A post enters the page on the terms of a category page, and every page applies Discover’s rule again in the same way. sort=top orders posts by Discover’s trend score and pages by offset, down to the 1,000th post; sort=new puts the newest first and pages by before. The first page of each order is shared by every reader and can be up to 5 seconds old.
The pagination signal is in the response headers: X-Next-Offset is the offset value for the next top page and X-Next-Before the before value for the next new page. A page can hold fewer posts than limit, or none, while its header is present; an empty page without it is the end. An unknown, removed or not yet started topic returns an empty page without a header.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| topic_id | snowflake | The ID of the topic |
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| sort? | string | top (default) or new |
| before? | snowflake | new only: the cursor selecting posts published before this post ID |
| offset? | integer | top only: the posts to skip (0-1,000, default 0) |
| limit? | integer | The maximum number of posts to return (1-50, default 25) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of post objects | The page was returned, possibly empty; X-Next-Offset or X-Next-Before says where the next page starts |
| 403 | error response | The caller’s feature_feed_topics limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
Rate limit
Section titled “Rate limit”20 requests per 10 seconds for each authenticated user, on the feed:topic:posts bucket.
Get feed suggestions
Section titled “Get feed suggestions”GET/v1/feed/suggestionsReturns 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.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | feed suggestions object | The suggestions were returned, possibly with empty arrays |
| 403 | error response | The caller’s feature_feed_suggestions limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:suggestions bucket.
List user posts
Section titled “List user posts”GET/v1/users/{user_id}/postsReturns 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| user_id | snowflake | The ID of the author |
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| before? | snowflake | The cursor selecting posts published before this post ID |
| limit? | integer | The maximum number of posts to return (1-50, default 25) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of post objects | The page was returned, possibly empty; X-Next-Before says where a public profile’s next page starts |
| 403 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 404 | error response | No account has the ID and the request returns UNKNOWN_USER |
Rate limit
Section titled “Rate limit”30 requests per 10 seconds for each authenticated user, on the feed:user_posts bucket.
Like post
Section titled “Like post”PUT/v1/feed/posts/{post_id}/likeLikes 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The caller likes the post |
| 403 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4041 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
1 Also returned when the caller and the author block each other
Rate limit
Section titled “Rate limit”30 requests per 10 seconds for each authenticated user, on the feed:post:like bucket.
Unlike post
Section titled “Unlike post”DELETE/v1/feed/posts/{post_id}/likeTakes the caller’s like off a post and returns 204 with an empty body. Without a like it changes nothing. Only the caller’s own like is read: no access to the post is needed, and a block or a deleted post does not keep the like in place.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The caller does not like the post |
Rate limit
Section titled “Rate limit”30 requests per 10 seconds for each authenticated user, on the feed:post:unlike bucket.
Vote in poll
Section titled “Vote in poll”PUT/v1/feed/posts/{post_id}/poll/voteCasts 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| option | integer | The index of the chosen option in options (0-3) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | post object | The vote was cast |
| 4001 | error response | The body is invalid, the post has no poll, or it has no option of the index |
| 4032 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4043 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
| 409 | error response | The 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
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:poll:vote bucket.
Record post views
Section titled “Record post views”POST/v1/feed/viewsCounts 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. A view adds its count, held to 1-20 and to one view per 3 seconds since its view_token was handed out plus one, and a post named twice in a batch adds its larger count once. A batch adds at most 60 views in all, in its order. The caller passes one batch a minute, so an account adds at most 20 views 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.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| views | array[view input object] | Posts seen since the last batch (1-100) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The batch was taken; views with an invalid ticket were dropped |
| 400 | error response | The body is invalid or holds more than 100 views, and the request returns INVALID_FORM_BODY |
| 403 | error response | The instance’s feature_feed_views limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
| 4291 | error response | The 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
Rate limit
Section titled “Rate limit”6 requests per 60 seconds for each authenticated user, on the feed:views bucket, outside the global limit; one batch a minute passes.
List post likers
Section titled “List post likers”GET/v1/feed/posts/{post_id}/likesReturns 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| after? | snowflake | The cursor selecting accounts with a user ID above this one |
| limit? | integer | The maximum number of accounts to return (1-100, default 50) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of partial user objects | The page was returned, possibly empty |
| 403 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4041 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
1 Also returned when the caller and the author block each other
Rate limit
Section titled “Rate limit”20 requests per 10 seconds for each authenticated user, on the feed:post:likers bucket.
List comments
Section titled “List comments”GET/v1/feed/posts/{post_id}/commentsReturns 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| before? | snowflake | The cursor selecting comments written before this comment ID |
| limit? | integer | The maximum number of comments to return (1-50, default 25) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of comment objects | The page was returned, possibly empty |
| 403 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4041 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
1 Also returned when the caller and the author block each other
Rate limit
Section titled “Rate limit”40 requests per 10 seconds for each authenticated user, on the feed:comments:list bucket.
Create comment
Section titled “Create comment”POST/v1/feed/posts/{post_id}/commentsAdds 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| content | string | The plain text of the comment (1-1,000 characters) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | comment object | The comment was added |
| 4001 | error response | The body is invalid or the post is full |
| 4033 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4042 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
| 4294 | error response | The 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
Rate limit
Section titled “Rate limit”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 comment
Section titled “Delete comment”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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
| comment_id | snowflake | The ID of the comment |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The comment was deleted |
| 403 | error response | The caller wrote neither the comment nor the post and the request returns MISSING_PERMISSIONS |
| 4041 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
1 A post with no comment of the ID returns UNKNOWN_FEED_COMMENT
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:comment:delete bucket.
Like comment
Section titled “Like comment”PUT/v1/feed/posts/{post_id}/comments/{comment_id}/likeLikes a top-level comment and returns 204 with an empty body. Liking it again changes nothing. Its like_count counts each account once, and liked is true in the caller’s comment objects. The caller needs the access Like post needs; an author may like their own comment. The author of the top-level comment receives a comment_like notification while the instance’s feature_feed_comment_like_notifications limit is not 0; a like of one’s own comment notifies nobody.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
| comment_id | snowflake | The ID of the comment |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The caller likes the comment |
| 403 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4041 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
1 A post with no such comment returns UNKNOWN_FEED_COMMENT (a reply ID named as a comment too), and so does a comment whose author and the caller block each other
Rate limit
Section titled “Rate limit”30 requests per 10 seconds for each authenticated user, on the feed:comment:like bucket.
Unlike comment
Section titled “Unlike comment”DELETE/v1/feed/posts/{post_id}/comments/{comment_id}/likeTakes the caller’s like off a top-level comment and returns 204 with an empty body, also when the caller did not like it. Only the caller’s own like is read: no access, visibility or block check applies.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
| comment_id | snowflake | The ID of the comment |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The caller does not like the comment |
Rate limit
Section titled “Rate limit”30 requests per 10 seconds for each authenticated user, on the feed:comment:unlike bucket.
List replies
Section titled “List replies”GET/v1/feed/posts/{post_id}/comments/{comment_id}/repliesReturns 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
| comment_id | snowflake | The ID of the top-level comment |
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| before? | snowflake | The cursor selecting replies written before this reply ID |
| limit? | integer | The maximum number of replies to return (1-50, default 25) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of comment objects | The page was returned, possibly empty |
| 403 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4041 | error response | No 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
Rate limit
Section titled “Rate limit”40 requests per 10 seconds for each authenticated user, on the feed:comments:list bucket, shared with List comments.
Create reply
Section titled “Create reply”POST/v1/feed/posts/{post_id}/comments/{comment_id}/repliesAdds 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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
| comment_id | snowflake | The ID of the top-level comment |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| content | string | The plain text of the reply (1-1,000 characters) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | comment object | The reply was added |
| 4001 | error response | The body is invalid or the post is full |
| 4033 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4042 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
| 4294 | error response | The 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
Rate limit
Section titled “Rate limit”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 reply
Section titled “Delete reply”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.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
| comment_id | snowflake | The ID of the top-level comment the reply answers |
| reply_id | snowflake | The ID of the reply |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The reply was deleted |
| 403 | error response | The caller wrote neither the reply nor the post and the request returns MISSING_PERMISSIONS |
| 4041 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
1 A comment with no reply of the ID returns UNKNOWN_FEED_COMMENT
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:comment:delete bucket, shared with Delete comment.
Like reply
Section titled “Like reply”PUT/v1/feed/posts/{post_id}/comments/{comment_id}/replies/{reply_id}/likeLikes a reply and returns 204 with an empty body. Liking it again changes nothing. Its like_count counts each account once, and liked is true in the caller’s comment objects. The caller needs the access Like post needs; an author may like their own reply. The author of the reply receives a comment_like notification while the instance’s feature_feed_comment_like_notifications limit is not 0; a like of one’s own reply notifies nobody.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
| comment_id | snowflake | The ID of the top-level comment the reply answers |
| reply_id | snowflake | The ID of the reply |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The caller likes the reply |
| 403 | error response | The caller may not read the author’s posts and the request returns MISSING_ACCESS |
| 4041 | error response | No post has the ID and the request returns UNKNOWN_FEED_POST |
1 A post with no such reply returns UNKNOWN_FEED_COMMENT, and so does a reply whose author and the caller block each other
Rate limit
Section titled “Rate limit”30 requests per 10 seconds for each authenticated user, on the feed:comment:like bucket, shared with Like comment.
Unlike reply
Section titled “Unlike reply”DELETE/v1/feed/posts/{post_id}/comments/{comment_id}/replies/{reply_id}/likeTakes the caller’s like off a reply and returns 204 with an empty body, also when the caller did not like it. Only the caller’s own like is read: no access, visibility or block check applies.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| post_id | snowflake | The ID of the post |
| comment_id | snowflake | The ID of the top-level comment the reply answers |
| reply_id | snowflake | The ID of the reply |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The caller does not like the reply |
Rate limit
Section titled “Rate limit”30 requests per 10 seconds for each authenticated user, on the feed:comment:unlike bucket, shared with Unlike comment.
List notifications
Section titled “List notifications”GET/v1/feed/notificationsReturns 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.
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| before? | snowflake | The cursor selecting notifications with an ID below this notification ID |
| limit? | integer | The maximum number of notifications to return (1-50, default 25) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | array of notification objects | The page was returned, possibly empty |
Rate limit
Section titled “Rate limit”20 requests per 10 seconds for each authenticated user, on the feed:notifications bucket.
Mark notifications read
Section titled “Mark notifications read”POST/v1/feed/notifications/ackMoves 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.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| read_at1 | ISO8601 timestamp | When the caller saw their notifications |
| unread_seq | integer | The newest unread sequence the session has seen (READY, pushes and acks), 0 or more |
| unread_epoch | integer | The 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
Response body
Section titled “Response body”| Field | Type | Description |
|---|---|---|
| last_read_at | ISO8601 timestamp | The read cursor after the request |
| unread_seq1 | integer | The unread sequence the read covers |
| unread_epoch | integer | The 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
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | response body | The read was taken; a cursor that would move back stays where it was |
| 400 | error response | A field is missing, read_at is not ISO8601 or a number is negative; returns INVALID_FORM_BODY |
| 403 | error response | The caller’s feature_feed limit is 0 and the request returns FEATURE_TEMPORARILY_DISABLED |
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user, on the feed:notifications:ack bucket.
Account deletion
Section titled “Account deletion”Deleting a post deletes its likes, poll votes, comments and replies and their likes, and its video from Bunny Stream. Deleting an account deletes its posts and their media, its videos that are not posts yet, its likes, comment likes, poll votes, comments and replies, and its notifications, and the posts and comments 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, the comments and replies it likes (comment_likes, a reply with the parent_id of its comment), 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.