PocketPass API

Read and write PocketPass user data with their permission. Access is controlled via OAuth 2.0.

https://pocketpass.xyz/functions/v1/api-v1

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

ScopeAccess
read:profileName, avatar, origin, age, greeting, mood, games, friend code, stats
read:encountersEncounter history with other users
read:friendsFriend list with basic profile info
read:leaderboardGlobal leaderboard rankings
read:spotpassSpotPass items and active events
read:messagesDirect-message metadata (sender, timestamps — not content, see E2EE)
read:notificationsThe user's notifications
read:chatroomsThe user's chatrooms and chatroom message metadata
write:profileUpdate greeting and mood
write:messagesSend messages to friends
write:notificationsMark notifications as read
write:friendsSend friend requests by friend code and remove friendships
read:eventsPoll the user's event stream via /events?since=
read:webhooksList registered webhooks and inspect delivery history
write:webhooksRegister and remove webhooks

Health

GET /health no auth

Unauthenticated health probe. Useful for status pages and uptime monitors.

{
  "status": "ok",
  "version": "1.1.0",
  "time": "2026-05-14T18:47:00.672Z"
}

Profile

GET /me read: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": { ... }
}
PUT /profile write:profile

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)

GET /profile/by-friend-code/:code read:profile

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
}
GET /me/scopes any token

Inspect the scopes granted to the current access token.

{
  "scopes": ["read:profile", "read:encounters"],
  "user_id": "312c8f81-ae71-...",
  "app_id": "uuid"
}

Stats

GET /me/stats read:profile

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

GET /me/avatar read:profile

Returns the user's Piip as a PNG image.

ParamDefaultDescription
width270Image width: 128, 256, or 512
typefaceface, 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' }
})
GET /avatar/:userId read:profile

Returns any user's Piip avatar by their user ID. Same query params as /me/avatar.

POST /me/avatar write:profile

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

GET /encounters read:encounters

Returns the user's encounter history, sorted newest first. Cursor-paginated — see Pagination.

ParamDefaultDescription
limit50Max 100
cursorOpaque 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"
}
GET /encounters/:id read:encounters

Single encounter detail by encounter_id. Returns 404 if not found.

Encounter Heatmap

GET /encounters/heatmap read:encounters

Daily encounter counts over a sliding window, ordered oldest first. Days with zero encounters are included (count: 0). Great for activity-graph widgets.

ParamDefaultDescription
days30Window length in days (1–365)
{
  "days": 30,
  "total": 87,
  "data": [
    { "date": "2026-04-15", "count": 0 },
    { "date": "2026-04-16", "count": 3 },
    ...
  ]
}

Friends

GET /friends read: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"
  }
]
POST /friends/request write:friends

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" }
DELETE /friends/:userId write:friends

Remove a friendship (in either direction).

Leaderboard

GET /leaderboard read:leaderboard

Returns global leaderboard rankings.

ParamDefaultDescription
sorttotal_encountersSort field (see below)
limit50Max 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
  }
]
GET /leaderboard/around-me read:leaderboard

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.

ParamDefaultDescription
sorttotal_encountersSame options as /leaderboard
context5Total 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

GET /spotpass read: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

GET /messages read: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.

ParamDefaultDescription
limit50Max 100
cursorOpaque next_cursor from a prior response
withFilter 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
}
POST /messages write:messages

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

GET /notifications read:notifications

List the user's notifications, newest first.

ParamDefaultDescription
limit50Max 100
cursorOpaque cursor
unread_onlyfalseFilter 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
}
PATCH /notifications/:id/read write:notifications

Mark a notification as read. Idempotent.

Chatrooms

GET /chatrooms read: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"
  }
]
GET /chatrooms/:id/messages read:chatrooms

List chatroom message metadata, newest first. Message content is end-to-end encrypted and not exposed via the API. See End-to-End Encryption.

ParamDefaultDescription
limit50Max 100
cursorOpaque 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.

GET /events read:events

Poll for new events since a given event ID. Stable cursor: event IDs are monotonically increasing integers.

ParamDefaultDescription
since0Return events with id > since. Use next_since from a prior response.
limit50Max 100
event_typeFilter 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

EventFires when
notification.createdA 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.

GET /webhooks read:webhooks

List the user's registered webhooks.

POST /webhooks write: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"
}
DELETE /webhooks/:id write:webhooks

Remove a webhook. Pending undelivered events for this webhook are discarded.

GET /webhooks/:id/deliveries read:webhooks

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:

HeaderValue
Content-Typeapplication/json
X-PocketPass-EventThe event type
X-PocketPass-DeliveryUnique delivery ID (use to dedupe replays)
X-PocketPass-Signaturesha256=<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:

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:

Cursor pagination is stable under inserts/deletes — offset pagination is not.

Endpoints currently using cursor pagination: /encounters, /messages, /notifications, /chatrooms/:id/messages.

Error Codes

StatusMeaning
400Bad request — missing fields or invalid data
401Missing or invalid access token
403Token doesn't have the required scope
404Endpoint or resource not found
429Rate limited
500Server 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:

HeaderMeaning
X-RateLimit-LimitMaximum requests per minute for this app
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetUnix 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

FieldTypeDescription
idUUIDUser ID
user_namestringDisplay name
avatar_hexstringPiip data (base64 studio encoding)
greetingstringPersonal greeting shown on encounter
moodstringCurrent mood emoji
originstringHome region/country
agestringAge (user-set)
hobbiesstringHobbies/interests
is_malebooleanGender for Piip body type
friend_codestring8-digit friend code
birthdaystringBirthday in MM-DD format (nullable)
selected_gamesJSONList of favorite games
token_balanceintegerCurrent token count

Encounter

FieldTypeDescription
other_user_idUUIDEncountered user's ID
other_user_namestringTheir display name
other_user_avatar_hexstringTheir Piip data
originstringTheir home region
greetingstringTheir greeting at time of encounter
meet_countintegerTimes met this person
timestamplongUnix epoch millis

Leaderboard Entry

FieldTypeDescription
user_idUUIDUser ID
user_namestringDisplay name
total_encountersintegerTotal encounters (including repeats)
unique_encountersintegerUnique people met
unique_regionsintegerDistinct regions encountered
puzzles_completedintegerPuzzle panels completed
achievements_unlockedintegerAchievements earned