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:
| What | Where |
|---|---|
| Authorization endpoint | GET /auth/v1/oauth/authorize |
| Token endpoint | POST /auth/v1/oauth/token |
| OpenID userinfo | GET /auth/v1/oauth/userinfo |
| Discovery document | GET /auth/v1/.well-known/openid-configuration |
| PocketPass API | POST /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
codeandhint. - 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
| Type | Use it for | Token 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.1and[::1]. Because the match is exact, register the port you will actually listen on —http://127.0.0.1:8765/callback, nothttp://127.0.0.1/callback— and pin that port in your app. If you are unsure which name the platform will use, register bothhttp://localhost:8765/callbackandhttp://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.
| Scope | Shown to the user as | Unlocks |
|---|---|---|
profile:read | See your profile (name, bio, avatar, age, country) | me.get, profiles.get, profiles.get_many, avatar downloads |
friends:read | See your friends list and friend requests | friends.list, friends.requests_list, the friends: Realtime topic |
friends:write | Add and remove friends and answer friend requests as you | friends.request_send, friends.request_respond, friends.request_cancel, friends.remove, friends.code_get, friends.code_resolve |
messages:read | Read your conversations and messages | conversations.list, conversations.get, messages.list, attachment downloads, the conversation: Realtime topics |
messages:write | Send, edit and delete messages as you | conversations.open, conversations.mark_read, messages.send (text and images), messages.edit, messages.delete, image uploads |
groups:write | Create group chats and manage their members as you | conversations.create, conversations.add_members, conversations.remove_member, conversations.leave, conversations.rename |
notifications:read | See and clear your notifications | notifications.list, notifications.mark_read, notifications.delete, the notifications: Realtime topic |
presence:read | See when your friends were last online, who is online now and who is active in your chats | Presence on the friend-presence: topics (with friends:read) and the conversation: topics (with messages:read) |
presence:write | Show you as online and typing to your friends | Tracking and reading live Presence on those same topics; reading stored last_seen_at still requires presence:read |
tokens:read | See your token balance and supporter status | tokens.get, the tokens: Realtime topic |
encounters:read | See the people you have met nearby | encounters.list, the encounters: Realtime topic |
puzzles:read | See your Puzzle Swap progress | puzzles.get |
piip:read | See your Piip's favourite colour | piips.get |
boards:read | Read public boards and your private boards, notes, drawings, members, invitations and activity | Board reads, stationery catalogue and boards: Realtime |
boards:write | Publish, edit and remove your notes and replies, react, report content and submit appeals as you | Participation and reporting |
boards:membership | Join and leave boards, accept invitations and ownership offers, request boards and change board notifications | Membership, proposals, notification preferences and read markers |
boards:invite | Invite people to your boards and create or revoke invitation codes as you | Direct invitations and codes |
boards:manage | Change your boards, artwork, moderators and ownership, and archive or reopen them | Owner tools and artwork uploads |
boards:moderate | Review your boards' reports and retained content, manage membership requests, and mute or ban members as a board moderator | Local moderator tools and explicit review of blocked content |
boards:drafts | Read, save and discard your private cloud note drafts | Cloud drafts and board-drafts: Realtime |
boards:purchase | Spend your PocketPass tokens on permanent stationery purchases | boards.buy_stationery |
blocks:read | See the accounts you have blocked | blocks.list and blocks: Realtime |
blocks:write | Block and unblock accounts as you | blocks.set |
privacy:read | See whether you have Block Messages and Block Invites enabled | privacy.get and privacy: Realtime |
privacy:write | Enable or disable Block Messages and Block Invites on your account | privacy.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 scope | Shown to the user as | Userinfo claims |
|---|---|---|
openid | (always) | sub — the PocketPass user id |
email | your email address | email, email_verified |
profile | your PocketPass username | name and preferred_username (both the PocketPass username), updated_at, and user_metadata with only username and name |
phone | your phone number | phone, 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 asAuthorization: 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 answerPT401 CONSENT_REVOKED, refresh fails withinvalid_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_SUSPENDEDand 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(andOPTIONSfor CORS preflight). AGETis a 404. - The body must be a JSON object; anything else, including invalid JSON, is
PT400 INVALID_FIELD. Unknown keys are rejected withPT400 UNKNOWN_FIELD, so typos fail loudly instead of silently doing nothing. - Every response body is a JSON object. Successful responses are
200. Errors carry a4xx/5xxstatus and a body{ "code", "message", "hint" }— branch onhint, showmessageto a developer, never to an end user. - Lists return
{ "items": [...], "next_cursor": null | "…" }. Passnext_cursorback ascursorto get the next page;nullmeans you have everything. Cursors are opaque strings — do not build or parse them. limitis 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, answers404with{ "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.
| Endpoint | Scope | Body | Returns |
|---|---|---|---|
session.getKotlin / 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.revokeKotlin / 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.getKotlin / JavaScript | profile:read | {} |
{ profile } — the connected user's own profile |
profiles.getKotlin / 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_manyKotlin / JavaScript | profile:read | { user_ids: [...] } (1…100) |
{ items: [profile, ...] } — same visibility rule; ids the user may not see are simply left out |
friends.listKotlin / JavaScript | friends:read | {} |
{ items: [friend, ...] } — the whole list, no paging |
friends.requests_listKotlin / JavaScript | friends:read | {} |
{ incoming: [friend_request, ...], outgoing: [friend_request, ...] } — pending requests only, newest first |
friends.request_sendKotlin / 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_respondKotlin / 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_cancelKotlin / 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.removeKotlin / 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_getKotlin / JavaScript | friends:write | {} |
{ code } — the user's own 8-digit friend code, the same one PocketPass shows them |
friends.code_resolveKotlin / 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.listKotlin / 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.getKotlin / JavaScript | messages:read | { conversation_id } |
{ conversation }; PT404 CONVERSATION_NOT_FOUND when the user is not a member |
conversations.openKotlin / 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.createKotlin / 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_membersKotlin / 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_memberKotlin / 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.leaveKotlin / 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.renameKotlin / JavaScript | groups:write | { conversation_id, title, client_operation_id } |
{ conversation } — owner only (PT403 NOT_OWNER); title is 1…80 characters after trimming. |
conversations.mark_readKotlin / JavaScript | messages:write | { conversation_id } |
{ last_read_at } |
messages.listKotlin / 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.sendKotlin / 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.editKotlin / JavaScript | messages:write | { message_id, body } |
{ message } — own messages only (PT403 NOT_SENDER); deleted messages cannot be edited (PT409 MESSAGE_DELETED) |
messages.deleteKotlin / JavaScript | messages:write | { message_id } |
{ message } with deleted_at set — own messages only |
notifications.listKotlin / 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_readKotlin / JavaScript | notifications:read | { notification_id } |
{ ok: true } |
notifications.deleteKotlin / JavaScript | notifications:read | { notification_id } |
{ ok: true } |
tokens.getKotlin / 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.listKotlin / 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.getKotlin / 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.getKotlin / 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.
| Endpoint | Scope | JSON fields | Result / behaviour |
|---|---|---|---|
boards.settingsKotlin / JavaScript | boards:read | {} | Feature/request switches and your board push preference. |
boards.listKotlin / JavaScript | boards:read | scope?, search?, limit?, cursor? | Board objects in items; joined by default, or explore for public boards. Returns next_cursor. |
boards.getKotlin / JavaScript | boards:read | board_id, review? | One board with your role, membership settings, member count, access revision and pending ownership offer. |
boards.feedKotlin / JavaScript | boards:read | board_id, sort?, period?, limit?, cursor?, review? | Top-level notes in items; newest, activity or popular ordering. Returns next_cursor. |
boards.repliesKotlin / JavaScript | boards:read | post_id, limit?, cursor?, review? | The parent post and chronological replies in items. Returns next_cursor. |
boards.postKotlin / JavaScript | boards:read | post_id, reveal?, review? | One note or reply. reveal explicitly reveals a spoiler. |
boards.previewKotlin / JavaScript | boards:read | post_id, reveal?, review? | { svg } static preview, or null while a spoiler is hidden. |
boards.assetKotlin / JavaScript | boards:read | asset_id | { mime, data } with base64 image bytes. Current audience is checked on every read. |
boards.branding_previewKotlin / JavaScript | boards:read | board_id, kind | { svg } for the drawn icon or cover. |
boards.draftsKotlin / JavaScript | boards:drafts | board_id?, limit?, cursor? | Your current cloud draft heads in items, including conflicting versions. Returns next_cursor. |
boards.inboxKotlin / JavaScript | boards:read | limit?, cursor? | Grouped activity in items, with event counts and read markers; no note text. Returns next_cursor. |
boards.invitationsKotlin / JavaScript | boards:read | limit?, cursor? | Your unaccepted direct invitations in items. Returns next_cursor. |
boards.proposalsKotlin / JavaScript | boards:read | limit?, cursor? | Your board proposals and their decisions in items. Returns next_cursor. |
boards.noticesKotlin / JavaScript | boards:read | limit?, cursor? | Your restrictions, actions and appeals arrays, each paginated with the same cursor. Returns next_cursor. |
boards.membersKotlin / JavaScript | boards:read | board_id, limit?, cursor?, review? | Members in items, including display_name and avatar_path. Returns next_cursor. |
boards.managementKotlin / JavaScript | boards:moderate | board_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_invitationsKotlin / JavaScript | boards:invite | board_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.historyKotlin / JavaScript | boards:moderate | board_id, post_id?, limit?, cursor? | Retained text/content versions in items, for board moderators only. Returns next_cursor. |
boards.stationeryKotlin / JavaScript | boards:read | limit?, cursor? | Catalogue items with available and owned flags; includes permanently owned inactive items. Returns next_cursor. |
boards.proposeKotlin / JavaScript | boards:membership | id?, name, description?, rules?, visibility, operation_id | Submit a proposal for dashboard approval; returns id. Does not create a board immediately. |
boards.joinKotlin / JavaScript | boards:membership | board_id, operation_id | Join a public board or request approval; returns board_id and status (joined or requested). |
boards.accept_invitationKotlin / JavaScript | boards:membership | id, operation_id | Accept a direct invitation; returns board_id and status. |
boards.join_codeKotlin / JavaScript | boards:membership | code, operation_id | Join or request approval using a code; returns board_id and status. |
boards.decline_invitationKotlin / JavaScript | boards:membership | board_id, id, operation_id | Decline your own direct invitation. |
boards.decide_joinKotlin / JavaScript | boards:moderate | board_id, user_id, approve, operation_id | Approve or reject a membership request as a board moderator. |
boards.inviteKotlin / JavaScript | boards:invite | board_id, user_id, id?, operation_id | Create a direct invitation; returns id. The recipient must accept. |
boards.create_codeKotlin / JavaScript | boards:invite | board_id, id?, operation_id | Create a reusable invitation; returns id and the code. Save the returned code securely. |
boards.revoke_invitationKotlin / JavaScript | boards:invite | board_id, id, operation_id | Revoke an invitation or code you are allowed to manage. |
boards.leaveKotlin / JavaScript | boards:membership | board_id, operation_id | Leave a board. An owner must first transfer ownership or archive it. |
boards.preferencesKotlin / JavaScript | boards:membership | board_id, muted?, push_enabled?, operation_id | Change your per-board mute and push settings. |
boards.push_preferenceKotlin / JavaScript | boards:membership | enabled, operation_id | Change your global Boards push preference. |
boards.update_boardKotlin / JavaScript | boards:manage | board_id, name?, description?, rules?, visibility?, accent?, join_policy?, code_policy?, members_can_invite?, operation_id | Update board details, joining/invitation rules or branding accent as the owner. |
boards.archiveKotlin / JavaScript | boards:manage | board_id, archived, operation_id | Archive or reopen your board. |
boards.transferKotlin / JavaScript | boards:manage | board_id, user_id, operation_id | Offer ownership to a current member; they must accept. |
boards.set_moderatorKotlin / JavaScript | boards:manage | board_id, user_id, moderator, operation_id | Appoint or remove a board moderator as the owner. |
boards.accept_transferKotlin / JavaScript | boards:membership | board_id, operation_id | Accept an ownership offer addressed to you. |
boards.publishKotlin / JavaScript | boards:write | board_id, id?, thread_id?, reply_to?, body?, drawing?, stationery_id?, spoiler?, draft_id?, draft_revision_id?, draft_base_id?, operation_id | Publish a note or reply; returns id, board_id and thread_id. Drawings are immutable after publishing. |
boards.editKotlin / JavaScript | boards:write | board_id, post_id, body, operation_id | Edit your note or reply text; preserves drawing and adds an edited marker. |
boards.delete_postKotlin / JavaScript | boards:write | board_id, post_id, reason?, review?, operation_id | Remove content while preserving a placeholder and replies. Moderating another author requires a reason and boards:moderate. |
boards.spoilerKotlin / JavaScript | boards:write | board_id, post_id, spoiler, reason?, review?, operation_id | Set a spoiler flag. Moderating another author requires a reason and boards:moderate. |
boards.reactKotlin / JavaScript | boards:write | board_id, post_id, yeah, operation_id | Set or clear your Yeah reaction; repeated true values do not add reactions. |
boards.draw_brandingKotlin / JavaScript | boards:manage | board_id, kind, drawing, draft_id?, draft_revision_id?, draft_base_id?, operation_id | Set a drawn icon or cover as the owner. |
boards.save_draftKotlin / JavaScript | boards:drafts | board_id, draft_id, revision_id, base_id?, payload, operation_id | Save an immutable revision; returns revision_id and conflict. Does not publish. |
boards.discard_draftKotlin / JavaScript | boards:drafts | revision_id, operation_id | Discard one of your own draft revisions. |
boards.reportKotlin / JavaScript | boards:write | board_id, post_id?, reason, id?, operation_id | Report a note/reply, or board branding when post_id is omitted; returns case_id. |
boards.resolve_reportKotlin / JavaScript | boards:moderate | board_id, id, reason, status?, operation_id | Resolve or dismiss a local board report. Reports against board staff remain dashboard-only. |
boards.restrictKotlin / JavaScript | boards:moderate | board_id, user_id, kind, reason, expires_at?, id?, operation_id | Issue a board mute or ban; returns case_id. Cannot issue a global suspension. |
boards.revoke_restrictionKotlin / JavaScript | boards:moderate | board_id, id, operation_id | Revoke a local board restriction. Central restrictions cannot be revoked here. |
boards.appealKotlin / JavaScript | boards:write | board_id, case_id, reason, id?, operation_id | Appeal your local board moderation case; returns case_id. Central appeals go through Discord. |
boards.resolve_appealKotlin / JavaScript | boards:moderate | board_id, id, reason, status?, operation_id | Resolve or dismiss a board appeal; revoking the restriction is a separate action. |
boards.read_eventKotlin / JavaScript | boards:membership | id, operation_id | Mark your activity event read. |
boards.read_threadKotlin / JavaScript | boards:membership | board_id, thread_id?, operation_id | Mark your activity in a thread read, or all activity for the board when thread_id is omitted. |
boards.buy_stationeryKotlin / JavaScript | boards:purchase | stationery_id, operation_id | Buy 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_idorimageis 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.
| Endpoint | Scope | JSON body | Response |
|---|---|---|---|
blocks.listKotlin / 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.setKotlin / JavaScript | blocks:write | { user_id, blocked, operation_id } | { user_id, blocked } |
privacy.getKotlin / JavaScript | privacy:read | {} | { block_messages, block_invites } |
privacy.setKotlin / 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 assubin 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
avatarsbucket, ornull. 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_idon 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.getworks for the other person.created_at,responded_at- Timestamps;
responded_atisnullwhile pending.
conversation
id- UUID.
kind"direct"or"group". Do not assume exactly two members.titlenullfor 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
messageobject, ornull. 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 themessage-mediabucket), ornull. 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, ornull.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_atisnulluntil 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_idon theencounters:Realtime topic. peer- The person the user met, as a
profile. Strangers are not otherwise visible to your app:profiles.geton them answersPT404, 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
nullwhile only one side has. Confirmed encounters are the ones PocketPass counts. updated_at- The later of
created_atandconfirmed_at; the value to pass back asupdated_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 / code | Hint | Meaning | What to do |
|---|---|---|---|
400 PT400 | MISSING_FIELD | A required body key is absent, or privacy.set got neither setting. | Fix the request. |
INVALID_FIELD | A 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_FIELD | The body contains a key the endpoint does not know. | Check the spelling against the reference. | |
INVALID_CURSOR | The cursor was not one this endpoint issued. | Restart from the first page. | |
BODY_LENGTH | Message body is empty or longer than 4000 characters after trimming. | Validate before sending. | |
REPLY_TARGET | reply_to_id is not a message of that conversation. | Only reply to messages from the same messages.list. | |
SELF_TARGET | A 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_LENGTH | Group title is empty or longer than 80 characters after trimming. | Validate before sending. | |
MEMBERS_REQUIRED | member_ids held nobody but the user themselves. | Pick at least one friend. | |
NOT_A_GROUP | A group endpoint was called on a direct conversation. | Only offer group actions when kind is "group". | |
INVALID_CODE | A friend code that is not eight digits. | Validate before sending. | |
UNSUPPORTED_MEDIA | attachment.mime_type is not image/jpeg, image/png, image/webp or image/gif. | Convert the image first. | |
INVALID_ATTACHMENT_PATH | attachment.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 PT401 | API_TOKEN_REQUIRED | No bearer token, an expired or invalid one, or a token that is not a connected-app token. | Refresh and retry once; then reconnect. |
CONSENT_REVOKED | The user disconnected your app, or its scopes changed since they approved it. | Delete the stored tokens and offer the connect flow again. | |
403 PT403 | APP_SUSPENDED | PocketPass suspended your app. | Stop calling; contact PocketPass. |
ACCOUNT_BANNED | The 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_REQUIRED | Your 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_MEMBER | The user is not an active member of that conversation (including after being removed from a group). | Refresh your conversation list. | |
BLOCKED | One 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_BLOCKED | The other person has Block Messages on. | Tell the user this person is not accepting messages. Do not retry. | |
GROUP_MESSAGES_BLOCKED | Someone 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_BLOCKED | The person has Block Invites on and is not accepting friend requests. | Tell the user. Do not retry. | |
BOARD_INVITATIONS_BLOCKED | The person has Block Invites on and is not accepting Board invitations. | Tell the user. Do not retry. | |
NOT_FRIENDS | conversations.open, conversations.create or conversations.add_members with users who are not the user's friends. | Only pick people from friends.list. | |
NOT_OWNER | Renaming a group or removing a member without owning the group. | Only offer rename/remove when the user is the first entry in members. | |
NOT_SENDER | Editing or deleting someone else's message. | Only offer edit/delete on messages whose sender_id is the user. | |
NOT_ADDRESSEE | Answering a friend request that was not sent to the user. | Only offer accept/decline on incoming requests. | |
NOT_REQUESTER | Cancelling a friend request the user did not send. | Only offer cancel on outgoing requests. | |
404 PT404 | APP_NOT_FOUND | The app in the token no longer exists. | The app was deleted; nothing to retry. |
UNKNOWN_ENDPOINT | No such endpoint, such as /v1/friends.nope, or a method other than POST. | Check the path against the reference. | |
PROFILE_NOT_FOUND | No such user, or the connected user may not see them. | Treat as unavailable. | |
CONVERSATION_NOT_FOUND | No such conversation, or the user is not in it. | Refresh your conversation list. | |
MESSAGE_NOT_FOUND | No such message in a conversation the user can see. | Refresh the message list. | |
NOTIFICATION_NOT_FOUND | No such notification for this user. | Refresh the notification list. | |
REQUEST_NOT_FOUND | No such friend request involving the user. | Refresh friends.requests_list. | |
CODE_NOT_FOUND | No account the user may see uses that friend code. | Tell the user to check the code. | |
ATTACHMENT_NOT_FOUND | attachment.path was never uploaded. | Upload first, then send. | |
409 PT409 | DUPLICATE_OPERATION_ID | A 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_DELETED | Editing a message that has been deleted. | Remove it from your UI. | |
GROUP_FULL | The group already has, or would exceed, 20 members. | Show the cap; let the owner remove someone first. | |
FRIEND_REQUEST_PENDING | The notification is a friend request the user has not answered yet. | Answer it with friends.request_respond or leave it in place. | |
ALREADY_FRIENDS | friends.request_send to someone who is already a friend. | Refresh friends.list. | |
REQUEST_CLOSED | The friend request was already accepted, declined or cancelled. | Refresh friends.requests_list. | |
429 PT429 | API_RATE_LIMITED | Too 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_LIMITED | More 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 PT500 | INTERNAL | Something 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
429whose body has nocodefield (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 forPT429. - 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 gets403with{ "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
PT429withRetry-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.getalways reports the limits that apply to your app. - Failed requests count. A
PT400, aPT403, aPT401 CONSENT_REVOKEDand 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 withAPI_TOKEN_REQUIREDorACCOUNT_BANNEDare stopped before the limit and don't count. - A
PT429response carries aRetry-Afterheader in seconds. Every call that gets past the token check also returnsRateLimit-RemainingandRateLimit-Reset(seconds until the minute resets) for the tighter of the two per-minute limits above.session.getreturnsrate_limit.remainingandrate_limit.reset_atfor 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-ResetandRetry-Aftercome 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:
- Poll
conversations.listwithupdated_afterset to the newestupdated_atyou have seen. The response contains only conversations that received a message since then, together with theirlast_messageandunread_count. An emptyitemsarray is the common, cheap case. - For the conversation that is open on screen, poll
messages.listwithchanged_sinceset to the newest ofcreated_at,edited_atanddeleted_atyou have seen. This returns new messages and messages that were edited or deleted after that instant, whichupdated_afteron the conversation does not surface because edits and deletions do not bump a conversation'supdated_at. - With
notifications:read, pollnotifications.listwithupdated_afterset to the newestupdated_atyou have seen. - With
encounters:read, pollencounters.listwithupdated_afterset to the newestupdated_atyou have seen; a confirmation bumps it, so an encounter comes back once more when the other side confirms.tokens.getis one cheap call: re-fetch it on anytokens: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:
| Topic | Scope | Events | Fires when |
|---|---|---|---|
conversation:<conversation id> | messages:read | INSERT, UPDATE, DELETE, membership, conversation, chat_colour | A 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:read | INSERT, UPDATE, DELETE, ACCOUNT_BANNED, BOARDS | A 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:read | INSERT, UPDATE, DELETE | A 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:read | INSERT, UPDATE, supporter_status | The 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:read | INSERT, UPDATE | The 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) | Scope | Event | Refresh |
|---|---|---|---|
boards:<user id> | boards:read | BOARDS | Visible feeds, memberships, invitations, proposals, notices and activity |
board-drafts:<user id> | boards:drafts | DRAFTS | Your cloud draft heads |
blocks:<user id> | blocks:read | BLOCKS | Your block list and visible content |
privacy:<user id> | privacy:read | PRIVACY | Your 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_idas 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:writeis 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: trueis mandatory. A public channel with the same name joins happily and never receives anything.- The join is refused (a
phx_replywith"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 likePT403/PT401on the API: do not retry in a loop. recordandold_recordare 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.listwithchanged_since,conversations.listwithupdated_after,notifications.listwithupdated_after,friends.list/friends.requests_list,tokens.get,encounters.listwithupdated_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_tokenmessage, 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 throughnotifications.list; message previews require bothnotifications:readandmessages:read. - Presence needs
presence:read, andpresence:writeto appear online. Theapp_updatestopic 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.
expiresInis 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_idfromsession.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 tomessages.sendwith the sameclient_operation_id. image/jpeg,image/png,image/webpandimage/gifup to 10 MiB. Storage refuses other types and larger files;messages.sendrefuses amime_typeoutside that list withPT400 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.senddouble-checks the path (PT400 INVALID_ATTACHMENT_PATH) and that the file exists (PT404 ATTACHMENT_NOT_FOUND). - Omit
bodyfor 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_REQUIREDimmediately; 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.openandconversations.createthe same conversation; for the friend endpoints the same request or the sameremovedanswer. - For
messages.senda 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. Itsidis 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. Formessages.sendthat 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.getandpuzzles.get. - The
app_updatesRealtime topic, and sending Broadcast messages. - The standard Supabase surface: tables under
/rest/v1/, first-party RPCs, GraphQL,/auth/v1/user. All of it answers403or404to 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)