PocketPass API
Read and write PocketPass user data with their permission. Access is controlled via OAuth 2.0.
Overview
The PocketPass API gives third-party apps access to user profiles, encounter history, friend lists, leaderboards, and more. All requests require an OAuth access token scoped to the data you need.
The API follows REST conventions: JSON request/response bodies, standard HTTP methods, and meaningful status codes.
Authentication
1. Register your app
Go to the Developer Dashboard, sign in, and create an app. You'll receive a client_id and client_secret.
The redirect_uri is optional. If omitted, the auth code will be displayed on screen for the user to copy (useful for CLI tools, desktop apps, or testing).
Scope allowlist
Each app declares the scopes it may request, on the dashboard. New apps default to the five read scopes (read:profile, read:encounters, read:friends, read:leaderboard, read:spotpass). Toggle chips on the app card to add or remove scopes from the allowlist.
Requesting a scope outside the allowlist at /authorize is rejected with 403 scope_not_allowed. You can tighten or expand the allowlist at any time — changes take effect on the next API call, since existing tokens are intersected with the current allowlist at request time.
2. Redirect user to authorize
// Send the user here
https://dev.pocketpass.xyz/authorize
?client_id=YOUR_CLIENT_ID
&scopes=read:profile,read:encounters
&redirect_uri=https://yourapp.com/callback
The user logs in and approves the requested scopes, then gets redirected back with an authorization code:
https://yourapp.com/callback?code=AUTH_CODE
3. Exchange code for token
POST https://pocketpass.xyz/functions/v1/api-token { "client_id": "your_client_id", "client_secret": "your_client_secret", "code": "AUTH_CODE" }
Response:
{
"access_token": "abc-123-...",
"token_type": "Bearer",
"scopes": ["read:profile", "read:encounters"]
}
Tokens do not expire. Users can revoke access at any time from social.pocketpass.xyz.
PKCE (recommended for public clients)
For mobile apps and SPAs, use PKCE to prevent authorization code interception:
// 1. Generate code_verifier (random 43-128 char string) // 2. Compute code_challenge = base64url(SHA-256(code_verifier)) // 3. Add code_challenge to authorize URL ?client_id=...&scopes=...&code_challenge=HASH // 4. Include code_verifier in token exchange { "code_verifier": "ORIGINAL", ... }
State parameter
Pass a random state value in the authorize URL. It's returned in the redirect so you can verify the response.
4. Make API calls
GET /me
Authorization: Bearer YOUR_ACCESS_TOKEN
Scopes
| Scope | Access |
|---|---|
read:profile | Name, avatar, origin, age, greeting, mood, games, friend code, stats |
read:encounters | Encounter history with other users |
read:friends | Friend list with basic profile info |
read:leaderboard | Global leaderboard rankings |
read:spotpass | SpotPass items and active events |
read:messages | Direct-message metadata (sender, timestamps — not content, see E2EE) |
read:notifications | The user's notifications |
read:chatrooms | The user's chatrooms and chatroom message metadata |
write:profile | Update greeting and mood |
write:messages | Send messages to friends |
write:notifications | Mark notifications as read |
write:friends | Send friend requests by friend code and remove friendships |
read:events | Poll the user's event stream via /events?since= |
read:webhooks | List registered webhooks and inspect delivery history |
write:webhooks | Register and remove webhooks |
Health
Unauthenticated health probe. Useful for status pages and uptime monitors.
{
"status": "ok",
"version": "1.1.0",
"time": "2026-05-14T18:47:00.672Z"
}
Profile
Returns the authorized user's profile.
{
"id": "312c8f81-ae71-...",
"user_name": "simply0004",
"avatar_hex": "AwEA...",
"greeting": "Hello!",
"mood": "HAPPY",
"origin": "Sweden",
"age": "18",
"hobbies": "Gaming",
"is_male": true,
"friend_code": "56814231",
"selected_games": ["Animal Crossing", "Mario Kart"],
"token_balance": 42,
"puzzle_progress": { ... }
}
Update the user's greeting, mood, or birthday. Only the fields you send are updated.
{
"greeting": "Hey there!",
"mood": "EXCITED"
}
Valid moods: HAPPY, SAD, EXCITED, SLEEPY, ANGRY, COOL, SHY, CONFUSED
Birthday format: MM-DD (e.g. 03-26 for March 26)
Look up a public profile by 8-digit friend code. Returns only public fields — no token balance, no birthday, no friend code echo.
{
"id": "uuid",
"user_name": "Bubbleeey",
"avatar_hex": "AwEA...",
"greeting": "Hi!",
"mood": "HAPPY",
"origin": "UK",
"is_male": true
}
Inspect the scopes granted to the current access token.
{
"scopes": ["read:profile", "read:encounters"],
"user_id": "312c8f81-ae71-...",
"app_id": "uuid"
}
Stats
Aggregated counts for dashboards. Cheap to query — one call instead of fetching full lists to count them.
{
"token_balance": 42,
"total_encounters": 247,
"unique_encounters": 189,
"friend_count": 23,
"puzzle_progress": { ... },
"leaderboard": {
"total_encounters": 247,
"unique_encounters": 189,
"unique_regions": 14,
"puzzles_completed": 8,
"achievements_unlocked": 12
}
}
Avatar
Returns the user's Piip as a PNG image.
| Param | Default | Description |
|---|---|---|
width | 270 | Image width: 128, 256, or 512 |
type | face | face, face_body, or all_body |
Response: image/png binary data.
// Embed in HTML <img src="https://pocketpass.xyz/functions/v1/api-v1/me/avatar?width=256" /> // Fetch with token fetch('/me/avatar?width=128', { headers: { 'Authorization': 'Bearer TOKEN' } })
Returns any user's Piip avatar by their user ID. Same query params as /me/avatar.
Update the user's Piip avatar data, and optionally their card style.
{
"avatar_hex": "AAcOVV1cZml0fXiCipOepa6rsrnAx87W5ejv7PT7/sXIz9THy9/i/PgDCgcKEiI=",
"card_style": "classic"
}
avatar_hex must be exactly 64 base64 characters — the Piip studio encoding used throughout the app. To generate one programmatically you'll need a Piip studio implementation; for most apps it's easier to read the user's existing avatar_hex from GET /me and let them edit it elsewhere.
Valid card_style values: classic, gradient, cool, warm, ocean, sunset, forest, royal, neon, midnight, cherry, rainbow.
Encounters
Returns the user's encounter history, sorted newest first. Cursor-paginated — see Pagination.
| Param | Default | Description |
|---|---|---|
limit | 50 | Max 100 |
cursor | — | Opaque next_cursor from a prior response |
{
"data": [
{
"encounter_id": "abc123",
"other_user_id": "uuid",
"other_user_name": "Nashi",
"other_user_avatar_hex": "AwEA...",
"origin": "Japan",
"greeting": "Hello!",
"meet_count": 3,
"timestamp": 1711152000000
}
],
"next_cursor": "eyJ0aW1lc3RhbXAiOjE3MTExNTIwMDAwMDB9"
}
Single encounter detail by encounter_id. Returns 404 if not found.
Encounter Heatmap
Daily encounter counts over a sliding window, ordered oldest first. Days with zero encounters are included (count: 0). Great for activity-graph widgets.
| Param | Default | Description |
|---|---|---|
days | 30 | Window length in days (1–365) |
{
"days": 30,
"total": 87,
"data": [
{ "date": "2026-04-15", "count": 0 },
{ "date": "2026-04-16", "count": 3 },
...
]
}
Friends
Returns the user's accepted friends with basic profile info.
[
{
"id": "uuid",
"user_name": "Bubbleeey",
"avatar_hex": "AwEA...",
"origin": "UK",
"greeting": "Hi!",
"mood": "HAPPY",
"friend_code": "08375706"
}
]
Send a friend request by 8-digit friend code. Returns 409 if a friendship (pending or accepted) already exists.
{ "friend_code": "08375706" }
{ "success": true, "addressee_id": "uuid" }
Remove a friendship (in either direction).
Leaderboard
Returns global leaderboard rankings.
| Param | Default | Description |
|---|---|---|
sort | total_encounters | Sort field (see below) |
limit | 50 | Max 100 |
Sort options: total_encounters, unique_encounters, unique_regions, puzzles_completed, achievements_unlocked
[
{
"user_id": "uuid",
"user_name": "xevylicious",
"avatar_hex": "AwEA...",
"total_encounters": 247,
"unique_encounters": 189,
"unique_regions": 14,
"puzzles_completed": 8,
"achievements_unlocked": 12
}
]
The user's rank plus a window of nearby competitors — ideal for "where do I sit" widgets without paging through the whole board. Each neighbor row is annotated with rank and is_me.
| Param | Default | Description |
|---|---|---|
sort | total_encounters | Same options as /leaderboard |
context | 5 | Total rows to return centered on the user (1–25) |
{
"own_rank": 12,
"sort": "total_encounters",
"neighbors": [
{ "rank": 10, "user_name": "...", "is_me": false, ... },
{ "rank": 11, "user_name": "...", "is_me": false, ... },
{ "rank": 12, "user_name": "you", "is_me": true, ... },
{ "rank": 13, "user_name": "...", "is_me": false, ... },
{ "rank": 14, "user_name": "...", "is_me": false, ... }
]
}
If the user has no leaderboard row (privacy toggle off), own_rank is null and neighbors is empty.
SpotPass
Returns all published SpotPass items (puzzle panels and events).
[
{
"id": "uuid",
"type": "puzzle_panel",
"title": "Castle Collection",
"description": "A medieval castle puzzle",
"panel_image_url": "https://...",
"published_at": "2026-03-20T12:00:00Z",
"expires_at": null
},
{
"id": "uuid",
"type": "event",
"title": "Double Token Weekend",
"event_effect": "{\"type\":\"token_multiplier\",\"value\":2}",
"published_at": "2026-03-25T00:00:00Z",
"expires_at": "2026-03-27T00:00:00Z"
}
]
Messages
List direct-message metadata, newest first. Message content is end-to-end encrypted and not exposed via the API. See End-to-End Encryption.
| Param | Default | Description |
|---|---|---|
limit | 50 | Max 100 |
cursor | — | Opaque next_cursor from a prior response |
with | — | Filter to a single conversation by friend user_id |
{
"data": [
{
"id": "uuid",
"sender_id": "uuid",
"receiver_id": "uuid",
"created_at": "2026-05-14T18:00:00Z",
"read_at": "2026-05-14T18:01:13Z",
"deleted_at": null,
"media_type": null,
"has_media": false
}
],
"next_cursor": null
}
Send a text message to a friend. The receiver must be in the user's friend list.
{
"receiver_id": "friend-uuid",
"content": "Hello from the API!"
}
Returns 200 OK on success.
Image messages are not supported via the API. Image messages in PocketPass are end-to-end encrypted with per-message keys wrapped using the sender's private EC key, which lives only on the user's device. The server has no way to perform that wrapping on the user's behalf. If an integration needs to send images, the user can do so from the app itself.
Notifications
List the user's notifications, newest first.
| Param | Default | Description |
|---|---|---|
limit | 50 | Max 100 |
cursor | — | Opaque cursor |
unread_only | false | Filter to unread notifications only |
Notification types: friend_request, friend_accepted, new_message, new_encounter, mention
{
"data": [
{
"id": "uuid",
"type": "friend_accepted",
"title": "Friend request accepted",
"body": "Bubbleeey accepted your request",
"related_user_id": "uuid",
"related_user_name": "Bubbleeey",
"related_user_avatar_hex": "AwEA...",
"read": false,
"created_at": "2026-05-14T17:30:00Z",
"room_id": null,
"room_name": null
}
],
"next_cursor": null
}
Mark a notification as read. Idempotent.
Chatrooms
List the chatrooms the user is a member of.
[
{
"id": "uuid",
"name": "Mii gang!",
"is_public": true,
"join_mode": "auto",
"avatar_url": "https://...",
"created_at": "2026-04-22T15:00:00Z",
"created_by": "uuid",
"member_role": "member",
"joined_at": "2026-04-24T20:50:49Z",
"last_read_at": "2026-05-14T18:00:00Z"
}
]
List chatroom message metadata, newest first. Message content is end-to-end encrypted and not exposed via the API. See End-to-End Encryption.
| Param | Default | Description |
|---|---|---|
limit | 50 | Max 100 |
cursor | — | Opaque cursor |
{
"data": [
{
"id": "uuid",
"room_id": "uuid",
"sender_id": "uuid",
"created_at": "2026-05-14T18:00:00Z",
"media_type": null,
"has_media": false,
"mentions": ["uuid"]
}
],
"next_cursor": null
}
Events
The event stream is an append-only log of things that happened to the user. Apps can either poll via GET /events?since= or subscribe via webhooks — the same event payload is delivered both ways.
Poll for new events since a given event ID. Stable cursor: event IDs are monotonically increasing integers.
| Param | Default | Description |
|---|---|---|
since | 0 | Return events with id > since. Use next_since from a prior response. |
limit | 50 | Max 100 |
event_type | — | Filter to a single event type (e.g. notification.created) |
{
"data": [
{
"id": 42,
"event_type": "notification.created",
"created_at": "2026-05-14T18:30:00Z",
"payload": { ...notification fields... }
}
],
"next_since": 42
}
When next_since is null, you've reached the end. Pass that value as ?since= on your next poll.
Event types
| Event | Fires when |
|---|---|
notification.created | A notification appears in the user's inbox — covers friend requests, accepts, new messages (metadata), mentions, encounters |
More event types will be added over time. Subscribe only to the events you care about.
Webhooks
Register an HTTPS URL and we'll POST events to it. Each delivery is signed with HMAC-SHA256 so you can verify it came from us.
List the user's registered webhooks.
Register a new webhook. The response is the only time the secret is returned — store it immediately.
{
"url": "https://yourapp.com/webhooks/pocketpass",
"event_types": ["notification.created"]
}
{
"id": "uuid",
"url": "https://yourapp.com/webhooks/pocketpass",
"event_types": ["notification.created"],
"active": true,
"secret": "whsec_...",
"created_at": "2026-05-14T18:30:00Z"
}
Remove a webhook. Pending undelivered events for this webhook are discarded.
Inspect recent delivery attempts for a webhook. Useful for debugging when events aren't arriving.
Delivery format
Each event POST has this body:
{
"event": "notification.created",
"event_id": 42,
"delivery_id": 7,
"attempt": 1,
"created_at": "2026-05-14T18:30:00Z",
"data": { ...event-specific payload... }
}
And these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-PocketPass-Event | The event type |
X-PocketPass-Delivery | Unique delivery ID (use to dedupe replays) |
X-PocketPass-Signature | sha256=<hmac hex> |
Verifying signatures
Compute HMAC-SHA256(secret, raw_request_body) and compare with the X-PocketPass-Signature header. Use a constant-time comparison.
// Node.js const crypto = require('crypto') function verify(rawBody, secret, header) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex') return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(header) ) }
Retry behavior
If your endpoint returns a non-2xx status or times out (10s), we retry with exponential backoff:
- Attempt 1: immediate
- Attempt 2: 30 seconds later
- Attempt 3: 2 minutes later
- Attempt 4: 10 minutes later
- Attempt 5: 1 hour later
- Then we give up.
After 20 consecutive failures the webhook is automatically deactivated. Reactivate by registering it again.
Tip: respond with 200 OK as fast as possible and queue any heavy work async. Webhooks that take > 10 seconds to respond are treated as failures.
Pagination
List endpoints with potentially large result sets use cursor-based pagination:
- Pass
?limit=N(default 50, max 100). - Response shape is
{ "data": [...], "next_cursor": "<opaque>" | null }. - To fetch the next page, pass
?cursor=<next_cursor>(URL-safe, base64-encoded). Whennext_cursorisnull, you've reached the end. - The cursor encodes the sort field of the last row returned. Don't rely on its internal shape — treat it as opaque.
Cursor pagination is stable under inserts/deletes — offset pagination is not.
Endpoints currently using cursor pagination: /encounters, /messages, /notifications, /chatrooms/:id/messages.
Error Codes
| Status | Meaning |
|---|---|
400 | Bad request — missing fields or invalid data |
401 | Missing or invalid access token |
403 | Token doesn't have the required scope |
404 | Endpoint or resource not found |
429 | Rate limited |
500 | Server error |
Error responses include a JSON body:
{
"error": "Token does not have the required scope: read:friends"
}
Rate Limits
New apps start at 60 requests per minute per access token. Exceeding the limit returns 429.
Every authenticated response includes rate-limit headers so well-behaved clients can self-throttle:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Maximum requests per minute for this app |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
Requesting a higher limit
Need more than 60/min? Open your app on the developer dashboard and click Request higher limit. Enter the number you need and a short justification. Every request is human-reviewed; you'll see the decision (approved or declined, with an optional note) back on the dashboard. Once approved, the new ceiling takes effect on your next API call — no token or code change required.
End-to-End Encryption
Direct messages and chatroom messages are end-to-end encrypted in PocketPass. The encryption keys are derived from the user's password and never leave the user's device. The Supabase server only stores ciphertext.
This means the API cannot decrypt message content. The read:messages and read:chatrooms scopes therefore expose only metadata: sender, receiver, timestamps, read state, and whether the message has a media attachment. They do not expose the message body or the media itself.
This is intentional and the current API surface reflects that trade-off. If you're building an integration that needs plaintext, the user's app itself can forward the decrypted content (which is how POST /messages already works for sends).
Data Models
Profile
| Field | Type | Description |
|---|---|---|
id | UUID | User ID |
user_name | string | Display name |
avatar_hex | string | Piip data (base64 studio encoding) |
greeting | string | Personal greeting shown on encounter |
mood | string | Current mood emoji |
origin | string | Home region/country |
age | string | Age (user-set) |
hobbies | string | Hobbies/interests |
is_male | boolean | Gender for Piip body type |
friend_code | string | 8-digit friend code |
birthday | string | Birthday in MM-DD format (nullable) |
selected_games | JSON | List of favorite games |
token_balance | integer | Current token count |
Encounter
| Field | Type | Description |
|---|---|---|
other_user_id | UUID | Encountered user's ID |
other_user_name | string | Their display name |
other_user_avatar_hex | string | Their Piip data |
origin | string | Their home region |
greeting | string | Their greeting at time of encounter |
meet_count | integer | Times met this person |
timestamp | long | Unix epoch millis |
Leaderboard Entry
| Field | Type | Description |
|---|---|---|
user_id | UUID | User ID |
user_name | string | Display name |
total_encounters | integer | Total encounters (including repeats) |
unique_encounters | integer | Unique people met |
unique_regions | integer | Distinct regions encountered |
puzzles_completed | integer | Puzzle panels completed |
achievements_unlocked | integer | Achievements earned |