PocketPass Developer
api.pocketpass.xyz

Build with PocketPass

Connect PocketPass accounts to your app. With each person's permission, you can use their profile, friends, messages, Boards, activity, blocking settings and game progress.

PocketPass is a friend-finding and messaging app for handheld consoles and phones. The public API is a curated JSON API behind standard OAuth 2.1: register an app in the developer portal, send users through the consent flow, and call POST https://api.pocketpass.xyz/v1/<resource>.<action> with the token you get back.

Overview

Kotlin & JavaScript examples →

Everything runs against one host, https://api.pocketpass.xyz:

WhatWhere
Authorization endpointGET /auth/v1/oauth/authorize
Token endpointPOST /auth/v1/oauth/token
OpenID userinfoGET /auth/v1/oauth/userinfo
Discovery documentGET /auth/v1/.well-known/openid-configuration
PocketPass APIPOST /v1/<resource>.<action>
Media (avatars, attachments)/storage/v1/object/…
Realtime (push)wss://api.pocketpass.xyz/realtime/v1/websocket
Consent screen (shown to users)https://links.pocketpass.xyz/oauth/consent
Connected apps (users manage grants)https://links.pocketpass.xyz/oauth/apps

The flow in one paragraph: your app sends the user to the authorization endpoint with a PKCE challenge. PocketPass signs the user in (email code, Discord, or a username and password) and shows a consent screen listing your app and the permissions you registered. On approval the user is redirected back to you with a one-time code, which you exchange for an access token (valid one hour) and a refresh token. From then on you call the API with Authorization: Bearer <access token>.

A few things that make this API different from a generic backend:

  • Permissions are app-level. You choose the PocketPass scopes when you register the app; every user of your app grants the same set. The OAuth scope= parameter only covers OpenID claims (see Scopes).
  • Every request is a POST with a JSON object body, even reads, and every response is a JSON object. Errors are JSON objects too, with a stable code and hint.
  • The token only works on the public API. A connected app cannot reach the first-party endpoints PocketPass itself uses. Available actions are listed in the endpoint reference, Boards and blocking and privacy, plus media and Realtime with the same token and the matching scopes.
  • Users can disconnect you at any time and PocketPass can suspend an app. Your code needs to handle a token that suddenly stops working (see Tokens).

Getting started

Kotlin & JavaScript examples →

1. Create a PocketPass account

The portal signs you in with a normal PocketPass account — there is no separate developer login. If you do not have one yet, download PocketPass, create the account in the app (email code or Discord), then sign in at developer.pocketpass.xyz with the same email or Discord account.

2. Register an app

Every account can register up to five apps. An app has:

  • Name, description, website and logo URL — shown on the consent screen so users know who is asking. Names cannot contain "PocketPass". Website and logo must be https:// URLs.
  • Redirect URIs — where users are sent back after consent. Exact-match only; see Redirect URIs.
  • Client type — public or confidential (below). Fixed at registration.
  • Permissions — the PocketPass scopes your app needs. Pick the smallest set that works; adding a scope later disconnects your existing users (see Changing scopes).

Registration gives you a client id (a UUID) and, for confidential clients, a client secret that is shown exactly once. Copy it straight into your server's secret store. If you lose it, rotate it from the app page; the old secret stops working the moment you do.

3. Choose the right client type

TypeUse it forToken endpoint authentication
Public Websites and single-page apps, browser extensions, desktop and mobile apps, CLI tools — anything that ships to the user's device and therefore cannot keep a secret. None. PKCE protects the code exchange. Send client_id in the request body.
Confidential Server-side apps where the token exchange happens on a machine you control (a web backend, a bot, a worker). HTTP Basic with client_id as the username and the client secret as the password, on every token request. PKCE is still required.

When in doubt pick public: a confidential client that leaks its secret is worse than a public one that never had it. PKCE is mandatory for both.

4. Test it

The app page in the portal has a Test connect button. It generates a PKCE verifier and state in your browser, opens the consent flow for your first redirect URI in a new tab, and shows you the exact curl commands to exchange the code and call session.get. Use it before writing any code so you know what the redirect looks like.

Connecting a user

Kotlin & JavaScript examples →

The connect flow is a standard OAuth 2.1 authorization code flow with PKCE (S256). Requests without a PKCE challenge are refused at the gateway.

Step 1 — generate a verifier, a challenge and a state

Create 32 random bytes and base64url-encode them: that is your code_verifier. Hash it with SHA-256 and base64url-encode the digest: that is your code_challenge. Generate a second random string as state. Keep both the verifier and the state somewhere tied to this user's session (a cookie, sessionStorage, a row in your database) — you need them when the user comes back.

Step 2 — send the user to the authorization endpoint

The URL is https://api.pocketpass.xyz/auth/v1/oauth/authorize with client_id, a registered redirect_uri, response_type=code, scope, code_challenge, code_challenge_method=S256 and state.

Open this URL in the user's browser (a full navigation on the web, the system browser or a custom tab on mobile and desktop — never a web view you control). PocketPass redirects to the consent screen at https://links.pocketpass.xyz/oauth/consent, where the user signs in if needed and sees your app's name, website, description and the PocketPass permissions you registered. If the user already has a live grant for your app the screen is skipped and they land straight back on your redirect URI.

Step 3 — handle the redirect

On approval the browser lands on your redirect_uri with ?code=…&state=…. On denial it lands there with ?error=access_denied&state=…. Always check that state matches the value you stored before doing anything with the code.

Step 4 — exchange the code for tokens

Codes are single-use and expire ten minutes after they were issued. Send it to POST https://api.pocketpass.xyz/auth/v1/oauth/token as application/x-www-form-urlencoded (JSON with Content-Type: application/json is accepted too), with grant_type=authorization_code, the code, the same redirect_uri, the code_verifier and, for public clients, your client_id.

Confidential clients leave client_id out of the body and send Authorization: Basic base64(client_id:client_secret) instead. The response has access_token, token_type (bearer), expires_in (3600 seconds), refresh_token and id_token.

Store the access token and the refresh token per user. You can ignore id_token; the user id it carries is also returned by session.get and me.get, which is the reliable way to know who just connected.

Step 5 — check the connection

session.get needs no scope and returns the user id, your client id, the scopes the user granted and your remaining rate-limit budget. It is the right first call after every connect and a good health check afterwards.

Websites (public clients)

The API sends CORS headers, so a browser app can call /v1/… directly with the access token. Remember that anything in the browser is visible to the user, which is why a website is a public client.

Native apps (public clients)

Desktop and CLI apps listen on a loopback port and register http://127.0.0.1:<port>/callback as the redirect URI.

Mobile apps do the same with a custom scheme (com.example.app:/callback) or an https App Link / Universal Link, opened in the system browser or an in-app browser tab.

Servers (confidential clients)

The browser part is the same. Run the code exchange on your server, with the client secret from your secret store.

Redirect URIs

Kotlin & JavaScript examples →

The redirect_uri in the authorization request and in the token exchange must match one of the URIs registered for your app exactly — same scheme, host, port, path, and no extra query or fragment. Wildcards and prefix matching are not supported.

  • https URIs are allowed for any host.
  • Custom schemes (com.example.app:/callback) are allowed for native apps.
  • http is allowed only for localhost, 127.0.0.1 and [::1]. Because the match is exact, register the port you will actually listen on — http://127.0.0.1:8765/callback, not http://127.0.0.1/callback — and pin that port in your app. If you are unsure which name the platform will use, register both http://localhost:8765/callback and http://127.0.0.1:8765/callback.
  • Rejected: javascript:, data:, file:, blob:, about:, vbscript:; URIs with a fragment (#), a comma, whitespace or control characters; anything without a scheme and host.
  • At most 10 URIs per app, each at most 2048 characters.

You can add and remove redirect URIs at any time from the app page; the change is live immediately and does not disconnect anyone.

Scopes

Kotlin & JavaScript examples →

There are two kinds of scopes and it is important not to mix them up.

PocketPass scopes (chosen in the portal)

These decide what your app may do through /v1/…. You pick them when you register the app; they apply to every user of the app and are listed on the consent screen. They are not sent in the scope= parameter — the consent screen refuses a request whose scope= contains anything other than openid, email, profile or phone.

ScopeShown to the user asUnlocks
profile:readSee your profile (name, bio, avatar, age, country)me.get, profiles.get, profiles.get_many, avatar downloads
friends:readSee your friends list and friend requestsfriends.list, friends.requests_list, the friends: Realtime topic
friends:writeAdd and remove friends and answer friend requests as youfriends.request_send, friends.request_respond, friends.request_cancel, friends.remove, friends.code_get, friends.code_resolve
messages:readRead your conversations and messagesconversations.list, conversations.get, messages.list, attachment downloads, the conversation: Realtime topics
messages:writeSend, edit and delete messages as youconversations.open, conversations.mark_read, messages.send (text and images), messages.edit, messages.delete, image uploads
groups:writeCreate group chats and manage their members as youconversations.create, conversations.add_members, conversations.remove_member, conversations.leave, conversations.rename
notifications:readSee and clear your notificationsnotifications.list, notifications.mark_read, notifications.delete, the notifications: Realtime topic
presence:readSee when your friends were last online, who is online now and who is active in your chatsPresence on the friend-presence: topics (with friends:read) and the conversation: topics (with messages:read)
presence:writeShow you as online and typing to your friendsTracking and reading live Presence on those same topics; reading stored last_seen_at still requires presence:read
tokens:readSee your token balance and supporter statustokens.get, the tokens: Realtime topic
encounters:readSee the people you have met nearbyencounters.list, the encounters: Realtime topic
puzzles:readSee your Puzzle Swap progresspuzzles.get
piip:readSee your Piip's favourite colourpiips.get
boards:readRead public boards and your private boards, notes, drawings, members, invitations and activityBoard reads, stationery catalogue and boards: Realtime
boards:writePublish, edit and remove your notes and replies, react, report content and submit appeals as youParticipation and reporting
boards:membershipJoin and leave boards, accept invitations and ownership offers, request boards and change board notificationsMembership, proposals, notification preferences and read markers
boards:inviteInvite people to your boards and create or revoke invitation codes as youDirect invitations and codes
boards:manageChange your boards, artwork, moderators and ownership, and archive or reopen themOwner tools and artwork uploads
boards:moderateReview your boards' reports and retained content, manage membership requests, and mute or ban members as a board moderatorLocal moderator tools and explicit review of blocked content
boards:draftsRead, save and discard your private cloud note draftsCloud drafts and board-drafts: Realtime
boards:purchaseSpend your PocketPass tokens on permanent stationery purchasesboards.buy_stationery
blocks:readSee the accounts you have blockedblocks.list and blocks: Realtime
blocks:writeBlock and unblock accounts as youblocks.set
privacy:readSee whether you have Block Messages and Block Invites enabledprivacy.get and privacy: Realtime
privacy:writeEnable or disable Block Messages and Block Invites on your accountprivacy.set

session.get and session.revoke need no scope. Calling an endpoint your app does not have the scope for returns PT403 SCOPE_REQUIRED.

OpenID scopes (the scope= parameter)

The scope= parameter of the authorization request is OpenID Connect only. Send scope=openid. Add email, profile or phone only if you need those claims in an ID token or from /auth/v1/oauth/userinfo — each one is shown to the user as an extra thing you will see ("your email address", "your PocketPass username", "your phone number"). The consent screen refuses a request with any other scope, including PocketPass scopes. Email is not available through me.get or ordinary profile requests. For almost every app me.get gives you what you need without asking for OpenID claims.

OpenID scopeShown to the user asUserinfo claims
openid(always)sub — the PocketPass user id
emailyour email addressemail, email_verified
profileyour PocketPass usernamename and preferred_username (both the PocketPass username), updated_at, and user_metadata with only username and name
phoneyour phone numberphone, phone_verified

The profile scope never includes an email address, a picture or anything from Discord. An account that has not picked a username yet has name set to PocketPass user and no preferred_username. For the display name and Piip, use profiles.get or me.get.

Tokens

Kotlin & JavaScript examples →

Lifetimes

  • Access token: a JWT valid for one hour (expires_in: 3600). Send it as Authorization: Bearer …. Treat it as opaque; do not rely on its claims.
  • Refresh token: long-lived and rotating. Every refresh returns a new refresh token and invalidates the one you used. Always persist the newest pair before you use it; if two of your processes race to refresh the same token, the second one fails and you have to reconnect the user.
  • Authorization code: single-use, ten minutes.

Refreshing

Send grant_type=refresh_token and the refresh_token to the same token endpoint, with your client_id for a public client or HTTP Basic for a confidential one.

Refresh a little before expiry (for example when fewer than 60 seconds remain) or on the first 401, then retry the request once. A refresh that answers 400 with "error": "invalid_grant" means the grant is gone — the user disconnected your app, PocketPass suspended it, you changed the app's scopes, or the refresh token was already rotated. Drop the stored tokens and offer the user the connect flow again. invalid_client means your client id, secret or client type is wrong. While the user's account is banned, the token endpoint answers 403; see ACCOUNT_BANNED under Errors.

Revocation

Three things end a connection. API calls stop at the next request. A Realtime channel that is already open keeps receiving until its access token expires (at most one hour), and signed URLs work until they expire (at most 15 minutes).

  • The user disconnects you on https://links.pocketpass.xyz/oauth/apps. Existing access tokens answer PT401 CONSENT_REVOKED, refresh fails with invalid_grant.
  • Your app calls session.revoke (the "disconnect" button in your own UI). Same effect, scoped to that user. Discard the tokens afterwards; the call is idempotent, so retrying is safe.
  • PocketPass suspends the app. Every call answers PT403 APP_SUSPENDED and the token endpoint refuses the client.

Connected apps cannot use the account endpoints PocketPass itself uses: /auth/v1/logout, /auth/v1/user, multi-factor and grant-management endpoints all answer 403 for an app token. Disconnecting is POST /v1/session.revoke, nothing else.

Never put an access token in a URL, a log line or client-side analytics. If you suspect a token or secret leaked, rotate the secret in the portal (confidential clients) and have affected users disconnect and reconnect.

Calling the API

Kotlin & JavaScript examples →

Every endpoint is POST https://api.pocketpass.xyz/v1/<resource>.<action> with Authorization: Bearer <access token> and Content-Type: application/json headers and a JSON object body — {} when the endpoint takes no arguments.

Rules that hold everywhere:

  • Only POST (and OPTIONS for CORS preflight). A GET is a 404.
  • The body must be a JSON object; anything else, including invalid JSON, is PT400 INVALID_FIELD. Unknown keys are rejected with PT400 UNKNOWN_FIELD, so typos fail loudly instead of silently doing nothing.
  • Every response body is a JSON object. Successful responses are 200. Errors carry a 4xx/5xx status and a body { "code", "message", "hint" } — branch on hint, show message to a developer, never to an end user.
  • Lists return { "items": [...], "next_cursor": null | "…" }. Pass next_cursor back as cursor to get the next page; null means you have everything. Cursors are opaque strings — do not build or parse them.
  • limit is clamped to 1…100 and defaults to 50.
  • Single objects come wrapped in their resource name: { "profile": {…} }, { "conversation": {…} }, { "message": {…} }.
  • Timestamps are ISO 8601 with a timezone offset; ids are UUIDs.
  • An unknown path under /v1/, such as /v1/friends.nope, answers 404 with { "code": "PT404", "message": "Unknown endpoint", "hint": "UNKNOWN_ENDPOINT" }.

Endpoint reference

Kotlin & JavaScript examples →

Optional body keys are marked with ?. Shapes of the returned objects are in Objects.

EndpointScopeBodyReturns
session.get
Kotlin / JavaScript
none{} { user_id, client_id, app: { name }, scopes: [...], rate_limit: { per_minute, app_per_minute, per_second, remaining, reset_at } } — unread_total is included only with notifications:read. per_minute is the per-user limit, app_per_minute and per_second the app-wide limits; remaining is what is left of the tighter per-minute budget until reset_at
session.revoke
Kotlin / JavaScript
none{} { revoked: true | false } — disconnects your app for this user and deletes its sessions; false when there was nothing left to revoke. Discard the tokens afterwards.
me.get
Kotlin / JavaScript
profile:read{} { profile } — the connected user's own profile
profiles.get
Kotlin / JavaScript
profile:read{ user_id } { profile } — only the user themself, their friends and people they share an active conversation with; anyone else is PT404 PROFILE_NOT_FOUND. There is no directory lookup.
profiles.get_many
Kotlin / JavaScript
profile:read{ user_ids: [...] } (1…100) { items: [profile, ...] } — same visibility rule; ids the user may not see are simply left out
friends.list
Kotlin / JavaScript
friends:read{} { items: [friend, ...] } — the whole list, no paging
friends.requests_list
Kotlin / JavaScript
friends:read{} { incoming: [friend_request, ...], outgoing: [friend_request, ...] } — pending requests only, newest first
friends.request_send
Kotlin / JavaScript
friends:write{ user_id, client_operation_id } { request: friend_request } — if that person already asked, their pending request is returned instead (check requester.user_id). PT409 ALREADY_FRIENDS, PT403 BLOCKED, PT400 SELF_TARGET. Get user_id from friends.code_resolve or from a shared conversation; there is no search.
friends.request_respond
Kotlin / JavaScript
friends:write{ request_id, accept, client_operation_id } { request: friend_request } with status accepted or declined; accepting creates the friendship. Only the addressee may answer (PT403 NOT_ADDRESSEE); an answered request is PT409 REQUEST_CLOSED.
friends.request_cancel
Kotlin / JavaScript
friends:write{ request_id } { request: friend_request } with status cancelled; idempotent. Only the requester may cancel (PT403 NOT_REQUESTER); the other person's notification disappears.
friends.remove
Kotlin / JavaScript
friends:write{ user_id, client_operation_id } { removed: true | false } — false when they were not friends. Existing conversations stay readable; conversations.open needs a new friendship.
friends.code_get
Kotlin / JavaScript
friends:write{} { code } — the user's own 8-digit friend code, the same one PocketPass shows them
friends.code_resolve
Kotlin / JavaScript
friends:write{ code } { profile } of the person behind an 8-digit code, ready for friends.request_send. PT404 CODE_NOT_FOUND covers unused codes, the user's own code and people either side has blocked. Every lookup counts toward a limit of 50 per user per hour, including codes that don't exist; past it you get PT429 FRIEND_CODE_RATE_LIMITED with Retry-After in seconds.
conversations.list
Kotlin / JavaScript
messages:read{ limit?, cursor?, updated_after? } { items: [conversation, ...], next_cursor } — most recently updated first; updated_after (timestamp) keeps only conversations that changed since then
conversations.get
Kotlin / JavaScript
messages:read{ conversation_id } { conversation }; PT404 CONVERSATION_NOT_FOUND when the user is not a member
conversations.open
Kotlin / JavaScript
messages:write{ user_id, client_operation_id } { conversation } — the direct conversation with that user, created if needed. The two must be friends (PT403 NOT_FRIENDS) and not blocked (PT403 BLOCKED).
conversations.create
Kotlin / JavaScript
groups:write{ title, member_ids, client_operation_id } { conversation } — a new group owned by the user. title is 1…80 characters after trimming (PT400 TITLE_LENGTH); member_ids lists 1…19 friends of the user (PT400 MEMBERS_REQUIRED, PT403 NOT_FRIENDS, PT403 BLOCKED); groups hold at most 20 members (PT409 GROUP_FULL).
conversations.add_members
Kotlin / JavaScript
groups:write{ conversation_id, member_ids, client_operation_id } { conversation, added } — any member may add their own friends; people who left are re-added. added counts the members that actually joined. PT400 NOT_A_GROUP on a direct conversation.
conversations.remove_member
Kotlin / JavaScript
groups:write{ conversation_id, user_id, client_operation_id } { conversation, removed } — owner only (PT403 NOT_OWNER); use conversations.leave for the user themselves (PT400 SELF_TARGET). removed is false when that user was not a member.
conversations.leave
Kotlin / JavaScript
groups:write{ conversation_id, client_operation_id } { left: true } — the user leaves the group. If they owned it, the longest-standing member becomes the owner; a group nobody is left in is deleted with its messages.
conversations.rename
Kotlin / JavaScript
groups:write{ conversation_id, title, client_operation_id } { conversation } — owner only (PT403 NOT_OWNER); title is 1…80 characters after trimming.
conversations.mark_read
Kotlin / JavaScript
messages:write{ conversation_id } { last_read_at }
messages.list
Kotlin / JavaScript
messages:read{ conversation_id, limit?, cursor?, changed_since? } { items: [message, ...], next_cursor } — newest first. With changed_since (timestamp) you get every message created, edited or deleted after that instant, which is how you pick up edits and deletions.
messages.send
Kotlin / JavaScript
messages:write{ conversation_id, body?, client_operation_id, reply_to_id?, attachment? } { message } — body is 1…4000 characters after trimming (PT400 BODY_LENGTH) and may be omitted only with an attachment; attachment is { path, mime_type } of an image you uploaded first (see Media); client_operation_id is required, and a retry with it returns the message as it is now, even after an edit or deletion (see Idempotency)
messages.edit
Kotlin / JavaScript
messages:write{ message_id, body } { message } — own messages only (PT403 NOT_SENDER); deleted messages cannot be edited (PT409 MESSAGE_DELETED)
messages.delete
Kotlin / JavaScript
messages:write{ message_id } { message } with deleted_at set — own messages only
notifications.list
Kotlin / JavaScript
notifications:read{ limit?, cursor?, updated_after? } { items: [notification, ...], next_cursor } — most recently updated first; updated_after (timestamp) keeps only notifications updated after it
notifications.mark_read
Kotlin / JavaScript
notifications:read{ notification_id } { ok: true }
notifications.delete
Kotlin / JavaScript
notifications:read{ notification_id } { ok: true }
tokens.get
Kotlin / JavaScript
tokens:read{} { tokens: { balance, updated_at }, supporter: { active, active_until } } — balance is the current token count (updated_at is null until the user has ever earned or spent any); supporter.active says whether a supporter period is running and active_until when it ends, null for someone who never was one
encounters.list
Kotlin / JavaScript
encounters:read{ limit?, cursor?, updated_after? } { items: [encounter, ...], next_cursor } — most recently created or confirmed first; people the user has blocked, or who blocked them, are left out; updated_after is a strict "after" filter on updated_at
puzzles.get
Kotlin / JavaScript
puzzles:read{} { current_index, piece_price, pieces_owned_total, puzzles: [puzzle, ...] } — the Puzzle Swap collection in play order, the own-Piip puzzle first and then the artwork panels. Each puzzle has kind (own_piip or panel), puzzle_key, title, slug, image_path (a public file under /storage/v1/object/public/puzzle-panels/; null for the own Piip, which the app draws itself), columns, rows, total_pieces, owned_pieces (row-major indexes from 0), started_at, completed_at and completed_by_handover. current_index is null when every puzzle is complete or the user has not opened Puzzle Swap yet.
piips.get
Kotlin / JavaScript
piip:read{ user_id? } { user_id, favorite_color } — the favourite colour of the user's active Piip, which is also its shirt colour. favorite_color is { index, name, hex }: index is 0–11 in the Piip palette (Red, Orange, Yellow, Lime green, Green, Blue, Light blue, Pink, Purple, Brown, White, Black), name is that colour's English name and hex is its #RRGGBB value. It is null if the user has not made a Piip yet. Leave out user_id for the connected user. Other people follow the same rules as profiles.get: someone the user cannot see is PT404 PROFILE_NOT_FOUND.

Every endpoint checks, in this order: that the token is a valid app token (PT401 API_TOKEN_REQUIRED) for an account that is not banned (PT403 ACCOUNT_BANNED), the rate limit (PT429), that the app exists and is active (PT404 APP_NOT_FOUND, PT403 APP_SUSPENDED), that the app has the scope (PT403 SCOPE_REQUIRED) and that the user's grant is still live (PT401 CONSENT_REVOKED). Only then does it look at the body.

Boards

Kotlin & JavaScript examples →

Boards use the same bearer token and POST /v1/… convention as messages. Register the scopes your app needs and obtain fresh consent before using them. Existing connected apps do not gain Boards or blocking access automatically.

Public boards are readable by signed-in users; private boards require membership. Joining is required to publish, reply or react. OAuth scopes never replace these rules: owner actions require ownership, moderator actions require a local board role, and blocks, restrictions, archiving and the Boards feature switch are enforced on the server.

Requests and pagination

All mutations below require operation_id, a UUID generated once per user action and reused with the identical payload on retries. Other IDs are UUIDs except stationery_id, which is a catalogue string. Unknown fields are rejected. A field marked ? is optional; booleans must be JSON booleans. The JSON request limit is 600,000 bytes, except for artwork uploads.

Paginated reads accept limit (default 30, clamped to 1–100) and an opaque cursor. Pass the returned next_cursor unchanged with the same filters until it is null; a final empty page is possible. Cursors cannot be moved between endpoints or filters. Management and notices paginate each queue together. Reads always recheck current access, including when a cursor was issued before a membership or privacy change.

boards.list uses scope: "joined" by default; choose "explore" for the public directory, with an optional name search. Feed sort is "newest" (default), "activity" (new replies) or "popular" (Yeah count, then creation time). Popular period is "today", "week" (default) or "all". Replies are chronological.

EndpointScopeJSON fieldsResult / behaviour
boards.settings
Kotlin / JavaScript
boards:read{}Feature/request switches and your board push preference.
boards.list
Kotlin / JavaScript
boards:readscope?, search?, limit?, cursor?Board objects in items; joined by default, or explore for public boards. Returns next_cursor.
boards.get
Kotlin / JavaScript
boards:readboard_id, review?One board with your role, membership settings, member count, access revision and pending ownership offer.
boards.feed
Kotlin / JavaScript
boards:readboard_id, sort?, period?, limit?, cursor?, review?Top-level notes in items; newest, activity or popular ordering. Returns next_cursor.
boards.replies
Kotlin / JavaScript
boards:readpost_id, limit?, cursor?, review?The parent post and chronological replies in items. Returns next_cursor.
boards.post
Kotlin / JavaScript
boards:readpost_id, reveal?, review?One note or reply. reveal explicitly reveals a spoiler.
boards.preview
Kotlin / JavaScript
boards:readpost_id, reveal?, review?{ svg } static preview, or null while a spoiler is hidden.
boards.asset
Kotlin / JavaScript
boards:readasset_id{ mime, data } with base64 image bytes. Current audience is checked on every read.
boards.branding_preview
Kotlin / JavaScript
boards:readboard_id, kind{ svg } for the drawn icon or cover.
boards.drafts
Kotlin / JavaScript
boards:draftsboard_id?, limit?, cursor?Your current cloud draft heads in items, including conflicting versions. Returns next_cursor.
boards.inbox
Kotlin / JavaScript
boards:readlimit?, cursor?Grouped activity in items, with event counts and read markers; no note text. Returns next_cursor.
boards.invitations
Kotlin / JavaScript
boards:readlimit?, cursor?Your unaccepted direct invitations in items. Returns next_cursor.
boards.proposals
Kotlin / JavaScript
boards:readlimit?, cursor?Your board proposals and their decisions in items. Returns next_cursor.
boards.notices
Kotlin / JavaScript
boards:readlimit?, cursor?Your restrictions, actions and appeals arrays, each paginated with the same cursor. Returns next_cursor.
boards.members
Kotlin / JavaScript
boards:readboard_id, limit?, cursor?, review?Members in items, including display_name and avatar_path. Returns next_cursor.
boards.management
Kotlin / JavaScript
boards:moderateboard_id, limit?, cursor?Board, requests, invitations, restrictions, appeals and reports. Each queue uses the same cursor. Reports omit reporter identity. Returns next_cursor.
boards.outgoing_invitations
Kotlin / JavaScript
boards:inviteboard_id, limit?, cursor?Invitations in items. Members see only those they created; owner/moderators see all. No code hashes or reports. Returns next_cursor.
boards.history
Kotlin / JavaScript
boards:moderateboard_id, post_id?, limit?, cursor?Retained text/content versions in items, for board moderators only. Returns next_cursor.
boards.stationery
Kotlin / JavaScript
boards:readlimit?, cursor?Catalogue items with available and owned flags; includes permanently owned inactive items. Returns next_cursor.
boards.propose
Kotlin / JavaScript
boards:membershipid?, name, description?, rules?, visibility, operation_idSubmit a proposal for dashboard approval; returns id. Does not create a board immediately.
boards.join
Kotlin / JavaScript
boards:membershipboard_id, operation_idJoin a public board or request approval; returns board_id and status (joined or requested).
boards.accept_invitation
Kotlin / JavaScript
boards:membershipid, operation_idAccept a direct invitation; returns board_id and status.
boards.join_code
Kotlin / JavaScript
boards:membershipcode, operation_idJoin or request approval using a code; returns board_id and status.
boards.decline_invitation
Kotlin / JavaScript
boards:membershipboard_id, id, operation_idDecline your own direct invitation.
boards.decide_join
Kotlin / JavaScript
boards:moderateboard_id, user_id, approve, operation_idApprove or reject a membership request as a board moderator.
boards.invite
Kotlin / JavaScript
boards:inviteboard_id, user_id, id?, operation_idCreate a direct invitation; returns id. The recipient must accept.
boards.create_code
Kotlin / JavaScript
boards:inviteboard_id, id?, operation_idCreate a reusable invitation; returns id and the code. Save the returned code securely.
boards.revoke_invitation
Kotlin / JavaScript
boards:inviteboard_id, id, operation_idRevoke an invitation or code you are allowed to manage.
boards.leave
Kotlin / JavaScript
boards:membershipboard_id, operation_idLeave a board. An owner must first transfer ownership or archive it.
boards.preferences
Kotlin / JavaScript
boards:membershipboard_id, muted?, push_enabled?, operation_idChange your per-board mute and push settings.
boards.push_preference
Kotlin / JavaScript
boards:membershipenabled, operation_idChange your global Boards push preference.
boards.update_board
Kotlin / JavaScript
boards:manageboard_id, name?, description?, rules?, visibility?, accent?, join_policy?, code_policy?, members_can_invite?, operation_idUpdate board details, joining/invitation rules or branding accent as the owner.
boards.archive
Kotlin / JavaScript
boards:manageboard_id, archived, operation_idArchive or reopen your board.
boards.transfer
Kotlin / JavaScript
boards:manageboard_id, user_id, operation_idOffer ownership to a current member; they must accept.
boards.set_moderator
Kotlin / JavaScript
boards:manageboard_id, user_id, moderator, operation_idAppoint or remove a board moderator as the owner.
boards.accept_transfer
Kotlin / JavaScript
boards:membershipboard_id, operation_idAccept an ownership offer addressed to you.
boards.publish
Kotlin / JavaScript
boards:writeboard_id, id?, thread_id?, reply_to?, body?, drawing?, stationery_id?, spoiler?, draft_id?, draft_revision_id?, draft_base_id?, operation_idPublish a note or reply; returns id, board_id and thread_id. Drawings are immutable after publishing.
boards.edit
Kotlin / JavaScript
boards:writeboard_id, post_id, body, operation_idEdit your note or reply text; preserves drawing and adds an edited marker.
boards.delete_post
Kotlin / JavaScript
boards:writeboard_id, post_id, reason?, review?, operation_idRemove content while preserving a placeholder and replies. Moderating another author requires a reason and boards:moderate.
boards.spoiler
Kotlin / JavaScript
boards:writeboard_id, post_id, spoiler, reason?, review?, operation_idSet a spoiler flag. Moderating another author requires a reason and boards:moderate.
boards.react
Kotlin / JavaScript
boards:writeboard_id, post_id, yeah, operation_idSet or clear your Yeah reaction; repeated true values do not add reactions.
boards.draw_branding
Kotlin / JavaScript
boards:manageboard_id, kind, drawing, draft_id?, draft_revision_id?, draft_base_id?, operation_idSet a drawn icon or cover as the owner.
boards.save_draft
Kotlin / JavaScript
boards:draftsboard_id, draft_id, revision_id, base_id?, payload, operation_idSave an immutable revision; returns revision_id and conflict. Does not publish.
boards.discard_draft
Kotlin / JavaScript
boards:draftsrevision_id, operation_idDiscard one of your own draft revisions.
boards.report
Kotlin / JavaScript
boards:writeboard_id, post_id?, reason, id?, operation_idReport a note/reply, or board branding when post_id is omitted; returns case_id.
boards.resolve_report
Kotlin / JavaScript
boards:moderateboard_id, id, reason, status?, operation_idResolve or dismiss a local board report. Reports against board staff remain dashboard-only.
boards.restrict
Kotlin / JavaScript
boards:moderateboard_id, user_id, kind, reason, expires_at?, id?, operation_idIssue a board mute or ban; returns case_id. Cannot issue a global suspension.
boards.revoke_restriction
Kotlin / JavaScript
boards:moderateboard_id, id, operation_idRevoke a local board restriction. Central restrictions cannot be revoked here.
boards.appeal
Kotlin / JavaScript
boards:writeboard_id, case_id, reason, id?, operation_idAppeal your local board moderation case; returns case_id. Central appeals go through Discord.
boards.resolve_appeal
Kotlin / JavaScript
boards:moderateboard_id, id, reason, status?, operation_idResolve or dismiss a board appeal; revoking the restriction is a separate action.
boards.read_event
Kotlin / JavaScript
boards:membershipid, operation_idMark your activity event read.
boards.read_thread
Kotlin / JavaScript
boards:membershipboard_id, thread_id?, operation_idMark your activity in a thread read, or all activity for the board when thread_id is omitted.
boards.buy_stationery
Kotlin / JavaScript
boards:purchasestationery_id, operation_idBuy permanent stationery ownership with your tokens, including while subscribed; returns stationery_id and owned.

Notes, replies and spoilers

A note has text, one drawing, or both. Text and captions are limited to 1,000 characters; replies to 500. Use thread_id to reply to a top-level note and optional reply_to to identify a particular note in that thread. Imported images, GIFs and audio cannot be attached to board notes.

Note objects include id, board_id, author_id, author_name, author_avatar, thread_id, reply_to, body, drawing, a versioned stationery snapshot, spoiler, has_drawing, removed, creation/activity/edit timestamps, yeah, yeah_count, reply_count and mentions (the people mentioned, as user_id and name). Avatar downloads retain the existing profile:read requirement.

Spoilers return an empty body and no drawing, stationery or preview until the user explicitly reveals them with boards.post or boards.preview and reveal: true. Activity previews contain no note text. Removed notes remain as placeholders so replies stay readable. Published drawings cannot be edited.

Drawing format

A drawing is { "version": 1, "width": 800, "height": 600, "strokes": [...] }. Each stroke has pen, color, size and points as [x, y] pairs.

The paper is always white and 4:3. Pens are pixel, smooth and eraser; sizes are numbers from 1 to 40. Colours are any six-digit hex code such as #3379D6. Each stroke needs at least one point inside the 800×600 canvas. A drawing can have any number of strokes and points, up to 524,288 bytes of validated drawing JSON. Pixel pen drawings also have a detail budget; very dense ones are rejected. Unknown fields and external media are rejected. Render the strokes locally or use boards.preview for a static SVG generated by the server.

Draft recovery and publishing

boards.save_draft takes a persistent draft_id, a new revision_id, optional base_id for the revision being replaced, and a payload containing body, drawing, spoiler, stationery_id, thread_id, reply_to and/or branding_kind (icon or cover). Saving never publishes. Concurrent edits keep separate heads and return conflict: true; let the user choose what to keep.

Cloud drafts are private to their author, including when a board moderator or dashboard staff member reviews the board. Keep a local copy during failures. If BOARD_DRAFT_BASE_EXPIRED is returned, recover the attempted content as a new revision without a base rather than overwriting another device's head.

When the user chooses Publish, include draft_id, draft_revision_id and, when needed, draft_base_id along with the explicit content. This additionally requires boards:drafts. A successful publish retires only those matching revisions; other conflicts survive. A failure leaves the draft intact. Reuse the original operation ID and payload after an uncertain result.

Membership, moderation and notifications

Owners choose join_policy and code_policy as open or approval, plus members_can_invite. Direct invitations always require acceptance. Codes remain valid until revoked or their inviter loses access. Public boards can become private; private boards cannot become public. Archiving keeps the existing audience able to read while participation and invitations stop.

Board names allow 1–60 characters, descriptions 1,000 and rules 4,000. Accent values are blue, green, pink, purple, orange and teal. Staff word rules apply to new submissions and edits. A censoring rule replaces matches with ****; a blocking rule rejects the submission with its reason. Old content is not automatically rescanned.

Moderators can use review: true on supported reads to inspect blocked content for their own boards, requiring both boards:read and boards:moderate. Deleting or changing another author's spoiler flag requires both boards:write and boards:moderate, with a reason; use review: true when a block would otherwise hide it. Retained artwork also requires boards:moderate. Reviews and moderation actions are logged.

Restrictions use kind: "mute" or "ban", with optional ISO 8601 expires_at. Bans prevent participation and revoke private membership; public boards remain readable. Reports concerning an owner/moderator go to PocketPass staff. Report queues omit reporter identity. Resolutions use status: "resolved" (default) or "dismissed" and a reason. Removed content and previous versions follow the existing 30-day retention policy, extended for active cases.

boards.preferences controls per-board muted and push_enabled; boards.push_preference controls the separate global board push preference. New memberships receive in-app and push activity by default, excluding the person's own actions. Activity is grouped by thread. Each inbox item has a kind: mention, reply (to the person's note or reply), yeah, note, activity, or report for moderators. Connected apps use scoped Realtime events and boards.inbox; registering a third-party push device is not exposed.

Board artwork and stationery

For a drawn icon or cover, use boards.draw_branding. Imported branding uses POST /v1/boards.artwork_upload, with boards:manage and a JSON body of board_id, kind: "icon", one new operation_id per upload and image (base64 PNG, JPEG, WebP or GIF bytes, without a data URL prefix).

Use kind: "cover" for a cover. The input must be at most 10 MiB (14 MiB for the whole JSON request). The service checks ownership before decoding pixels, strips metadata, flattens animation, limits dimensions to 2048 pixels and stores a WebP up to 2 MiB. It rechecks consent, scope and board ownership before committing. Success returns { ok, board_id, asset_id }; fetch the bytes through boards.asset. Keep the same operation ID and exact source bytes on retries. Incomplete uploads never appear as board artwork.

Upload errors use the usual { code, message, hint } body:

  • 401 PT401 API_TOKEN_REQUIRED: the bearer token is missing or invalid.
  • 400 PT400 MISSING_FIELD: board_id, kind, operation_id or image is missing.
  • 400 PT400 UNKNOWN_FIELD: the body has any other field.
  • 400 PT400 INVALID_FIELD: the JSON, the base64 or the image is invalid.
  • 413 PT413 MEDIA_TOO_LARGE: the image or the request is too large.
  • 503 PT503 MEDIA_BUSY: the processor is busy. Retry later.
  • 403 PT403 BOARD_ACCESS_DENIED: ownership or permission was lost after the upload was prepared.
  • 503 PT503 MEDIA_UNCONFIRMED: the commit could not be confirmed. Retry with the same operation ID and bytes.

The usual API errors also apply, such as SCOPE_REQUIRED, API_RATE_LIMITED and ACCOUNT_BANNED.

boards.prepare_branding is the processor's scoped preparation endpoint: it accepts board_id, kind, operation_id and a lowercase SHA-256 source_hash, and returns { ticket }. A ticket cannot be committed by a connected app or reused across apps. Use boards.artwork_upload for a complete upload.

Stationery starts with plain paper. Catalogue entries include versioned artwork, access rules, token price, available and owned. Active supporters can use all active stationery and can still buy permanent ownership through boards.buy_stationery. A purchase requires boards:purchase and never charges twice on a retry. Owned items remain owned after rule changes or supporter expiry; published notes keep their original stationery snapshot. Reading token balances separately requires tokens:read.

Board errors and rate limits

Boards requests use the normal app/user API budget, plus shared per-account burst limits: 30 submissions/edits, 120 reaction changes and 10 invitations/reports per minute by default. Staff can change these limits. PT429 BOARD_RATE_LIMIT carries Retry-After. PT403 BOARDS_DISABLED means the emergency switch is off; stored data remains and boards.settings is still available. Do not discard drafts.

Other hints include BOARD_ACCESS_DENIED, BOARD_DRAFT_BASE_EXPIRED, BOARD_INVITATIONS_BLOCKED, INVALID_CURSOR, UNKNOWN_FIELD, MISSING_FIELD, INVALID_FIELD, ALREADY_EXISTS and DUPLICATE_OPERATION_ID. Display the supplied reason for a word-filter or moderation rejection. Artwork upload errors are listed under Board artwork and stationery.

Blocking and message privacy

Kotlin & JavaScript examples →

These settings belong to the connected user. They work independently of the Boards feature switch and apply to native PocketPass and connected apps alike.

EndpointScopeJSON bodyResponse
blocks.list
Kotlin / JavaScript
blocks:read{ limit?, cursor? }{ items: [{ user_id, display_name, avatar_path, blocked_at }], next_cursor }; newest first, default 30, maximum 100. An over-long or malformed cursor is PT400 INVALID_CURSOR.
blocks.set
Kotlin / JavaScript
blocks:write{ user_id, blocked, operation_id }{ user_id, blocked }
privacy.get
Kotlin / JavaScript
privacy:read{}{ block_messages, block_invites }
privacy.set
Kotlin / JavaScript
privacy:write{ block_messages?, block_invites?, operation_id }{ block_messages, block_invites } with both current values. Send at least one setting; sending neither is PT400 MISSING_FIELD.

blocked, block_messages and block_invites are booleans; user_id and operation_id are UUIDs. Generate an operation ID once per change and reuse it with identical fields on retry. The list shows only accounts this user has blocked, not who has blocked them.

Blocking removes the friendship and pending friend requests. Unblocking does not restore them. In Boards, each person's content is hidden from the other and direct replies, reactions and invitations between them are refused. Local moderator review is available only with the extra moderation scope and a valid board role.

Block Messages prevents incoming direct messages and edits, including from friends. Block Invites, a separate setting, prevents new friend requests, new group-chat additions by other people and direct board invitations. Existing group chats and board memberships remain; sending in an existing group and voluntarily joining with a board code still work. A refusal returns HTTP 403 with DIRECT_MESSAGES_BLOCKED, FRIEND_REQUESTS_BLOCKED, GROUP_MESSAGES_BLOCKED or BOARD_INVITATIONS_BLOCKED, so show the supplied explanation instead of retrying.

Objects

Kotlin & JavaScript examples →

profile

user_id
UUID. The same id appears as sender_id, in members lists and as sub in the id token.
username
Lowercase handle, 3…32 characters, unique.
display_name
What PocketPass shows in the UI.
bio
Up to 280 characters; may be empty.
avatar_path
Path inside the avatars bucket, or null. See Media.
age
Integer or null.
country_code
Two-letter ISO 3166-1 code or null.
created_at, updated_at
Timestamps.
setup_complete
Whether the user finished account setup in the app.

last_seen_at is conditionally included in full profile objects with presence:read; see Presence for visibility and timestamp semantics.

friend

A profile as above, plus friends_since — when the friendship was accepted.

friend_request

id
UUID; the same id appears as friend_request_id on notifications.
status
"pending", "accepted", "declined" or "cancelled".
requester, addressee
{ user_id, username, display_name, avatar_path }. The connected user is one of the two. While a request is pending, profiles.get works for the other person.
created_at, responded_at
Timestamps; responded_at is null while pending.

conversation

id
UUID.
kind
"direct" or "group". Do not assume exactly two members.
title
null for direct conversations — show the other member's display name. The group name otherwise.
members
Array of { user_id, display_name, avatar_path } for the active members, including the connected user; up to 20 for groups, in the order they joined (the owner first for groups).
last_message
The newest message object, or null.
unread_count
Messages the connected user has not read.
updated_at
Bumped when a message is sent and when a group is renamed or its membership changes. Edits and deletions do not bump it — see Keeping in sync.

message

id
UUID. For messages your app sent, it is derived from the client_operation_id.
conversation_id, sender_id
UUIDs.
body
The text. "Message deleted" once the message is deleted.
reply_to_id
Id of the message being replied to, or null.
attachment
{ path, mime_type } for an image attachment (path inside the message-media bucket), or null. An image-only message has the body "📷", which is what PocketPass itself sends.
created_at
Timestamp.
edited_at
Timestamp of the last edit, or null.
deleted_at
Timestamp when the sender deleted it, or null. Deleted messages stay in the list so you can remove them from your UI.

notification

id
UUID.
kind
"friend_request", "friend_accepted", "message" or "system".
title, body
Ready-to-display text.
actor
{ user_id, display_name, avatar_path } of the person who caused it, or null.
conversation_id
Set for "message" notifications and for the "system" notifications sent when the user is added to, removed from or handed a group.
event_count
How many events were folded into this notification (for example unread messages in one conversation).
created_at, updated_at, read_at
Timestamps; read_at is null until marked read.

A "friend_request" notification carries friend_request_id; answer it with friends.request_respond (needs friends:write) or leave it for the user to answer in PocketPass.

encounter

id
UUID; the same id arrives as encounter_id on the encounters: Realtime topic.
peer
The person the user met, as a profile. Strangers are not otherwise visible to your app: profiles.get on them answers PT404, so keep what the embedded object gives you.
occurred_at
When the two devices met.
created_at
When the first of the two reported it.
confirmed_at
When the other side reported it too, or null while only one side has. Confirmed encounters are the ones PocketPass counts.
updated_at
The later of created_at and confirmed_at; the value to pass back as updated_after.

Errors

Kotlin & JavaScript examples →

Every error is { "code": "PTxxx", "message": "…", "hint": "…" } with a matching HTTP status. The code tells you the class, the hint the exact reason. Both are stable; the message is not. This includes a missing or expired token, an unknown endpoint and a body that is not JSON.

Status / codeHintMeaningWhat to do
400 PT400MISSING_FIELDA required body key is absent, or privacy.set got neither setting.Fix the request.
INVALID_FIELDA key has the wrong type or value (not a UUID, not a timestamp, list too long…), or the body is not a JSON object.Fix the request.
UNKNOWN_FIELDThe body contains a key the endpoint does not know.Check the spelling against the reference.
INVALID_CURSORThe cursor was not one this endpoint issued.Restart from the first page.
BODY_LENGTHMessage body is empty or longer than 4000 characters after trimming.Validate before sending.
REPLY_TARGETreply_to_id is not a message of that conversation.Only reply to messages from the same messages.list.
SELF_TARGETA friend endpoint or conversations.remove_member was called with the user's own id.Hide the user from their own friend actions; use conversations.leave.
TITLE_LENGTHGroup title is empty or longer than 80 characters after trimming.Validate before sending.
MEMBERS_REQUIREDmember_ids held nobody but the user themselves.Pick at least one friend.
NOT_A_GROUPA group endpoint was called on a direct conversation.Only offer group actions when kind is "group".
INVALID_CODEA friend code that is not eight digits.Validate before sending.
UNSUPPORTED_MEDIAattachment.mime_type is not image/jpeg, image/png, image/webp or image/gif.Convert the image first.
INVALID_ATTACHMENT_PATHattachment.path is not <user id>/<conversation id>/<file> for this user and conversation.Upload to the documented path and send that exact path back.
401 PT401API_TOKEN_REQUIREDNo bearer token, an expired or invalid one, or a token that is not a connected-app token.Refresh and retry once; then reconnect.
CONSENT_REVOKEDThe user disconnected your app, or its scopes changed since they approved it.Delete the stored tokens and offer the connect flow again.
403 PT403APP_SUSPENDEDPocketPass suspended your app.Stop calling; contact PocketPass.
ACCOUNT_BANNEDThe user's PocketPass account is banned. Every call, Realtime join and upload fails while the ban lasts, and the token endpoint refuses new tokens with 403.Stop and show the user as signed out. Do not retry.
SCOPE_REQUIREDYour app was not registered with the scope this endpoint needs.Add the scope in the portal (this disconnects users) or stop calling the endpoint.
NOT_A_MEMBERThe user is not an active member of that conversation (including after being removed from a group).Refresh your conversation list.
BLOCKEDOne of the two users blocked the other. Blocks stop direct messaging, friend requests and adding someone to a group; messages inside a group are not block-gated.Show "unavailable".
DIRECT_MESSAGES_BLOCKEDThe other person has Block Messages on.Tell the user this person is not accepting messages. Do not retry.
GROUP_MESSAGES_BLOCKEDSomeone being added to a group has Block Invites on and is not accepting group invitations.Leave them out and tell the user. Do not retry.
FRIEND_REQUESTS_BLOCKEDThe person has Block Invites on and is not accepting friend requests.Tell the user. Do not retry.
BOARD_INVITATIONS_BLOCKEDThe person has Block Invites on and is not accepting Board invitations.Tell the user. Do not retry.
NOT_FRIENDSconversations.open, conversations.create or conversations.add_members with users who are not the user's friends.Only pick people from friends.list.
NOT_OWNERRenaming a group or removing a member without owning the group.Only offer rename/remove when the user is the first entry in members.
NOT_SENDEREditing or deleting someone else's message.Only offer edit/delete on messages whose sender_id is the user.
NOT_ADDRESSEEAnswering a friend request that was not sent to the user.Only offer accept/decline on incoming requests.
NOT_REQUESTERCancelling a friend request the user did not send.Only offer cancel on outgoing requests.
404 PT404APP_NOT_FOUNDThe app in the token no longer exists.The app was deleted; nothing to retry.
UNKNOWN_ENDPOINTNo such endpoint, such as /v1/friends.nope, or a method other than POST.Check the path against the reference.
PROFILE_NOT_FOUNDNo such user, or the connected user may not see them.Treat as unavailable.
CONVERSATION_NOT_FOUNDNo such conversation, or the user is not in it.Refresh your conversation list.
MESSAGE_NOT_FOUNDNo such message in a conversation the user can see.Refresh the message list.
NOTIFICATION_NOT_FOUNDNo such notification for this user.Refresh the notification list.
REQUEST_NOT_FOUNDNo such friend request involving the user.Refresh friends.requests_list.
CODE_NOT_FOUNDNo account the user may see uses that friend code.Tell the user to check the code.
ATTACHMENT_NOT_FOUNDattachment.path was never uploaded.Upload first, then send.
409 PT409DUPLICATE_OPERATION_IDA client_operation_id was reused with a different request.Generate a fresh id per user action; reuse it only for retries of the same action.
MESSAGE_DELETEDEditing a message that has been deleted.Remove it from your UI.
GROUP_FULLThe group already has, or would exceed, 20 members.Show the cap; let the owner remove someone first.
FRIEND_REQUEST_PENDINGThe notification is a friend request the user has not answered yet.Answer it with friends.request_respond or leave it in place.
ALREADY_FRIENDSfriends.request_send to someone who is already a friend.Refresh friends.list.
REQUEST_CLOSEDThe friend request was already accepted, declined or cancelled.Refresh friends.requests_list.
429 PT429API_RATE_LIMITEDToo many requests for this app + user (or for the app as a whole).Wait for the Retry-After seconds, then resume. See Rate limits.
FRIEND_CODE_RATE_LIMITEDMore than 50 friends.code_resolve lookups for this user in an hour. Every lookup counts, including codes that don't exist.Wait for the Retry-After seconds; do not resolve codes on every keystroke.
500 PT500INTERNALSomething failed on our side; the message is deliberately generic.Retry with backoff. If it persists, tell us the time and endpoint.

Errors without a code

  • Gateway rate limit: a 429 whose body has no code field (it looks like { "message": "API rate limit exceeded" }). This is the per-IP backstop of 1,000,000 requests a minute in front of the API, so you should not meet it; back off exactly as for PT429.
  • Token endpoint: OAuth errors use the standard shape { "error": "invalid_grant", "error_description": "…" }.
  • Blocked endpoints: an app token used on anything outside /v1/…, the OAuth token and userinfo endpoints, Storage and Realtime gets 403 with { "error": "oauth_client_forbidden", "error_description": "…" }.
  • Media: Storage answers with its own error objects (for example { "statusCode": "404", "error": "not_found", "message": "Object not found" }). A file the user may not see is a 404 or 400, never a 403.

Rate limits

Kotlin & JavaScript examples →

  • 120 requests per minute per app and user by default. This is the limit you will normally meet. It is a fixed window that resets at the start of each clock minute, keyed on your client id plus the user id.
  • A per-app ceiling of 600 requests per minute across all users and a burst cap of 100 requests per second per app by default. The burst cap answers PT429 with Retry-After: 1.
  • Need more? Open the app in the portal and use Request higher limits under Limits: say what the app does, how many users you expect and which limit you are hitting. Requests are reviewed by hand; an approval applies to the app immediately and the decision, with any note, shows on the same page. session.get always reports the limits that apply to your app.
  • Failed requests count. A PT400, a PT403, a PT401 CONSENT_REVOKED and even a call for a suspended app all consume budget, and denied calls are reported as "denied" in the portal's usage table. A retry loop against a broken token will rate-limit itself. Calls refused with API_TOKEN_REQUIRED or ACCOUNT_BANNED are stopped before the limit and don't count.
  • A PT429 response carries a Retry-After header in seconds. Every call that gets past the token check also returns RateLimit-Remaining and RateLimit-Reset (seconds until the minute resets) for the tighter of the two per-minute limits above. session.get returns rate_limit.remaining and rate_limit.reset_at for the current user so you can pace yourself before hitting the limit.
  • The gateway's per-IP backstop is 1,000,000 requests a minute with no hourly cap, so in practice the per-app and per-user limits above are the ones that apply. RateLimit-Remaining, RateLimit-Reset and Retry-After come only from PocketPass's own limits.

Media downloads and the token endpoint have their own limits (the token endpoint is 30 requests per minute per IP) and do not count against the API budget.

Keeping in sync

Kotlin & JavaScript examples →

Realtime pushes changes to you the moment they happen; use it whenever your app can hold a websocket open. Polling is the fallback for servers, for the seconds after a reconnect and for apps that cannot keep a socket. Two parameters make polling cheap:

  1. Poll conversations.list with updated_after set to the newest updated_at you have seen. The response contains only conversations that received a message since then, together with their last_message and unread_count. An empty items array is the common, cheap case.
  2. For the conversation that is open on screen, poll messages.list with changed_since set to the newest of created_at, edited_at and deleted_at you have seen. This returns new messages and messages that were edited or deleted after that instant, which updated_after on the conversation does not surface because edits and deletions do not bump a conversation's updated_at.
  3. With notifications:read, poll notifications.list with updated_after set to the newest updated_at you have seen.
  4. With encounters:read, poll encounters.list with updated_after set to the newest updated_at you have seen; a confirmation bumps it, so an encounter comes back once more when the other side confirms. tokens.get is one cheap call: re-fetch it on any tokens: event, or once a minute while a balance is on screen.

Recommended cadence: every 5 seconds while your app is in the foreground and a chat is visible, every 60 seconds in the background or when only the conversation list is showing. At 5 seconds a user with one open conversation uses 24 requests per minute — well inside the 120 per minute budget, with room for their own actions. Stop polling entirely when the window is hidden or the device is idle, and after a PT401, PT403 or PT429.

Keep the timestamps you pass back exactly as the API returned them (do not round to seconds). Because changed_since and updated_after are strict "after" comparisons, a message you already have will not be returned again, and a message with the same timestamp as your watermark is impossible in practice.

Catch-up pass. Items can change while you walk the pages of a list, and their timestamps come from when the change started. After a full walk, run the list again with updated_after set to the time the walk started minus one minute, and de-duplicate by id.

Unread state: call conversations.mark_read when the user actually sees the conversation, not on every poll — it is a write, it needs messages:write, and PocketPass uses it to clear the user's badge.

Realtime

Kotlin & JavaScript examples →

The same token opens PocketPass's Realtime websocket. Connected apps can join the following private Broadcast topics, each behind its matching scope. Presence scopes also allow seeing and tracking Presence:

TopicScopeEventsFires when
conversation:<conversation id>messages:readINSERT, UPDATE, DELETE, membership, conversation, chat_colourA message in that conversation is sent, edited or deleted (INSERT/UPDATE/DELETE, table: "messages"); someone is added, removed, leaves or becomes the owner (membership, a conversation_members row); the group is renamed (conversation); a member changes their chat bubble colour (chat_colour, payload { operation: "UPDATE", schema: "public", table: "profile_chat_colours", record: { user_id, conversation_id } }). The user must be an active member; re-fetch with conversations.get on the last three.
notifications:<user id>notifications:readINSERT, UPDATE, DELETE, ACCOUNT_BANNED, BOARDSA notification for the connected user is created, changes (marked read, folded, answered) or is removed. ACCOUNT_BANNED (payload {}; Realtime also adds an id) means the user's ban status just changed, on a ban and on an unban; while banned, every call fails with PT403 ACCOUNT_BANNED. BOARDS (payload {}) is the legacy Boards signal described below.
friends:<user id>friends:readINSERT, UPDATE, DELETEA friendship or a friend request involving the connected user changes. An UPDATE whose payload is only { user_id } (Realtime also adds an id) arrives when the user or a friend changes their name, bio, avatar, age or country, when the user changes their chat colour or privacy settings, and when a friend or someone with a pending friend request is banned or unbanned. Treat it as a signal to re-fetch friends.list or profiles.get.
tokens:<user id>tokens:readINSERT, UPDATE, supporter_statusThe connected user's token balance changes (table: "token_balances", the record carries balance) or their supporter status changes (supporter_status, payload { user_id, active_until }). Re-fetch with tokens.get.
encounters:<user id>encounters:readINSERT, UPDATEThe connected user meets someone (INSERT) or the other side confirms an encounter (UPDATE). The payload carries encounter_id, occurred_at and confirmed_at; re-fetch with encounters.list and updated_after.

Boards and privacy events

Topic (your own user ID only)ScopeEventRefresh
boards:<user id>boards:readBOARDSVisible feeds, memberships, invitations, proposals, notices and activity
board-drafts:<user id>boards:draftsDRAFTSYour cloud draft heads
blocks:<user id>blocks:readBLOCKSYour block list and visible content
privacy:<user id>privacy:readPRIVACYYour Block Messages and Block Invites settings

These four event payloads are empty objects. Coalesce rapid events and refetch through the scoped API, which rechecks current consent and access. Events never contain private board IDs, note text, draft contents or block-list identities. Clear cached private content after losing access; do not use a cached feed as proof of continued membership. The legacy notifications topic may also receive a generic BOARDS event for native-client compatibility; it grants no access to board data.

Presence

Always sort the two lowercase UUID user IDs. Do not always put the signed-in user first as friend-presence:me:friend: that only works when their ID sorts first. The reverse order is rejected as unauthorized. Use user IDs, not usernames or friend codes.

With presence:read, full profile objects include last_seen_at for yourself and, with friends:read, accepted friends with no block in either direction. Read it through friends.list, or me.get/profiles.get/profiles.get_many with their usual scopes. The key is omitted without permission, and is null if no activity has been recorded. It is an ISO 8601 timestamp of last recorded PocketPass app activity, not an exact disconnect time; a connected app tracking Presence does not update this timestamp. Use Realtime Presence for live online status. Refetch when needed; there is no last-seen broadcast event.

Presence is the ephemeral "who is here" state of a channel. With presence:read your app sees it on two kinds of topic it can already read: friend-presence:<lower user id>:<higher user id>, one per accepted friendship with the two ids sorted as strings (needs friends:read), tells whether that friend is online; conversation:<conversation id> (needs messages:read) tells who has the conversation open and who is typing. With presence:write your app may also track Presence there, which makes the connected user appear online to their friends and shows them as typing in a chat — the consent screen says so in as many words.

  • PocketPass itself tracks { user_id } on friend pairs and { user_id, active_conversation_id, is_typing } in conversations, keyed by the user id. Track the same shape so PocketPass and other apps understand you, and untrack or leave the channel when the user goes away.
  • Presence payloads come from clients and are not verified by the server: read the key and user_id as a hint, not as proof.
  • Joining a pair topic is refused unless the two users are friends right now; joining a conversation is refused unless the user is an active member. Tracking without presence:write is refused while the join itself succeeds.
  • Permissions are checked when a channel is joined and whenever you push a refreshed access_token, so a channel that is already open keeps working until the token expires (at most one hour) even after the user disconnects your app.

Connecting

Connect to wss://api.pocketpass.xyz/realtime/v1/websocket?apikey=<publishable key>&vsn=1.0.0. The publishable key is public and the same for every app: see https://developer.pocketpass.xyz/config.js. The user's access token goes into every join message, never into the URL.

Without a library, speak the Phoenix channel protocol over the socket: join, heartbeat every 30 seconds, and hand over each refreshed token before the previous one expires.

The join is answered with a phx_reply whose payload.status is "ok" or "error". Events then arrive as broadcast frames. Row events carry the database change; the other payloads are described above.

  • private: true is mandatory. A public channel with the same name joins happily and never receives anything.
  • The join is refused (a phx_reply with "status": "error") when your app lacks the scope, the user is not in that conversation, the topic belongs to someone else, or the user disconnected your app. Treat it like PT403 / PT401 on the API: do not retry in a loop.
  • record and old_record are the raw table rows, not the API objects, and their columns are not part of the API contract. The robust pattern is to treat an event as a signal and re-fetch through the API: messages.list with changed_since, conversations.list with updated_after, notifications.list with updated_after, friends.list / friends.requests_list, tokens.get, encounters.list with updated_after.
  • Send a heartbeat every 30 seconds or the server drops the socket. Access tokens last one hour: push the refreshed token with the access_token message, otherwise the channel closes when the old one expires.
  • Realtime does not count against the request budget, but it is a shared resource. Subscribe to the conversation on screen plus the user's notifications: topic, not to every conversation in the list; leave channels you no longer show. Message notification broadcasts contain a generic body. Refetch through notifications.list; message previews require both notifications:read and messages:read.
  • Presence needs presence:read, and presence:write to appear online. The app_updates topic stays first-party, and connected apps cannot send Broadcast messages of their own.
  • While disconnected, poll as described in Keeping in sync, and re-fetch once after every reconnect — nothing is replayed.

Media

Kotlin & JavaScript examples →

Avatars and image attachments are files in two private buckets, referenced by path: profile.avatar_path lives in avatars, message.attachment.path in message-media. The same token that calls the API can read them, subject to the same rules — an avatar is readable when the user could see that profile (profile:read), an attachment when the user is a member of its conversation (messages:read).

Direct download

GET https://api.pocketpass.xyz/storage/v1/object/authenticated/avatars/<avatar_path>, or message-media/<path>, with the bearer token.

Use this from a server or a native app. The response is the file itself with its Content-Type. Files you may not read come back as a 404 or a 400, never a 403.

Signed URLs for browsers

An <img> tag cannot send a bearer token, so ask for short-lived signed URLs instead. Batch up to a whole page of avatars in one call to POST /storage/v1/object/sign/avatars with { "expiresIn": 600, "paths": [...] }. Each result has path, signedURL and error.

Or sign one file with POST /storage/v1/object/sign/message-media/<attachment path> and { "expiresIn": 600 }.

signedURL is relative. Prefix it with https://api.pocketpass.xyz/storage/v1 before you use it. The full URL then works without any headers, in an <img> or anywhere else.

  • expiresIn is in seconds and is clamped to 15 minutes (900); ask for a larger value and you silently get 900. Keep it short — a minute or two for a screen you are rendering now is plenty — and re-sign when the user comes back.
  • Revocation caveat: a signed URL is a bearer credential for that one file. It keeps working until it expires even if the user disconnects your app, blocks the other person or the message is deleted in the meantime. That is the reason for the 15-minute cap; do not cache signed URLs beyond the screen that requested them and never store them.
  • Signing a file the user may not see returns an error for that path (batch) or a 400/404 (single). Treat it as "no image".

Sending images

Upload the file first, then send the message that references it. Both steps use the same token and need messages:write. Upload with POST /storage/v1/object/message-media/<user id>/<conversation id>/<file name> and the image's Content-Type, then call messages.send with a client_operation_id and attachment: { path, mime_type }.

  • The path is exactly <user id>/<conversation id>/<file name>: the connected user's id (user_id from session.get), a conversation they are an active member of, and a file name of 1…128 characters made of letters, digits, ., _ and - that starts with a letter or digit. Use a fresh UUID per message as the name, and keep the same path on retries.
  • An app can neither overwrite nor delete uploads. Uploading to the same name again gets a 409 from Storage (the HTTP status, or "statusCode": "409" in a 400 body): the file is already there. Continue to messages.send with the same client_operation_id.
  • image/jpeg, image/png, image/webp and image/gif up to 10 MiB. Storage refuses other types and larger files; messages.send refuses a mime_type outside that list with PT400 UNSUPPORTED_MEDIA.
  • The upload is refused (a 400 or 403 from Storage) for another user's folder, for a conversation the user is not in, without messages:write, and after the user disconnects your app. messages.send double-checks the path (PT400 INVALID_ATTACHMENT_PATH) and that the file exists (PT404 ATTACHMENT_NOT_FOUND).
  • Omit body for an image-only message; it is stored as "📷", which is what PocketPass itself sends. A caption of 1…4000 characters is kept as the body.
  • An uploaded file that never ends up in a message is simply unused; there is nothing to clean up.
  • Avatars cannot be changed by apps.

Changing scopes

Kotlin & JavaScript examples →

A user approved a specific list of permissions, so the rules are:

  • Adding a scope disconnects every connected user. The moment you save an app with a scope it did not have before, all live grants are revoked and all sessions for your app are deleted. Users stay disconnected until they go through the connect flow again and approve the new list. The portal warns you and tells you how many users are affected before it saves.
  • Removing a scope disconnects nobody. Calls that needed the removed scope start answering PT403 SCOPE_REQUIRED immediately; everything else keeps working.
  • Name, description, website, logo and redirect URIs can be changed freely.

From your code's point of view a scope expansion looks exactly like the user disconnecting you: API calls answer PT401 CONSENT_REVOKED, and refreshing answers 400 with "error": "invalid_grant". Handle both in one place — forget the stored tokens, mark the account as disconnected, and show a "Reconnect PocketPass" action that starts the flow again. Because the consent screen shows the new permissions, users see what changed. If your app has an audience, add the scope during a quiet hour and ship the client that needs it first, so the reconnect prompt makes sense when it appears.

Idempotency

Kotlin & JavaScript examples →

Every Boards mutation, artwork upload, blocks.set and privacy.set uses the field operation_id, with the same once-per-action retry rules below. Existing messaging/friendship endpoints retain client_operation_id.

Networks drop responses. To make retries safe, these endpoints require a client_operation_id: messages.send, conversations.open, conversations.create, conversations.add_members, conversations.remove_member, conversations.leave, conversations.rename, friends.request_send, friends.request_respond and friends.remove.

  • Generate a fresh UUID once per user action — when the user presses "Send", not when you build the HTTP request.
  • Keep it with the pending action and reuse the same id on every retry of that action, including after a restart if you queue outgoing messages.
  • A retry with the same id and the same content returns the same result: for conversations.open and conversations.create the same conversation; for the friend endpoints the same request or the same removed answer.
  • For messages.send a retry returns the message as it is now, also after it was edited or deleted, after the sender left or was removed from the chat, and after a block. Its id is derived from the operation id, so there is never a duplicate row.
  • The same id with different content is refused with PT409 DUPLICATE_OPERATION_ID. For messages.send that means the id was already used for a message with another conversation, reply target or content that has never been edited or deleted. That is a bug in the caller, not something to retry.

Retry on network failures and on PT500, PT429 (after Retry-After) and HTTP 502/503/504. Do not retry PT400, PT401, PT403, PT404 or PT409 — the answer will not change.

First-party only in v1

Kotlin & JavaScript examples →

The public API is deliberately a subset of what the PocketPass app can do. The following remain first-party for now and are not reachable with an app token, however the app was registered:

  • Recording and confirming encounters (listing them is encounters.list).
  • Changing the avatar and uploads other than message media or board branding.
  • Account settings other than the documented Block Messages, Block Invites and board preferences; email and Discord links; deleting the account; shop purchases other than board stationery; buying puzzle pieces; granting achievements; and Piip editing. Balances and Puzzle Swap progress remain readable through tokens.get and puzzles.get.
  • The app_updates Realtime topic, and sending Broadcast messages.
  • The standard Supabase surface: tables under /rest/v1/, first-party RPCs, GraphQL, /auth/v1/user. All of it answers 403 or 404 to an app token by design.

New capabilities arrive as new endpoints and new scopes, so existing apps keep working unchanged.

Kotlin & JavaScript examples

Open a topic, then choose a language. Examples do not execute here. Replace sample UUIDs and values with IDs returned by your API calls. Only run mutations after the corresponding user action. Kotlin examples use Android’s org.json (add that dependency on JVM) and blocking HTTP: call them on a background/IO thread, never the Android main thread. JavaScript examples use browser fetch and Web Crypto. Start with the shared HTTP helpers.

Browse Kotlin and JavaScript examples
Shared HTTP helpers and errors

Set currentAccessToken from the OAuth token response. Helpers throw structured errors and expose Retry-After. Refresh expired tokens through OAuth, obtain new consent for scope changes, and retry only transient failures; do not blindly retry permission or validation errors.

JavaScript
let currentAccessToken = "YOUR_USER_ACCESS_TOKEN";
class ApiError extends Error {
  constructor(status, data, retryAfter) {
    super(data.message || data.error_description || `HTTP ${status}`);
    this.status = status; this.data = data; this.retryAfter = retryAfter;
  }
}
async function requestJson(url, body, contentType = "application/json") {
  const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": contentType,
      ...(!new URL(url).pathname.startsWith("/auth/v1/oauth/") ? { Authorization: `Bearer ${currentAccessToken}` } : {}) },
    body: contentType === "application/json" ? JSON.stringify(body) : body.toString()
  });
  const text = await response.text();
  let data;
  try { data = text ? JSON.parse(text) : {}; }
  catch { data = { message: `HTTP ${response.status}: unexpected response` }; }
  if (!response.ok) throw new ApiError(response.status, data, response.headers.get("Retry-After"));
  return data;
}
async function api(endpoint, body = {}) {
  return requestJson(`https://api.pocketpass.xyz/v1/${endpoint}`, body);
}
const session = await api("session.get");
// Use session.scopes to decide which features to show.
Kotlin (Android / JVM)
import org.json.JSONObject
import org.json.JSONArray
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder

var currentAccessToken = "YOUR_USER_ACCESS_TOKEN"
class ApiError(val status: Int, val data: JSONObject, val retryAfter: String?) :
    Exception(data.optString("message", "HTTP $status"))

// Blocking: run on an IO/background thread. Do not log tokens or responses.
fun requestJson(url: String, body: String, contentType: String = "application/json"): JSONObject {
    val connection = URL(url).openConnection() as HttpURLConnection
    try {
        connection.requestMethod = "POST"
        connection.connectTimeout = 15000
        connection.readTimeout = 30000
        connection.instanceFollowRedirects = false
        connection.doOutput = true
        if (!URL(url).path.startsWith("/auth/v1/oauth/")) {
            connection.setRequestProperty("Authorization", "Bearer $currentAccessToken")
        }
        connection.setRequestProperty("Content-Type", contentType)
        connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
        val status = connection.responseCode
        val stream = if (status in 200..299) connection.inputStream else connection.errorStream
        val text = stream?.bufferedReader()?.use { it.readText() }.orEmpty()
        val data = try { JSONObject(text.ifBlank { "{}" }) }
            catch (_: Exception) { JSONObject().put("message", "HTTP $status: unexpected response") }
        if (status !in 200..299) throw ApiError(status, data, connection.getHeaderField("Retry-After"))
        return data
    } finally { connection.disconnect() }
}
fun api(endpoint: String, body: JSONObject = JSONObject()): JSONObject =
    requestJson("https://api.pocketpass.xyz/v1/$endpoint", body.toString())
fun form(values: Map<String, String>): String = values.entries.joinToString("&") {
    URLEncoder.encode(it.key, "UTF-8") + "=" + URLEncoder.encode(it.value, "UTF-8")
}
val session = api("session.get")
// Use session.getJSONArray("scopes") to decide which features to show.
OAuth PKCE: connect, verify the callback, exchange code

Use a registered redirect URI exactly. Persist state, verifier and redirect URI through the round trip, validate state before exchanging, and clear the pending attempt after success. On Android, open the authorization URL in the system browser and handle your registered app link/custom scheme; localhost is for desktop loopback listeners. Confidential client secrets belong only on a server.

JavaScript
const clientId = "YOUR_CLIENT_ID";
const redirectUri = "https://your-app.example/callback";
const base64url = bytes => btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const state = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
sessionStorage.setItem("pendingOAuth", JSON.stringify({ verifier, state, redirectUri }));
const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri,
  response_type: "code", scope: "openid", state, code_challenge: challenge, code_challenge_method: "S256" });
location.assign(`https://api.pocketpass.xyz/auth/v1/oauth/authorize?${params}`);

// In the callback page, using the shared requestJson helper:
const pending = JSON.parse(sessionStorage.getItem("pendingOAuth") || "null");
const callback = new URL(location.href);
if (!pending || callback.searchParams.get("state") !== pending.state) throw new Error("Invalid OAuth state");
if (callback.searchParams.has("error")) throw new Error(callback.searchParams.get("error"));
const code = callback.searchParams.get("code");
if (!code) throw new Error("Missing authorization code");
const tokens = await requestJson("https://api.pocketpass.xyz/auth/v1/oauth/token", new URLSearchParams({
  grant_type: "authorization_code", client_id: clientId, code,
  redirect_uri: pending.redirectUri, code_verifier: pending.verifier
}), "application/x-www-form-urlencoded");
currentAccessToken = tokens.access_token;
sessionStorage.removeItem("pendingOAuth");
// Store tokens according to your platform's secure-session design.
Kotlin (Android / JVM)
import java.security.SecureRandom
import java.security.MessageDigest
import java.util.Base64
import android.net.Uri

val clientId = "YOUR_CLIENT_ID"
val redirectUri = "yourapp://oauth/callback" // Register exactly this URI first.
fun randomUrlToken(): String = Base64.getUrlEncoder().withoutPadding().encodeToString(
    ByteArray(32).also { SecureRandom().nextBytes(it) })
val verifier = randomUrlToken()
val state = randomUrlToken()
val challenge = Base64.getUrlEncoder().withoutPadding().encodeToString(
    MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII)))
val authorizeUrl = "https://api.pocketpass.xyz/auth/v1/oauth/authorize?" + form(mapOf(
    "client_id" to clientId, "redirect_uri" to redirectUri, "response_type" to "code",
    "scope" to "openid", "state" to state, "code_challenge" to challenge, "code_challenge_method" to "S256"))
// Persist verifier/state/redirectUri privately, then launch authorizeUrl in the system browser.

// In your redirect handler, restore the persisted values first:
fun finishLogin(callbackUrl: String, expectedState: String, savedVerifier: String): JSONObject {
    val callback = Uri.parse(callbackUrl)
    val expected = Uri.parse(redirectUri)
    require(callback.scheme == expected.scheme && callback.authority == expected.authority && callback.path == expected.path)
    require(callback.getQueryParameter("state") == expectedState) { "Invalid OAuth state" }
    require(callback.getQueryParameter("error") == null) { "Authorization rejected" }
    val code = requireNotNull(callback.getQueryParameter("code"))
    return requestJson("https://api.pocketpass.xyz/auth/v1/oauth/token", form(mapOf(
        "grant_type" to "authorization_code", "client_id" to clientId, "code" to code,
        "redirect_uri" to redirectUri, "code_verifier" to savedVerifier)), "application/x-www-form-urlencoded")
}
// Run exchange on an IO thread; securely store returned tokens and clear pending state.
Refresh, disconnect and confidential clients

Serialize refreshes for each user and replace both stored tokens after a successful refresh. Never embed a confidential-client secret in a browser or mobile app. Confidential clients authenticate token requests with HTTP Basic from their trusted server; see the server-side example.

JavaScript
const refreshed = await requestJson("https://api.pocketpass.xyz/auth/v1/oauth/token",
  new URLSearchParams({ grant_type: "refresh_token", client_id: "YOUR_CLIENT_ID", refresh_token: savedRefreshToken }),
  "application/x-www-form-urlencoded");
currentAccessToken = refreshed.access_token;
// Atomically replace saved access and refresh tokens, then reauthenticate Realtime.
// When the user explicitly disconnects:
await api("session.revoke");
// Clear local tokens and leave all Realtime channels.
Kotlin (Android / JVM)
val refreshed = requestJson("https://api.pocketpass.xyz/auth/v1/oauth/token", form(mapOf(
    "grant_type" to "refresh_token", "client_id" to "YOUR_CLIENT_ID", "refresh_token" to savedRefreshToken
)), "application/x-www-form-urlencoded")
currentAccessToken = refreshed.getString("access_token")
// Atomically replace saved access and refresh tokens, then reauthenticate Realtime.
// When the user explicitly disconnects:
api("session.revoke")
// Clear local tokens and leave all Realtime channels.
Pagination and incremental sync

Treat cursors as opaque. Use the documented timestamp filters for incremental messages, conversations, notifications and encounters. After a full walk, run a catch-up pass with updated_after set to one minute before the walk started, and de-duplicate by id. Realtime is an invalidation signal: refetch through the scoped API and discard private caches when access is lost.

JavaScript
async function walk(endpoint, filters, byId) {
  let cursor = null;
  do {
    const page = await api(endpoint, { ...filters, limit: 50, ...(cursor ? { cursor } : {}) });
    for (const item of page.items) byId.set(item.id, item);
    cursor = page.next_cursor;
  } while (cursor);
}
const catchUpFrom = new Date(Date.now() - 60000).toISOString();
const conversations = new Map();
await walk("conversations.list", {}, conversations);
await walk("conversations.list", { updated_after: catchUpFrom }, conversations);
for (const conversation of conversations.values()) renderConversation(conversation);
// For message edits/deletions since your saved watermark:
const changes = await api("messages.list", { conversation_id: conversationId, changed_since: savedWatermark });
// Continue through changes.next_cursor too; apply deleted_at markers to your cache.
Kotlin (Android / JVM)
fun walk(endpoint: String, filters: JSONObject, byId: MutableMap<String, JSONObject>) {
    var cursor: String? = null
    do {
        val body = JSONObject(filters.toString()).put("limit", 50)
        cursor?.let { body.put("cursor", it) }
        val page = api(endpoint, body)
        val items = page.getJSONArray("items")
        for (i in 0 until items.length()) items.getJSONObject(i).let { byId[it.getString("id")] = it }
        cursor = if (page.isNull("next_cursor")) null else page.getString("next_cursor")
    } while (cursor != null)
}
val catchUpFrom = java.time.Instant.now().minusSeconds(60).toString()
val conversations = LinkedHashMap<String, JSONObject>()
walk("conversations.list", JSONObject(), conversations)
walk("conversations.list", JSONObject().put("updated_after", catchUpFrom), conversations)
conversations.values.forEach { renderConversation(it) }
val changes = api("messages.list", JSONObject()
    .put("conversation_id", conversationId).put("changed_since", savedWatermark))
// Continue through next_cursor too; apply deleted_at markers to your cache.
Realtime presence and sorted friend topics

These snippets construct the raw Phoenix join for your WebSocket client; they are not complete transport implementations. Use the documented WebSocket URL and a unique ref for each frame, maintain Phoenix heartbeats, handle presence_state/presence_diff, and rejoin after reconnecting. Set presence.enabled explicitly. Never log the access token.

JavaScript
const [low, high] = [myUserId.toLowerCase(), friendUserId.toLowerCase()].sort();
const topic = `friend-presence:${low}:${high}`;
const join = {
  topic: `realtime:${topic}`, event: "phx_join", ref: "1", join_ref: "1",
  payload: { access_token: currentAccessToken,
    config: { private: true, broadcast: { self: false }, presence: { enabled: true, key: myUserId } } }
};
// websocket.send(JSON.stringify(join));
// Requires friends:read + presence:read and an accepted friendship.
// Keep myUserId as the presence key even when friendUserId sorts first.
// Track only with presence:write, after the successful join:
const track = { topic: join.topic, event: "presence", ref: "2", join_ref: "1",
  payload: { type: "presence", event: "track", payload: { user_id: myUserId } } };
// websocket.send(JSON.stringify(track));
Kotlin (Android / JVM)
fun friendPresenceTopic(me: String, friend: String): String {
    val (low, high) = listOf(me.trim().lowercase(), friend.trim().lowercase()).sorted()
    return "friend-presence:$low:$high"
}
val topic = "realtime:${friendPresenceTopic(myUserId, friendUserId)}"
val join = JSONObject().put("topic", topic).put("event", "phx_join").put("ref", "1").put("join_ref", "1")
    .put("payload", JSONObject().put("access_token", currentAccessToken).put("config", JSONObject()
        .put("private", true).put("broadcast", JSONObject().put("self", false))
        .put("presence", JSONObject().put("enabled", true).put("key", myUserId))))
// websocket.send(join.toString())
// Requires friends:read + presence:read and an accepted friendship.
// Keep myUserId as the presence key even when friendUserId sorts first.
val track = JSONObject().put("topic", topic).put("event", "presence").put("ref", "2").put("join_ref", "1")
    .put("payload", JSONObject().put("type", "presence").put("event", "track")
        .put("payload", JSONObject().put("user_id", myUserId)))
// Send track only with presence:write, after the successful join.
Read last-seen time

Requires friends:read plus presence:read. The timestamp is not a replacement for live Presence. An absent field means it is not available to this request; null means no activity was recorded. Display a localized time or relative age without claiming the friend is currently online.

JavaScript
const { items: friends } = await api("friends.list");
for (const friend of friends) {
  if (!Object.hasOwn(friend, "last_seen_at")) continue;
  const lastSeen = friend.last_seen_at ? new Date(friend.last_seen_at) : null;
  showLastSeen(friend.user_id, lastSeen);
}
Kotlin (Android / JVM)
val friends = api("friends.list").getJSONArray("items")
for (i in 0 until friends.length()) {
    val friend = friends.getJSONObject(i)
    if (!friend.has("last_seen_at")) continue
    val lastSeen = if (friend.isNull("last_seen_at")) null
        else java.time.OffsetDateTime.parse(friend.getString("last_seen_at")).toInstant()
    showLastSeen(friend.getString("user_id"), lastSeen)
}
Draw, save a draft, publish and react

Use the drawing format below with boards.publish or boards.draw_branding. Save drafts explicitly with boards.save_draft; preserve revisions when conflict is true. Keep publication requests and operation IDs after errors. Publishing is always a user action, never an automatic result of reconnecting.

JavaScript
const drawing = { version: 1, width: 800, height: 600,
  strokes: [{ pen: "pixel", color: "#222222", size: 4, points: [[10, 10], [40, 30]] }] };
const draft = { board_id: boardId, draft_id: crypto.randomUUID(), revision_id: crypto.randomUUID(),
  payload: { body: "Hello everyone!", drawing }, operation_id: crypto.randomUUID() };
const saved = await api("boards.save_draft", draft);
// Save the returned revision_id. Preserve both versions when saved.conflict is true.
// Only when the user presses Publish:
const pending = { board_id: boardId, body: draft.payload.body, drawing, operation_id: crypto.randomUUID() };
const published = await api("boards.publish", pending);
// Only when the user presses Yeah:
const reaction = { board_id: boardId, post_id: published.id, yeah: true, operation_id: crypto.randomUUID() };
await api("boards.react", reaction);
Kotlin (Android / JVM)
val drawing = JSONObject("""{"version":1,"width":800,"height":600,"strokes":[
  {"pen":"pixel","color":"#222222","size":4,"points":[[10,10],[40,30]]}]}""")
val draft = JSONObject().put("board_id", boardId)
    .put("draft_id", java.util.UUID.randomUUID().toString())
    .put("revision_id", java.util.UUID.randomUUID().toString())
    .put("payload", JSONObject().put("body", "Hello everyone!").put("drawing", drawing))
    .put("operation_id", java.util.UUID.randomUUID().toString())
val saved = api("boards.save_draft", draft)
// Preserve both versions when conflict is true. Publish only after the user's action:
val pending = JSONObject().put("board_id", boardId).put("body", "Hello everyone!").put("drawing", drawing)
    .put("operation_id", java.util.UUID.randomUUID().toString())
val published = api("boards.publish", pending)
val reaction = JSONObject().put("board_id", boardId).put("post_id", published.getString("id"))
    .put("yeah", true).put("operation_id", java.util.UUID.randomUUID().toString())
// Send only when the user presses Yeah:
api("boards.react", reaction)
Block accounts and block incoming messages

These are separate settings. Account blocking removes the friendship; unblocking does not restore it. Block Messages blocks new direct messages. Block Invites blocks friend requests, group additions and Board invitations. privacy.set changes either or both. Neither setting removes the user from existing groups or boards.

JavaScript
const block = { user_id: selectedUserId, blocked: true, operation_id: crypto.randomUUID() };
await api("blocks.set", block); // Requires blocks:write.
const privacy = { block_messages: true, block_invites: true, operation_id: crypto.randomUUID() };
await api("privacy.set", privacy); // Requires privacy:write.
Kotlin (Android / JVM)
val block = JSONObject().put("user_id", selectedUserId).put("blocked", true)
    .put("operation_id", java.util.UUID.randomUUID().toString())
api("blocks.set", block) // Requires blocks:write.
val privacy = JSONObject().put("block_messages", true).put("block_invites", true)
    .put("operation_id", java.util.UUID.randomUUID().toString())
api("privacy.set", privacy) // Requires privacy:write.
Download, sign and upload message media

Use the existing media limits and allowed MIME types from the Media reference. Upload bytes first, then send the message with its attachment path. On a retry, upload to the same path again. A 409 from Storage means the file is already there, so continue to messages.send with the same client_operation_id. Avatar signing uses profile:read; message media uses the relevant messaging scope.

JavaScript
// A short-lived avatar URL:
const signed = await requestJson("https://api.pocketpass.xyz/storage/v1/object/sign/avatars",
  { paths: [avatarPath], expiresIn: 300 });
// Render only the returned permitted URL; signed URLs expire.
// Upload the user's selected image (Blob/File):
const operationId = crypto.randomUUID(); // Persist once per send.
const path = `${myUserId}/${conversationId}/${operationId}.png`;
const upload = await fetch(`https://api.pocketpass.xyz/storage/v1/object/message-media/${path}`, {
  method: "POST", headers: { Authorization: `Bearer ${currentAccessToken}`, "Content-Type": "image/png" }, body: imageBlob
});
if (!upload.ok) {
  const failure = await upload.json().catch(() => ({}));
  const alreadyUploaded = upload.status === 409 || failure.statusCode === "409";
  if (!alreadyUploaded) throw new Error(`Upload failed: ${upload.status}`);
}
const pending = { conversation_id: conversationId, client_operation_id: operationId,
  attachment: { path, mime_type: "image/png" } };
await api("messages.send", pending);
Kotlin (Android / JVM)
// Encode each path segment, retaining slash separators.
val encodedAvatarPath = avatarPath.split("/").joinToString("/") { URLEncoder.encode(it, "UTF-8").replace("+", "%20") }
val signed = requestJson("https://api.pocketpass.xyz/storage/v1/object/sign/avatars/$encodedAvatarPath",
    JSONObject().put("expiresIn", 300).toString())
// Render the returned signed URL before it expires.
val operationId = java.util.UUID.randomUUID().toString()
val path = "$myUserId/$conversationId/$operationId.png"
val connection = URL("https://api.pocketpass.xyz/storage/v1/object/message-media/$path").openConnection() as HttpURLConnection
try {
    connection.requestMethod = "POST"; connection.doOutput = true
    connection.connectTimeout = 15000; connection.readTimeout = 30000
    connection.instanceFollowRedirects = false
    connection.setRequestProperty("Authorization", "Bearer $currentAccessToken")
    connection.setRequestProperty("Content-Type", "image/png")
    connection.outputStream.use { it.write(imageBytes) }
    val status = connection.responseCode
    val failure = if (status in 200..299) "" else connection.errorStream?.bufferedReader()?.use { it.readText() }.orEmpty()
    val alreadyUploaded = status == 409 ||
        runCatching { JSONObject(failure).optString("statusCode") == "409" }.getOrDefault(false)
    check(status in 200..299 || alreadyUploaded) { "Upload failed: $status" }
} finally { connection.disconnect() }
val pending = JSONObject().put("conversation_id", conversationId).put("client_operation_id", operationId)
    .put("attachment", JSONObject().put("path", path).put("mime_type", "image/png"))
api("messages.send", pending)
Server-side confidential-client token exchange

Server only. Retrieve the secret from your secret store. Validate callback state and use the saved PKCE verifier before running this exchange. Do not put the secret in a browser, APK or iOS app.

JavaScript
const form = new URLSearchParams({ grant_type: "authorization_code", code,
  redirect_uri: savedRedirectUri, code_verifier: savedVerifier });
// Node.js server; clientId and clientSecret come from server configuration.
const basic = Buffer.from(`${clientId}:${clientSecret}`, "utf8").toString("base64");
const response = await fetch("https://api.pocketpass.xyz/auth/v1/oauth/token", {
  method: "POST", headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/x-www-form-urlencoded" }, body: form
});
if (!response.ok) throw new Error(`Token exchange failed: ${response.status}`);
const tokens = await response.json();
// Store tokens securely for this user; never log them.
Kotlin (Android / JVM)
// JVM server only. Use the shared form helper; restore savedVerifier after validating state.
val connection = URL("https://api.pocketpass.xyz/auth/v1/oauth/token").openConnection() as HttpURLConnection
val tokens = try {
    connection.requestMethod = "POST"; connection.doOutput = true
    connection.connectTimeout = 15000; connection.readTimeout = 30000
    connection.instanceFollowRedirects = false
    val basic = java.util.Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray(Charsets.UTF_8))
    connection.setRequestProperty("Authorization", "Basic $basic")
    connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
    val body = form(mapOf("grant_type" to "authorization_code", "code" to code,
        "redirect_uri" to savedRedirectUri, "code_verifier" to savedVerifier))
    connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
    check(connection.responseCode in 200..299) { "Token exchange failed: ${connection.responseCode}" }
    JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
} finally { connection.disconnect() }
// Store tokens securely for this user; never log them.
Realtime broadcast and reconnection

Raw frames for your WebSocket transport. Use the publishable key from developer.pocketpass.xyz/config.js in the connection URL, and the user access token in the join payload. Maintain heartbeats and merge presence_state/presence_diff with your library. On disconnect, reconnect and rejoin with the current token, then refetch to recover missed events.

JavaScript
const socketUrl = `wss://api.pocketpass.xyz/realtime/v1/websocket?apikey=${encodeURIComponent(publishableKey)}&vsn=1.0.0`;
const join = { topic: `realtime:conversation:${conversationId}`, event: "phx_join", ref: "1", join_ref: "1",
  payload: { access_token: currentAccessToken, config: { private: true, broadcast: { self: false }, presence: { enabled: false } } } };
// websocket.send(JSON.stringify(join)) after the socket opens.
function onFrame(frame) {
  if (frame.event === "phx_reply" && frame.payload.status === "error") {
    showConnectionError(frame.payload.response); return;
  }
  if (frame.topic === join.topic && frame.event === "broadcast") scheduleMessageRefresh(conversationId);
}
const heartbeat = { topic: "phoenix", event: "heartbeat", payload: {}, ref: "2" };
// Send a heartbeat every 25 seconds with a NEW ref; reconnect if replies stop.
const refresh = { topic: join.topic, event: "access_token", payload: { access_token: currentAccessToken }, ref: "3", join_ref: "1" };
// Send after OAuth refresh on each joined channel. Never reuse these illustrative refs.
Kotlin (Android / JVM)
val socketUrl = "wss://api.pocketpass.xyz/realtime/v1/websocket?apikey=" +
    URLEncoder.encode(publishableKey, "UTF-8") + "&vsn=1.0.0"
val topic = "realtime:conversation:$conversationId"
val join = JSONObject().put("topic", topic).put("event", "phx_join").put("ref", "1").put("join_ref", "1")
    .put("payload", JSONObject().put("access_token", currentAccessToken).put("config", JSONObject()
        .put("private", true).put("broadcast", JSONObject().put("self", false))
        .put("presence", JSONObject().put("enabled", false))))
// websocket.send(join.toString()) after opening the socket.
fun onFrame(frame: JSONObject) {
    if (frame.optString("event") == "phx_reply" && frame.optJSONObject("payload")?.optString("status") == "error") {
        showConnectionError(frame.getJSONObject("payload")); return
    }
    if (frame.optString("topic") == topic && frame.optString("event") == "broadcast") scheduleMessageRefresh(conversationId)
}
val heartbeat = JSONObject().put("topic", "phoenix").put("event", "heartbeat").put("payload", JSONObject()).put("ref", "2")
// Send every 25 seconds with a NEW ref; reconnect if replies stop.
val refresh = JSONObject().put("topic", topic).put("event", "access_token").put("ref", "3").put("join_ref", "1")
    .put("payload", JSONObject().put("access_token", currentAccessToken))
// Send after OAuth refresh on every joined channel. Never reuse these illustrative refs.

Every endpoint

Use the shared HTTP helpers. Fields shown are a minimal useful request; the endpoint reference lists optional fields, response shapes and access rules. Keep the entire request and operation ID for retries, including across app restarts.

session.get

Scope: none. Endpoint reference.

JavaScript
const request = {};
const result = await api("session.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("session.get", request)
// Render result in your app; handle ApiError without discarding pending work.
session.revoke

Scope: none. Endpoint reference.

JavaScript
const request = {};
const result = await api("session.revoke", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("session.revoke", request)
// Render result in your app; handle ApiError without discarding pending work.
me.get

Scope: profile:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("me.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("me.get", request)
// Render result in your app; handle ApiError without discarding pending work.
profiles.get

Scope: profile:read. Endpoint reference.

JavaScript
const request = {
  "user_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("profiles.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "user_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("profiles.get", request)
// Render result in your app; handle ApiError without discarding pending work.
profiles.get_many

Scope: profile:read. Endpoint reference.

JavaScript
const request = {
  "user_ids": [
    "97000000-0000-4000-8000-000000000001"
  ]
};
const result = await api("profiles.get_many", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "user_ids": [
    "97000000-0000-4000-8000-000000000001"
  ]
}
""")
val result = api("profiles.get_many", request)
// Render result in your app; handle ApiError without discarding pending work.
friends.list

Scope: friends:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("friends.list", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("friends.list", request)
// Render result in your app; handle ApiError without discarding pending work.
friends.requests_list

Scope: friends:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("friends.requests_list", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("friends.requests_list", request)
// Render result in your app; handle ApiError without discarding pending work.
friends.request_send

Scope: friends:write. Endpoint reference.

JavaScript
const request = {
  "user_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("friends.request_send", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "user_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("friends.request_send", request)
// Render result in your app; handle ApiError without discarding pending work.
friends.request_respond

Scope: friends:write. Endpoint reference.

JavaScript
const request = {
  "request_id": "97000000-0000-4000-8000-000000000001",
  "accept": true,
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("friends.request_respond", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "request_id": "97000000-0000-4000-8000-000000000001",
  "accept": true,
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("friends.request_respond", request)
// Render result in your app; handle ApiError without discarding pending work.
friends.request_cancel

Scope: friends:write. Endpoint reference.

JavaScript
const request = {
  "request_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("friends.request_cancel", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "request_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("friends.request_cancel", request)
// Render result in your app; handle ApiError without discarding pending work.
friends.remove

Scope: friends:write. Endpoint reference.

JavaScript
const request = {
  "user_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("friends.remove", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "user_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("friends.remove", request)
// Render result in your app; handle ApiError without discarding pending work.
friends.code_get

Scope: friends:write. Endpoint reference.

JavaScript
const request = {};
const result = await api("friends.code_get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("friends.code_get", request)
// Render result in your app; handle ApiError without discarding pending work.
friends.code_resolve

Scope: friends:write. Endpoint reference.

JavaScript
const request = {
  "code": "12345678"
};
const result = await api("friends.code_resolve", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "code": "12345678"
}
""")
val result = api("friends.code_resolve", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.list

Scope: messages:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("conversations.list", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("conversations.list", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.get

Scope: messages:read. Endpoint reference.

JavaScript
const request = {
  "conversation_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("conversations.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "conversation_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("conversations.get", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.open

Scope: messages:write. Endpoint reference.

JavaScript
const request = {
  "user_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("conversations.open", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "user_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("conversations.open", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.create

Scope: groups:write. Endpoint reference.

JavaScript
const request = {
  "title": "Friends",
  "member_ids": [
    "97000000-0000-4000-8000-000000000001"
  ],
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("conversations.create", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "title": "Friends",
  "member_ids": [
    "97000000-0000-4000-8000-000000000001"
  ],
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("conversations.create", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.add_members

Scope: groups:write. Endpoint reference.

JavaScript
const request = {
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "member_ids": [
    "97000000-0000-4000-8000-000000000001"
  ],
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("conversations.add_members", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "member_ids": [
    "97000000-0000-4000-8000-000000000001"
  ],
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("conversations.add_members", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.remove_member

Scope: groups:write. Endpoint reference.

JavaScript
const request = {
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("conversations.remove_member", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("conversations.remove_member", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.leave

Scope: groups:write. Endpoint reference.

JavaScript
const request = {
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("conversations.leave", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("conversations.leave", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.rename

Scope: groups:write. Endpoint reference.

JavaScript
const request = {
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "title": "Friends",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("conversations.rename", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "title": "Friends",
  "client_operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("conversations.rename", request)
// Render result in your app; handle ApiError without discarding pending work.
conversations.mark_read

Scope: messages:write. Endpoint reference.

JavaScript
const request = {
  "conversation_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("conversations.mark_read", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "conversation_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("conversations.mark_read", request)
// Render result in your app; handle ApiError without discarding pending work.
messages.list

Scope: messages:read. Endpoint reference.

JavaScript
const request = {
  "conversation_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("messages.list", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "conversation_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("messages.list", request)
// Render result in your app; handle ApiError without discarding pending work.
messages.send

Scope: messages:write. Endpoint reference.

JavaScript
const request = {
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001",
  "body": "Hello!"
};
// Generate once per user action; persist request before sending.
request.client_operation_id = crypto.randomUUID();
const result = await api("messages.send", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "conversation_id": "97000000-0000-4000-8000-000000000001",
  "client_operation_id": "97000000-0000-4000-8000-000000000001",
  "body": "Hello!"
}
""")
// Generate once per user action; persist request before sending.
request.put("client_operation_id", java.util.UUID.randomUUID().toString())
val result = api("messages.send", request)
// Render result in your app; handle ApiError without discarding pending work.
messages.edit

Scope: messages:write. Endpoint reference.

JavaScript
const request = {
  "message_id": "97000000-0000-4000-8000-000000000001",
  "body": "Hello everyone!"
};
const result = await api("messages.edit", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "message_id": "97000000-0000-4000-8000-000000000001",
  "body": "Hello everyone!"
}
""")
val result = api("messages.edit", request)
// Render result in your app; handle ApiError without discarding pending work.
messages.delete

Scope: messages:write. Endpoint reference.

JavaScript
const request = {
  "message_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("messages.delete", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "message_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("messages.delete", request)
// Render result in your app; handle ApiError without discarding pending work.
notifications.list

Scope: notifications:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("notifications.list", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("notifications.list", request)
// Render result in your app; handle ApiError without discarding pending work.
notifications.mark_read

Scope: notifications:read. Endpoint reference.

JavaScript
const request = {
  "notification_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("notifications.mark_read", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "notification_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("notifications.mark_read", request)
// Render result in your app; handle ApiError without discarding pending work.
notifications.delete

Scope: notifications:read. Endpoint reference.

JavaScript
const request = {
  "notification_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("notifications.delete", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "notification_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("notifications.delete", request)
// Render result in your app; handle ApiError without discarding pending work.
tokens.get

Scope: tokens:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("tokens.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("tokens.get", request)
// Render result in your app; handle ApiError without discarding pending work.
encounters.list

Scope: encounters:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("encounters.list", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("encounters.list", request)
// Render result in your app; handle ApiError without discarding pending work.
puzzles.get

Scope: puzzles:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("puzzles.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("puzzles.get", request)
// Render result in your app; handle ApiError without discarding pending work.
piips.get

Scope: piip:read. Endpoint reference.

JavaScript
const request = {
  "user_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("piips.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "user_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("piips.get", request)
// Render result in your app; handle ApiError without discarding pending work.
blocks.list

Scope: blocks:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("blocks.list", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("blocks.list", request)
// Render result in your app; handle ApiError without discarding pending work.
blocks.set

Scope: blocks:write. Endpoint reference.

JavaScript
const request = {
  "user_id": "97000000-0000-4000-8000-000000000001",
  "blocked": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("blocks.set", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "user_id": "97000000-0000-4000-8000-000000000001",
  "blocked": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("blocks.set", request)
// Render result in your app; handle ApiError without discarding pending work.
privacy.get

Scope: privacy:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("privacy.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("privacy.get", request)
// Render result in your app; handle ApiError without discarding pending work.
privacy.set

Scope: privacy:write. Endpoint reference.

JavaScript
const request = {
  "block_messages": true,
  "block_invites": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("privacy.set", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "block_messages": true,
  "block_invites": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("privacy.set", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.settings

Scope: boards:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("boards.settings", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("boards.settings", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.list

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "scope": "explore",
  "limit": 30
};
const result = await api("boards.list", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "scope": "explore",
  "limit": 30
}
""")
val result = api("boards.list", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.get

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.get", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.get", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.feed

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "sort": "newest",
  "limit": 30
};
const result = await api("boards.feed", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "sort": "newest",
  "limit": 30
}
""")
val result = api("boards.feed", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.replies

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "post_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.replies", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "post_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.replies", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.post

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "post_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.post", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "post_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.post", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.preview

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "post_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.preview", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "post_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.preview", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.asset

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "asset_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.asset", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "asset_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.asset", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.branding_preview

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "kind": "icon"
};
const result = await api("boards.branding_preview", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "kind": "icon"
}
""")
val result = api("boards.branding_preview", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.drafts

Scope: boards:drafts. Endpoint reference.

JavaScript
const request = {};
const result = await api("boards.drafts", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("boards.drafts", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.inbox

Scope: boards:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("boards.inbox", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("boards.inbox", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.invitations

Scope: boards:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("boards.invitations", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("boards.invitations", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.proposals

Scope: boards:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("boards.proposals", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("boards.proposals", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.notices

Scope: boards:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("boards.notices", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("boards.notices", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.members

Scope: boards:read. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.members", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.members", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.management

Scope: boards:moderate. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.management", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.management", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.outgoing_invitations

Scope: boards:invite. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.outgoing_invitations", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.outgoing_invitations", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.history

Scope: boards:moderate. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001"
};
const result = await api("boards.history", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001"
}
""")
val result = api("boards.history", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.stationery

Scope: boards:read. Endpoint reference.

JavaScript
const request = {};
const result = await api("boards.stationery", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{}
""")
val result = api("boards.stationery", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.propose

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "name": "Handheld chat",
  "visibility": "public",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.propose", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "name": "Handheld chat",
  "visibility": "public",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.propose", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.join

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.join", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.join", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.accept_invitation

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.accept_invitation", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.accept_invitation", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.join_code

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "code": "REPLACE_WITH_INVITATION_CODE",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.join_code", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "code": "REPLACE_WITH_INVITATION_CODE",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.join_code", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.decline_invitation

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.decline_invitation", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.decline_invitation", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.decide_join

Scope: boards:moderate. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "approve": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.decide_join", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "approve": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.decide_join", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.invite

Scope: boards:invite. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.invite", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.invite", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.create_code

Scope: boards:invite. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.create_code", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.create_code", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.revoke_invitation

Scope: boards:invite. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.revoke_invitation", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.revoke_invitation", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.leave

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.leave", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.leave", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.preferences

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "muted": true
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.preferences", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "muted": true
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.preferences", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.push_preference

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "enabled": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.push_preference", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "enabled": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.push_preference", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.update_board

Scope: boards:manage. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "description": "A place to share handheld games."
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.update_board", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "description": "A place to share handheld games."
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.update_board", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.archive

Scope: boards:manage. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "archived": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.archive", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "archived": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.archive", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.transfer

Scope: boards:manage. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.transfer", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.transfer", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.set_moderator

Scope: boards:manage. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "moderator": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.set_moderator", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "moderator": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.set_moderator", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.accept_transfer

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.accept_transfer", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.accept_transfer", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.publish

Scope: boards:write. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "body": "Hello everyone!"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.publish", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "body": "Hello everyone!"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.publish", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.edit

Scope: boards:write. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "post_id": "97000000-0000-4000-8000-000000000001",
  "body": "Hello everyone!",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.edit", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "post_id": "97000000-0000-4000-8000-000000000001",
  "body": "Hello everyone!",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.edit", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.delete_post

Scope: boards:write. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "post_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.delete_post", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "post_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.delete_post", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.spoiler

Scope: boards:write. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "post_id": "97000000-0000-4000-8000-000000000001",
  "spoiler": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.spoiler", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "post_id": "97000000-0000-4000-8000-000000000001",
  "spoiler": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.spoiler", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.react

Scope: boards:write. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "post_id": "97000000-0000-4000-8000-000000000001",
  "yeah": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.react", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "post_id": "97000000-0000-4000-8000-000000000001",
  "yeah": true,
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.react", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.draw_branding

Scope: boards:manage. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "kind": "icon",
  "drawing": {
    "version": 1,
    "width": 800,
    "height": 600,
    "strokes": [
      {
        "pen": "pixel",
        "color": "#222222",
        "size": 4,
        "points": [
          [
            10,
            10
          ],
          [
            40,
            30
          ]
        ]
      }
    ]
  },
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.draw_branding", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "kind": "icon",
  "drawing": {
    "version": 1,
    "width": 800,
    "height": 600,
    "strokes": [
      {
        "pen": "pixel",
        "color": "#222222",
        "size": 4,
        "points": [
          [
            10,
            10
          ],
          [
            40,
            30
          ]
        ]
      }
    ]
  },
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.draw_branding", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.save_draft

Scope: boards:drafts. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "draft_id": "97000000-0000-4000-8000-000000000001",
  "revision_id": "97000000-0000-4000-8000-000000000001",
  "payload": {
    "body": "My unfinished note"
  },
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.save_draft", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "draft_id": "97000000-0000-4000-8000-000000000001",
  "revision_id": "97000000-0000-4000-8000-000000000001",
  "payload": {
    "body": "My unfinished note"
  },
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.save_draft", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.discard_draft

Scope: boards:drafts. Endpoint reference.

JavaScript
const request = {
  "revision_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.discard_draft", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "revision_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.discard_draft", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.report

Scope: boards:write. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.report", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.report", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.resolve_report

Scope: boards:moderate. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.resolve_report", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.resolve_report", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.restrict

Scope: boards:moderate. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "kind": "mute",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.restrict", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "user_id": "97000000-0000-4000-8000-000000000001",
  "kind": "mute",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.restrict", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.revoke_restriction

Scope: boards:moderate. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.revoke_restriction", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.revoke_restriction", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.appeal

Scope: boards:write. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "case_id": "97000000-0000-4000-8000-000000000001",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.appeal", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "case_id": "97000000-0000-4000-8000-000000000001",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.appeal", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.resolve_appeal

Scope: boards:moderate. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.resolve_appeal", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "id": "97000000-0000-4000-8000-000000000001",
  "reason": "Explain the reason for this action.",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.resolve_appeal", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.read_event

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.read_event", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.read_event", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.read_thread

Scope: boards:membership. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.read_thread", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.read_thread", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.buy_stationery

Scope: boards:purchase. Endpoint reference.

JavaScript
const request = {
  "stationery_id": "plain",
  "operation_id": "97000000-0000-4000-8000-000000000001"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.buy_stationery", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "stationery_id": "plain",
  "operation_id": "97000000-0000-4000-8000-000000000001"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.buy_stationery", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.artwork_upload

Scope: boards:manage. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "kind": "icon",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "image": "REPLACE_WITH_BASE64_IMAGE_BYTES"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.artwork_upload", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "kind": "icon",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "image": "REPLACE_WITH_BASE64_IMAGE_BYTES"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.artwork_upload", request)
// Render result in your app; handle ApiError without discarding pending work.
boards.prepare_branding

Scope: boards:manage. Endpoint reference.

JavaScript
const request = {
  "board_id": "97000000-0000-4000-8000-000000000001",
  "kind": "icon",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "source_hash": "REPLACE_WITH_LOWERCASE_SHA256"
};
// Generate once per user action; persist request before sending.
request.operation_id = crypto.randomUUID();
const result = await api("boards.prepare_branding", request);
// Render result in your app; handle ApiError without discarding pending work.
Kotlin (Android / JVM)
val request = JSONObject("""
{
  "board_id": "97000000-0000-4000-8000-000000000001",
  "kind": "icon",
  "operation_id": "97000000-0000-4000-8000-000000000001",
  "source_hash": "REPLACE_WITH_LOWERCASE_SHA256"
}
""")
// Generate once per user action; persist request before sending.
request.put("operation_id", java.util.UUID.randomUUID().toString())
val result = api("boards.prepare_branding", request)
// Render result in your app; handle ApiError without discarding pending work.

PocketPass Developer Docs · Portal · Connected apps (for users)