Acting on behalf of users
Origin is in Early Beta and subject to change.
Origin Apps use installation user tokens to act on behalf of members of the namespace they're installed in. Each request is limited to permissions held by both the installation and the user.
Use the Origin API base URL and error model. Mint tokens with an app JWT.
How it works
- A workspace admin approves the
namespace:user_tokens:writescope for your app's installation. - Optionally, confirm the user's Cursor identity. Origin returns a signed receipt with their
user_…ID and the installation they confirmed for. - Sign an app JWT and mint an installation user token for the user's ID or email.
- Use the installation user token with the REST API or Git over HTTPS until
expiresAt, then mint another.
Request the scope
Add namespace:user_tokens:write to the scope parameter of the installation URL. For an existing installation, also send include_granted_scopes=true so the admin approves only the addition.
https://cursor.com/codebase/apps/install ?client_id=APP_ID &scope=namespace:user_tokens:write%20repository:pull_requests:write &redirect_uri=REGISTERED_CALLBACK &state=RANDOM_ANTI_FORGERY_VALUE &include_granted_scopes=trueThis scope lets the installation mint tokens for any active namespace member. It can't be included in a token's scopes. Other admin-approved scopes still cap each token's permissions.
When to confirm a user
User confirmation is optional: the admin's approval of namespace:user_tokens:write covers every namespace member.
Confirm a user to link an account in your product to their Cursor account, or to verify an identity instead of trusting a typed email address. The receipt proves the signed-in user's user_… ID, email, and namespace membership at confirmation time.
Receipts prove identity and grant no permissions. Create Installation User Token neither accepts nor requires one.
User confirmation
Send the user to Origin
Open this URL in the user's browser. Name the installation when you know which of the user's namespaces your app is installed in:
https://cursor.com/codebase/apps/user-confirmation ?installation_id=INSTALLATION_ID &redirect_uri=REGISTERED_CALLBACK &state=RANDOM_ANTI_FORGERY_VALUEName your app instead when you don't. Origin finds the installations the user can confirm for:
https://cursor.com/codebase/apps/user-confirmation ?client_id=APP_ID &redirect_uri=REGISTERED_CALLBACK &state=RANDOM_ANTI_FORGERY_VALUE| Parameter | Required | Description |
|---|---|---|
installation_id | One of installation_id or client_id | The i_… ID of your app's active installation in the user's namespace. |
client_id | One of installation_id or client_id | Your app's app_… ID. On its own, it resolves to the app's active installations in the namespaces the user belongs to. Alongside installation_id, the installation is used and client_id must be the ID of the app it belongs to; any other value makes the link invalid. |
redirect_uri | Yes | Callback URI. Must exactly match an entry in the app's installationRedirectUris, the installation flow's allowlist. A loopback URI, such as http://127.0.0.1:PORT/callback or http://localhost:PORT/callback, is accepted on any port without being registered. |
state | Strongly recommended | Random anti-forgery value, echoed as the state claim of the confirmation receipt. |
Send each parameter at most once. A link that names neither an installation nor an app, omits redirect_uri, or repeats a parameter produces an error after sign-in.
What the user sees
Signed-out users sign in to Cursor, then return to the same link with its parameters. Origin validates parameters after sign-in, so even malformed links go through sign-in before showing an error.
A link that names your app resolves to one installation first. Origin lists the app's active installations in the namespaces the user belongs to: their personal namespace, then their teams by slug. With one installation, the page continues as if the link had named it. With several, the user picks the namespace whose membership they want to confirm. With none, the user sees an error and nothing is confirmed; see Confirmation errors.
Origin shows your app's name, icon, and description alongside the installation's namespace. It also lists the details your app will receive:
- Cursor user ID (
user_…) - Origin namespace slug and ID
The page explains that confirmation shares identity and current namespace membership without granting repository access. The user chooses Confirm or Cancel.
Only active members of the owning team, or the owner of a personal namespace, can confirm.
Callback
On confirmation, Origin redirects to your callback:
https://app.example.com/origin/confirm?confirmation_receipt=RECEIPT_JWT&state=RANDOM_ANTI_FORGERY_VALUEVerify the confirmation receipt before reading its user claims. The callback includes state only if the confirmation URL supplied a non-empty value. Trust the signed state claim rather than the query parameter.
Canceling or failing to confirm sends no callback or redirect. Treat a missing callback as "not confirmed" and let the user restart.
Confirmation receipt
confirmation_receipt is a compact JWT signed by Origin with the same keys as installation receipts.
JOSE header:
{ "alg": "EdDSA", "kid": "origin-key-id", "typ": "origin-user-confirmation-receipt+jwt"}Claims:
{ "iss": "https://api.cursor.com/v1/origin", "aud": "app_01...", "sub": "user_01...", "installation_id": "i_01...", "namespace_id": "ns_01...", "email": "user@example.com", "iat": 1786465200, "exp": 1786465500, "jti": "RECEIPT_UUID", "state": "ORIGINAL_VALUE"}audis your app ID;subis the confirmed user'suser_…ID, used asuserIdwhen minting.installation_idandnamespace_ididentify the installation and namespace where membership was confirmed. For a link that named your app, they tell you which installation the user chose.emailis the user's account email at confirmation time.- Receipts expire five minutes after issuance.
jtiis unique per receipt. stateis included only if the confirmation URL supplied a non-empty value.
Verify the receipt before trusting the callback:
- Resolve the signing key from the JWKS by the
kidheader. - Require
alg: EdDSAandtyp: origin-user-confirmation-receipt+jwtto distinguish confirmation receipts from installation receipts and access tokens. - Validate the signature,
iss,aud, andexp. - Match the signed
stateto the value you sent. When the link named an installation, also matchinstallation_id. When it named your app, readinstallation_idfrom the receipt; it's the installation to mint for.
Reject the callback when any check fails.
Confirmation errors
Failures stay on the Cursor page without calling your callback.
| Error | Cause |
|---|---|
| Invalid link | The link names neither an installation nor an app, omits redirect_uri, repeats a parameter, or pairs installation_id with a client_id that isn't the app the installation belongs to. |
| Not authorized | The installation doesn't exist or isn't active, redirect_uri isn't registered for the app, or the user isn't a member of the installation's namespace. For a link that names an app, the app doesn't exist, or only its owner can install it and the user has no installation to confirm for. The page doesn't say which. |
| Not installed | The link names an app anyone can install, and the app has no active installation in a namespace the user belongs to. The page names the app. |
| Temporarily unavailable | Origin couldn't sign the receipt after confirmation. The user can retry. |
Mint an installation user token
Call Create Installation User Token, POST /v1/origin/app/installations/{installationId}/user_access_tokens, with an app JWT. Set exactly one of userId or userEmail.
curl --request POST \ --url 'https://api.cursor.com/v1/origin/app/installations/INSTALLATION_ID/user_access_tokens' \ --header 'Authorization: Bearer APP_JWT' \ --header 'Content-Type: application/json' \ --data '{ "userId": "user_01...", "scopes": [ "repository:pull_requests:write" ], "repositoryIds": [ "repo_01..." ]}'{ "token": "YOUR_INSTALLATION_USER_TOKEN", "expiresAt": "2026-01-01T00:15:00Z"}| Field | Description |
|---|---|
userId | The user's user_… ID from a confirmation receipt's sub or an actor payload. |
userEmail | The user's account email. It must match exactly one active member of the installation's namespace. |
scopes | Optional limit. Values must be unique and approved for the installation. namespace:user_tokens:write and scopes prefixed with app: or installation: can't be delegated. Empty or omitted uses current permissions. |
repositoryIds | Optional limit of up to 50 unique repository IDs accessible to the installation. Empty or omitted uses current permissions. |
The user must have an active Cursor account and meet the namespace membership rules. Unknown users, non-members, and emails matching several members return the same 403.
With both scopes and repositoryIds set, minting succeeds only if the installation and user each hold every requested scope on every listed repository. With fewer limits, permissions are checked on each request instead.
Lifetime
expiresAt is at most 15 minutes after minting and never later than the app JWT's exp, as with installation access tokens. There's no refresh token: mint another with a fresh app JWT after expiry. Each mint costs 1 point against the app JWT's rate limit.
Use the token
Treat installation user tokens as opaque: don't inspect or parse their contents. Send the token as a Bearer credential to the REST API, or as the password for Git over HTTPS with the username x-access-token:
Authorization: Bearer YOUR_INSTALLATION_USER_TOKENEach request must fit the installation's approved scopes and repository selection, the user's Origin grants, and the token's limits. Otherwise it returns 403, or 404 if the resource isn't visible.
Actor fields name the user and can include performedVia.app with your app's id and optional displayName. Attribution applies to the action that field describes: on a comment's author, it identifies the app that created the comment. It doesn't identify an app that later edited or deleted it. performedVia is absent for direct user actions and can be absent when delegation data is unavailable.
Mint errors
| HTTP status | Cause |
|---|---|
400 | The request doesn't set exactly one of userId or userEmail, uses an invalid user_… ID or email address, includes duplicate, malformed, or non-delegable scopes, or includes duplicate repository IDs or more than 50. |
401 | The app JWT is invalid or expired, or the installation doesn't exist, belongs to another app, or is suspended. |
403 | The installation lacks namespace:user_tokens:write, the requested limits exceed the installation's grant, the user isn't eligible, or a request with both limits includes a permission the installation or user lacks. |
429 | The app JWT's rate limit is exhausted. See Exceeding the limit. |
503 | Origin couldn't look up the user or sign the token. Retry with backoff. |
Revocation
Individual tokens can't be revoked. These changes affect token access:
- Uninstalling or deleting the app invalidates its user tokens before
expiresAt. Requests return401. - Closing the user's Cursor account invalidates their tokens. Requests return
401. - Removing
namespace:user_tokens:writeor suspending the installation stops new mints. Issued tokens expire atexpiresAt. - Changes to the installation's or the user's grants apply no later than
expiresAt.
Security notes
- Mint just in time with the smallest
scopesandrepositoryIdsneeded. Treat tokens like passwords; don't store or log them. - Key linked accounts on
sub, notemail: the user'suser_…ID is stable; their email can change. - Verify email addresses before minting with
userEmail. A typed address can name a different member. - Use each receipt once. Record its
jtiand reject repeats within its five-minute lifetime. - A receipt isn't a credential: don't send it as a Bearer token or log it, since it contains the user's email.
- Receipts describe membership at confirmation time. Each mint rechecks membership, so a stored receipt doesn't let you mint for someone who left.