Apps and OAuth
An app is how someone else's frontend talks to undrstand on a user's behalf. You register it once, it asks users for the scopes it needs, and each user decides on a consent screen. This page covers registration, the scopes, and the authorization code flow with PKCE.
Why apps
Your own token is yours: it opens everything on your account. A frontend built for other people must not hold their tokens. It holds a token each user granted it, limited to what they approved.
That is what an app is: a registered client with a name, redirect URIs and a list of scopes it may ask for. A user who signs in to your app sees a consent screen with those scopes and decides.
Register an app
Register with your own account and your own token. An app cannot register apps for a user: the apps routes refuse tokens that were granted to an app.
curl -X POST https://api.undrstand.com/apps \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"name": "Pattern board",
"redirect_uris": ["https://board.example.com/callback", "http://localhost:5173/callback"],
"scopes": ["captures:read", "patterns:read", "patterns:review"],
"website_url": "https://board.example.com",
"confidential": false
}'Rules for the request: a name of at most a hundred characters; one to ten redirect URIs, https only, except http on localhost or 127.0.0.1 on any port while developing, and never a fragment or a custom scheme; at least one of the six scopes, and never mcp:use, which only Claude and ChatGPT get; an optional https website URL, shown on the consent screen.
{
"data": {
"id": "0199a3c2-…",
"name": "Pattern board",
"redirect_uris": ["https://board.example.com/callback", "http://localhost:5173/callback"],
"website_url": "https://board.example.com",
"scopes": ["captures:read", "patterns:read", "patterns:review"],
"confidential": false,
"secret": null,
"created_at": "2026-09-27T09:12:44.000000Z",
"updated_at": "2026-09-27T09:12:44.000000Z"
}
}The response is your app. Keep the id: it is the client_id in the flow. Change the name, redirect URIs, website or scopes later with a patch on the app, list your apps, or delete one, which revokes every token it was granted.
GET https://api.undrstand.com/apps # your apps, paginated (page, limit)
GET https://api.undrstand.com/apps/{id}
PATCH https://api.undrstand.com/apps/{id} # name, redirect_uris, website_url, scopes
DELETE https://api.undrstand.com/apps/{id} # 204, revokes every token the app was grantedPublic or confidential
An app is public by default: a browser page, a mobile app, anything that cannot keep a secret. It proves itself with PKCE alone.
Pass confidential true for an app with a server that can keep a secret. The secret is in the create response once and nowhere after; store it then. A confidential app sends it as client_secret at the token endpoint, next to PKCE.
The flow
Authorization code with PKCE, on the API domain. There is no other grant.
Make a code verifier and its challenge. The verifier is a random string of 43 to 128 characters; the challenge is its SHA-256 hash, base64url encoded.
PKCE// browser or Node 20+ const bytes = crypto.getRandomValues(new Uint8Array(32)); const base64url = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))) .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); const verifier = base64url(bytes); // keep it const challenge = base64url( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)), );Send the user to the authorize endpoint. Scopes are space separated; state is yours to check on the way back.
GET /oauth/authorizehttps://api.undrstand.com/oauth/authorize ?response_type=code &client_id=0199a3c2-… &redirect_uri=https://board.example.com/callback &scope=captures:read patterns:read &state=k3j9… &code_challenge=… &code_challenge_method=S256A user who is not signed in lands on the connect page first, signs in with a mailed link or a social account, and comes back. Then the consent screen lists the scopes you asked for, limited to the ones your app registered with. On approve, the user returns to your redirect URI with a code and your state.
https://board.example.com/callback?code=def502…&state=k3j9…Exchange the code for tokens. Send the verifier from step 1. A confidential app adds its client_secret.
POST /oauth/tokencurl -X POST https://api.undrstand.com/oauth/token \ -H "Content-Type: application/json" -H "Accept: application/json" \ -d '{ "grant_type": "authorization_code", "client_id": "0199a3c2-…", "redirect_uri": "https://board.example.com/callback", "code": "def502…", "code_verifier": "…" }' # {"token_type": "Bearer", "expires_in": 86400, # "access_token": "eyJ…", "refresh_token": "def502…"}Call the API with the access token as a bearer header, exactly like a personal token.
GET /patternscurl https://api.undrstand.com/patterns \ -H "Authorization: Bearer eyJ…" -H "Accept: application/json"
Scopes
A route refuses a token without its scope with a 403 that names the scope. The profile route needs a valid token and no scope, so an app can always tell who it is talking to.
HTTP/1.1 403 Forbidden
{"message": "This token is missing the captures:write scope."}| Scope | Opens | The user reads |
|---|---|---|
captures:read | list and read captures | Read what you handed in and what was found in it |
captures:write | post and delete captures | Hand in new text on your behalf and delete captures |
patterns:read | patterns, one pattern with its correlations and flows, the timeline | Read your patterns, correlations, flows and timeline |
patterns:review | accept or reject patterns and correlations | Accept or reject patterns and correlations for you |
settings:read | read settings | Read your settings |
settings:write | change settings | Change your settings |
Refresh and expiry
An access token lives one day, a refresh token thirty. Refresh before the day is over; the answer is a new pair and the old refresh token is gone.
curl -X POST https://api.undrstand.com/oauth/token \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"grant_type": "refresh_token",
"client_id": "0199a3c2-…",
"refresh_token": "def502…"
}'A user can pull the plug: deleting an app revokes its tokens, and so does deleting the account. Expect a 401 at any time and send the user through the flow again.
Rules for apps
- Ask only for the scopes you use. A pattern board needs patterns:read, not captures:write.
- Register exact redirect URIs. The flow refuses one that is not on the list.
- This is private life data. Show it, do not keep it: store nothing beyond what the user asked you to, and never move it to another service without saying so.
- Do not present a pattern as a diagnosis or a label. Describe what the data shows, in the words the description already uses.
- Never name the analysis method. The user gets the insight, not the machinery.