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.

POST /apps
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.

201 Created
{
  "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 granted

Public 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.

  1. 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)),
    );
  2. Send the user to the authorize endpoint. Scopes are space separated; state is yours to check on the way back.

    GET /oauth/authorize
    https://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=S256
  3. A 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…
  4. Exchange the code for tokens. Send the verifier from step 1. A confidential app adds its client_secret.

    POST /oauth/token
    curl -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…"}
  5. Call the API with the access token as a bearer header, exactly like a personal token.

    GET /patterns
    curl 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."}
ScopeOpensThe user reads
captures:readlist and read capturesRead what you handed in and what was found in it
captures:writepost and delete capturesHand in new text on your behalf and delete captures
patterns:readpatterns, one pattern with its correlations and flows, the timelineRead your patterns, correlations, flows and timeline
patterns:reviewaccept or reject patterns and correlationsAccept or reject patterns and correlations for you
settings:readread settingsRead your settings
settings:writechange settingsChange 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.

POST /oauth/token
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.

Where to next

  • API — The API guide has the resources your app will read and write.
  • /docs/api — The generated reference lists every route, field and error on the API domain.