View all releases

API Release 1.0.0

Released on

v0.9.6 will continue to be supported as-is until further notice. The only change should be that user streams and app streams are no longer supported for v0.9.6. Streams will only work on v1 going forward.

All items below affect v1.0.0, and most do not affect v0.9.6. If it also affects v0.9.6, it will be marked with v0.9.6+.


E-mail Digest Notifications

From, you can now enable a configurable E-mail digest of some activity on Pnut!

It's fairly simple, currently, but will be expanded on in the future. It can be a nice supplement to other notifications, or an alternative to normal notifications.

New Channel Fields

  • recent_deleted_message
  • recent_deleted_message_id

Previously, channel.recent_message_id referenced the last message in a channel.

Now, recent_message_id will always reference the last non-deleted message in the channel. If there are no messages or only deleted messages in a channel, this field will not be set.

An additional field has been added to channels, called recent_deleted_message_id. This will only be included if the last message in a channel is a deleted message.

When handling stream markers for channels, you will generally not need to mind deleted messages more recent than the recent_message_id in a channel. If you make no change to your app, you will simply never have a recent_message or recent_message_id that is deleted.

  • created_at

Channels now include a created_at field like other objects. Because the date wasn't collected before now, previously created channels will have created_at retroactively set to the date when the first message was created in the channel. Channels without messages will have it set to "now".

GET /channels/search can now be searched by created_before and created_after.

  • has_sticky_messages

Channels now include has_sticky_messages boolean, indicating if there are sticky messages in the channel.

Chat Room Name on Messages in Streams

App and User streams now include the field meta.channel_name when messages are sent from channels of type.

Chat Room Rich Text Description

Chat room description is now handled similarly to Post, Message, and User content objects. Instead of a plain text string, raw items now have a parsed string for the room description, a whole content-like object will be embedded, with entities and rendered HTML alongside.

For a specific example, read the how-to.

Channel Explore Streams

  • GET /channels/streams/explore
  • GET /channels/streams/explore/{name}

New endpoints have been added to help users discover chat rooms ( to explore. These endpoints are just like the post explore streams, but for channels.

Initially, there are four streams listed:

  • Conversations - chat rooms with recent activity
  • New - rooms the user isn't subscribed to, ordered by creation
  • Topical - a list of rooms with specific topics to discuss (e.g., music, movies, politics)
  • Trending - popular chat rooms

The trending and conversations streams can currently be recreated through the channel search, but the others can't, and over time these will likely change and improve.

Changing Channel Owners

  • PUT /channels/{channel_id}/owner

A new endpoint has been introduced for changing the owner of a channel.

Message Replies Count

Messages now track how many replies there have been made against a message.

The message object includes counts.replies.

NSFW Reposts

When reposting a post, the repost can be marked as NSFW even when the original was not. Simply including JSON with is_nsfw: true when reposting will do.

Search Improvements

  • by Attached File or Poll

Posts, messages, polls, and channels can be searched by what files or polls are attached to them. This way, for example, when you are looking at a file, you can look up if there are any posts that reference the file in their raw data.

file_id and poll_id have been added as filters on various searches.

Additionally, file_kinds has been added as a search filter for messages and posts, to only return ones with an oEmbed-attached file of the specified kinds.

  • by Created At

All search endpoints can now search by created_before and created_after.

Polls Added to App Streams

The polls app stream type is now functional. The stream sends updates on creation, deletion, every user's first response to a poll, and when the poll closes. Because poll results can only be seen after the poll ends, updates only include the poll object without current results unless it is closed.

RSS Feeds Include Media

Now RSS feeds include OEmbed pictures and audio files in the RSS stream. The feed item descriptions will include HTML, which means in particular they will now include HTML links.

Breaking Changes

User Streams and App Streams disabled for v0 v0.9.6

User and App streams no longer work on v0 of the API. They are only available on v1.

All of the other 1.0 breaking changes on the streamed objects will have to be handled for notification servers, bots, and other projects using streams.

file, token, and follow User Stream Events Include Data

file, token, and follow events previously only included the meta field identifying the ID of the object that had been acted against. Now User Streams include the actual data from these events.

Files and tokens are included as the data object. When someone follows or unfollows someone, both of their user streams would receive a follow-type event with data in this format:

  "data": {
    "followed_user": "...user object...",
    "user": "...user object..."

Posts and Messages in User and App Streams

These objects come across the wire with data as a list, instead of a single message/post object.

Private Message ACL Changes

Private message channels no longer have an "owner" or "creator" of the channel. The owner used to be in its own owner field, and clients had to add that user to the list of users in the channel.acl.write.user_ids. Now all users in the channel will be included in channel.acl.write.user_ids, and the channel.user and channel.user_id will not be set at all.

This more accurately reflects the capabilities of the users in a private message, and removes a step or two from how clients parse them.

Poll Response Handling Changed

  1. The legacy poll response endpoint has been removed.

PUT /polls/{poll_id}/response/{position} has been removed in favor of PUT /polls/{poll_id}/response.

  1. Poll responses can be changed any time up until the poll closes.

An empty positions will indicate to clear any responses for the user, but they will still be notified by E-mail when the poll closes, if they have the option enabled on

  1. Poll responses cannot be viewed until after the poll closes.

  2. GET /users/me/polls/responses

This endpoint returns a list of limited poll objects embedded with the authenticated user's responses to the polls. This change is just aligning the embedded poll objects more with normal poll objects:

The field poll.poll_id has been renamed, and the field poll.created_at has been added in the response.

Raw Objects Reformatted

Raw objects had been a list of type-value pairs, like so:

    "type": "io.pnut.core.oembed",
    "value": {}
    "type": "arbitrary_raw_type",
    "value": {}

Now, there are lists of values for each type, like so:

  "io.pnut.core.oembed": [
  "arbitrary_raw_type": [

User Objects No Longer Overloaded

User objects are included on many objects. There are a handful of cases where the user will not be available to be included on an object. For example, if the user is deleted, blocked, or the call has specified not to include the user, with a ?include_user=0 parameter.

In most of these cases, the user field still exists on API v0, with a value of the user's ID, instead of including the whole user object.

On v1 of the API, when a user is not included, the user field will be absent and a separate user_id field will be included.

Inversely, when ?include_limited_user=1 is set on channel calls, the user_ids field will remain a list of the user IDs, and a separate users field will be included.

User "users" Count Removed

User objects no longer include counts.users.

Channel Owner Renamed

The channel owner field is now user, to be in line with all other API objects.

Message is_sticky Always Present

The field is_sticky is now always present on messages, even when false.

References to "link" Now "url"

These fields in the API have been changed from link to url:

  • on posts, messages, files, polls
  • the link in a link entity is now url (but entities.links is still "links")
  • post and message search terms links and link_domains are now urls and url_domains




  • GET /posts/explore link

Files (and derived_files)

  • link
  • link_short is now url_short
  • link_expires_at is now url_expires_at

Post Revision Now an Integer

post.revision, if included on the post, is now an integer instead of a string.

System Stats Endpoint Adjustment

  • GET /sys/stats

The data returned is no longer a child of a counts field. has been replaced with, listing the last 7 days' account totals that touched the API, not including the current day.

System Config Endpoint Adjustment

  • GET /sys/config


  • post.repost_max_length


  • file.audio_max_size_bytes
  • file.max_size_bytes
  • post.seconds_for_revision
  • user.name_max_length
  • user.presence_max_length


  • rate_limit.anonymous.seconds_between_reset is now "read_reset_seconds"
  • user.username_max_length was erroneously referring to the name max length, not username max length

Text Render Endpoint Adjustment

  • POST /text/process

The entities are now placed within an entities field, more like posts and other standard objects.

Search Changes

  • GET /channels/search

The field is_private has been removed from channel search, and instead is_public can be set to 1, 0, or not set at all.

  • GET /polls/search

The user_id field has been changed to creator_id on poll searches, to be consistent with other searches.

  • GET /posts/search

Searching posts without any parameters set will now return results. Previously, it would return an empty list, but now it is possible to effectively get a "Global" stream (with bot- and feed-type accounts included) from the search by not setting any filters or queries.

Also, if is_reply is set to 0 now, results will only include posts that are not replies. Previously, this only mattered if set to 1.

  • GET /users/search

The types field on user search has been renamed user_types like in other searches, for clarity and consistency.

User search no longer requires the q field, and can return unbounded results, then. For example, you can now look up "all bots" without a search term.

Minor Changes

"basic" Authentication Scope Always Included v0.9.6+

Whether your app requests the basic scope or not, it will now always be included if a user authorizes your app. This will not change what is actually allowed, but will add transparency around what your app can do when someone authorizes it.

More info has been fleshed out in the documentation for basic, as well as clarity in the rest of the scopes' functionality.

Error Responses

Many endpoints' error messages have improved when a field is populated with the wrong type. Where previously it would simply say Wrong Type, now usually it will say which field and type was wrong. Additionally, some resources, such as GET posts/{id}, will now return a 400 error instead of a 404, if the wrong type is used for the lookup.

Animated GIF Avatars

Now when uploading an animated GIF as an avatar, if it is uploaded as a square, it will retain its animation. However, the animation will be lost if requesting the avatar resized. Many clients request avatars be a specific size.

Accepted Actions Against Reposts

It used to be that reposting, replying to, or bookmarking a repost of a post would respond with a 400 error.

Now, these actions will automatically be done to the underlying post instead of erroring on the repost. It is still preferable for clients to act on the underlying post themselves, but this is smoothing out an issue newcomers may have to deal with.

E-mail Notifications for Follow, Repost, Bookmark

E-mail notifications for follows, reposts, and bookmarks used to require the user to have a Pnut Badge, but now anyone can use these notifications from

Informational Changes

New TLDs Recognized v0.9.6+

Domains with these new TLDs are now recognized: charity, cpa, gay, inc, llc, llp, sport, ss, موريتانيا, البحرين, 招聘, ລາວ, ευ, amazon, spa, アマゾン, 亚马逊.

Documentation Structure Reorganized

API documentation has been moved from /docs/api/* to /docs/*. For example, /docs/api/implementation/overview is now at /docs/implementation/overview.


Channel ACL Duplicates v0.9.6+

If a user existed in the ACLs already, moving the user to a lower-permission role would duplicate them instead of removing the higher permission. So a full-capable user would be in full and write if you moved them to write.

public_messages Scope Creep v0.9.6+

If messages scope was already authorized, public_messages could be authorized in a subsequent request, despite the overlap.

Updating User Badge v0.9.6+

Updating a user's badge through the API would fail.

Revised Post created_at Format v0.9.6+

The created_at field on previous versions of a post was YYYY-MM-DD 00:00:00-00 instead of YYYY-MM-DDT00:00:00Z.

Search Fixes

  • GET /channels/messages/search v0.9.6+

Searching channel messages by user ID and some ordering combinations would fail.

  • GET /polls/search

Poll raw items were not being indexed, so searching by raw_types did not work.

  • GET /users/search

User search documented an ?order= query parameter option, but it was not implemented!

Now order by id will occur if it is set, or if the q parameter is not set.

International Punycode Links v0.9.6+

Links like were not parsed as links, because the double dash (--) is otherwise not considered valid in a domain. Now the API recognizes it as an edge case to parse.

Error Handling of Multiple Messages or Posts by ID

  • GET /channels/messages
  • GET /posts

When retrieving multiple posts or messages, if no messages or posts in the given ids were valid or found, no data list was returned. Now, as long as the API returns 200 OK, data will be included, even if empty.

Additionally, GET /channels/messages used to return the error as data if there were an issue.