API
Users and Collections
Resolve a user by address, read what they own and earn, manage their links and roles, and curate collections.
Every payout address in the protocol resolves to a user, so an address is the practical lookup
key: prefer /api/users/address/{stxAddress} over the numeric id wherever you have a chain
address in hand. Resolving many at once, use batch-lookup rather than a loop.
Identity is owned by the profile hub. A client app should render a profile and link out to
the hub to edit it, not write to PATCH /api/users/{id}.
| Method | Path | Auth | Purpose |
|---|
GET | /api/users | app key | List users. |
POST | /api/users | app key + user | Sign-up and profile upsert. The session token must prove the stxAddress in the body, or it is a 403. An invite code rides in the X-Invite-Code header or the body's inviteCode, and is claimed in the same transaction that creates the account, so an invalid code creates no user. A new user on an app that requires an invite and sends none is a 403 with code INVITE_REQUIRED. |
POST | /api/users/connect | app key + user | Sign-in handshake. Links an existing user to the calling app and answers { exists, linked }. It never creates a user: signup is POST /api/users. |
POST | /api/users/me/storage-wallet | app key + user | Record the caller's storage wallet: the Privy embedded EVM wallet the app created in their session. Confirmed against Privy; idempotent; 409 for a bring-your-own-wallet account. |
GET | /api/users/me/storage | app key + user | The caller's storage posture: wallet, free allowance (bytes and about-N-pages), credits in winc and as pages, whether room can be bought and on what terms, automatic destination, and how many owned editions are saved but not written. Null means the bundler could not be read. |
PATCH | /api/users/me/storage | app key + user | Set saveAsDefault (NONE or ARWEAVE). ARWEAVE needs a storage wallet. |
POST | /api/users/me/storage/checkout | app key + user | Start a card payment that adds room to the caller's own storage wallet. amount is in the currency's smallest unit and must sit inside the offer the posture carries. 409 when the paid rail is off or the caller has no storage wallet; 503 when the destination is not quoting. |
GET | /api/users/search | app key | Search users by name or address. |
POST | /api/users/batch-lookup | app key | Resolve many addresses to users in one call. Use this instead of a loop. |
DELETE | /api/users/{id} | app key + user | Delete a user. |
GET | /api/users/{id} | app key + user, entitlement | One user by numeric id, with the contact fields (email, xHandle, Privy ids) that every other user route blanks. Operator read: needs VIEW_ADMIN_PANEL. Apps look a person up by address. |
PATCH | /api/users/{id} | app key + user | Update a profile. Identity is owned by the profile hub - client apps should link out rather than write here. |
GET | /api/users/address/{stxAddress} | app key | One user by Stacks address. This is the lookup most apps want. |
GET | /api/users/address/{stxAddress}/profile | app key | Public profile readout: name, bio, avatar, links. |
| Method | Path | Auth | Purpose |
|---|
GET | /api/users/address/{stxAddress}/instances | app key | Everything this address owns. |
GET | /api/users/address/{stxAddress}/revenue | app key | What this address has earned, by source. |
GET | /api/users/{id}/contributions | app key | Every contribution row naming this user. |
GET | /api/users/address/{stxAddress}/collectors | app key | Who collects this creator. |
GET | /api/users/address/{stxAddress}/supporting | app key | Who this address collects. |
GET | /api/users/address/{stxAddress}/arweave | app key | Permanent-storage records for this address. |
| Method | Path | Auth | Purpose |
|---|
GET | /api/users/address/{stxAddress}/activity | app key | This address's activity stream. |
GET | /api/users/address/{stxAddress}/feed | app key | The feed shown on a profile. |
GET | /api/users/address/{stxAddress}/notifications | app key | In-app notifications for this address: the last things that involved it and that it did not do itself. appId narrows to one app, limit is 1-100 (default 30) and cursor pages with the nextCursor of the last answer. Answers items, nextCursor, unreadCount, unreadCapped and readAt. Never cached. |
POST | /api/users/address/{stxAddress}/notifications/read | app key + user | Move the read watermark, optionally to { readAt } (ISO timestamp, default now). Only the address's own session may: anyone else is a 403. It only moves forward, and is a separate call from the feed so that fetching never marks anything read. |
GET | /api/users/address/{stxAddress}/achievements | app key, user optional | The shelf: awards earned, plus exactly one next rung (or the graduated state) when the bearer token is the address. A stranger gets the awards and next: null. ?appId= says which app the viewer is standing in. |
GET | /api/achievements/ladder | app key | The ladder as a map: every rung in order, what earns it, what it pays, and its precursor - the rung that must be held first (null on the first rung and on the return family). No address and no bearer token - the rungs are the same for everybody. Join it with the shelf on slug to mark where a viewer stands and which rungs are locked. |
Push delivers the same items as the notification feed above, to a device, while the app is closed. Sending is switched off until the server is given a key: while it is, the key route answers 503 and a browser cannot subscribe without that key, so treat 503 as push not being offered. A browser subscription belongs to one origin, so a device subscribed in two apps holds two subscriptions. Each item is pushed through the app it belongs to, or through the hub when the person has no subscription in that app, and never through both.
| Method | Path | Auth | Purpose |
|---|
GET | /api/push/vapid-public-key | app key | The VAPID public key to pass as applicationServerKey. 503 when push is not configured, which means push is not offered. |
GET | /api/push/subscriptions | app key + user | Every device the signed-in person receives push on, across all apps. Endpoints are never returned. |
POST | /api/push/subscriptions | app key + user | Save this device: { appId, endpoint, keys: { p256dh, auth } }. The endpoint must belong to a browser push service (FCM, Mozilla, Apple, WNS). An app-scoped key may only name its own app. A person holds at most ten devices per app: an eleventh is a 409 TOO_MANY_DEVICES. |
DELETE | /api/push/subscriptions | app key + user | Stop pushing to a device: { endpoint } for this one, or { id } from the list. Only the caller's own rows. |
POST | /api/push/opens | app key + user | Record a tap: { deliveryId } from the payload. Stamps the ledger row once, and only when it is the caller's own; { recorded } either way. RisingTideProviders sends it for you. |
The service worker receives a PushPayload: the NotificationItem itself, a more count of items folded into the push, an app-neutral fallback sentence to show for any kind the app does not recognise, a badge count, and a deliveryId. Carry the id on the URL a tap opens, as ?via=push&pd=<deliveryId>, and the app records the tap when it loads.
| Method | Path | Auth | Purpose |
|---|
GET | /api/users/address/{stxAddress}/apps | app key | The app catalog as this user sees it, each entry OPEN, UNLOCKED or LOCKED. Membership comes from the catalog and the user's grants; activity only orders it, and never confers access. includeHidden=true adds the apps they hid from the rail. |
PATCH | /api/users/address/{stxAddress}/apps/{appId} | app key + user | Show or hide an app on the caller's own rail: { displayInRail }. Self only. A gated app the caller holds no grant for cannot be added; hiding is always allowed. |
GET | /api/users/{id}/apps | app key | App access and activity, by user id. |
GET | /api/users/{id}/links | app key | Social links. |
POST | /api/users/{id}/links | app key + user | Add a social link. |
DELETE | /api/users/{id}/links/{linkId} | app key + user | Remove a link. |
GET | /api/users/{id}/links/{linkId} | app key | One link. |
PATCH | /api/users/{id}/links/{linkId} | app key + user | Update a link. |
GET | /api/users/{id}/roles | app key | Roles held. |
POST | /api/users/{id}/roles | app key + user, entitlement | Grant a role. Needs MANAGE_ROLES. |
DELETE | /api/users/{id}/roles/{userRoleId} | app key + user, entitlement | Revoke a role grant. Needs MANAGE_ROLES. |
GET | /api/users/{id}/roles/{userRoleId} | app key | One role grant. |
GET | /api/users/{id}/invites | app key | Invites this user has claimed. bindingValue is always null here, because the route answers an app key and names no person. |
GET | /api/users/{id}/test-accounts | app key | Linked test accounts. |
POST | /api/users/{id}/test-accounts | app key + user | Link a test account. |
DELETE | /api/users/{id}/test-accounts/{accountId} | app key + user | Unlink a test account. |
GET | /api/users/{id}/test-accounts/{accountId} | app key | One test account. |
PATCH | /api/users/{id}/test-accounts/{accountId} | app key + user | Update a test account. |
| Method | Path | Auth | Purpose |
|---|
GET | /api/collections | app key | List collections. |
POST | /api/collections | app key + user, entitlement | Create a collection. Needs MANAGE_LISTINGS. |
DELETE | /api/collections/{id} | app key + user | Delete a collection. |
GET | /api/collections/{id} | app key | One collection. |
PATCH | /api/collections/{id} | app key + user | Update a collection. |
GET | /api/collections/by-slug/{slug} | app key | Resolve a collection by slug. |
GET | /api/collections/{id}/items | app key | Items in a collection. |
POST | /api/collections/{id}/items | app key + user | Add an item. |
DELETE | /api/collections/{id}/items/{itemId} | app key + user | Remove an item. |
PATCH | /api/collections/{id}/items/{itemId} | app key + user | Update an item. |
POST | /api/collections/{id}/items/reorder | app key + user | Reorder items. |
GET | /api/collections/{id}/analytics | app key | Aggregate counters for a collection. |