Origin API
Origin is in Early Beta and subject to change. Review the OpenAPI specification when updating an integration.
Origin is Cursor's code forge. Its public REST API lets apps and tools work with Origin repositories, commits, checks, pull requests, and app installations.
- Origin Apps authenticate with app JWTs and installation access tokens. See Authentication.
- View the full OpenAPI specification for detailed schemas and examples.
- Agents can load the llms.txt index or the complete reference as Markdown at llms-full.txt.
Overview
Origin apps implement an OAuth-style installation consent and GitHub App–style authentication model:
- The app signs a short-lived EdDSA JWT with its Ed25519 private key.
- The app exchanges that JWT and an installation ID for a short-lived installation access token (
oit_…). - The installation token calls repository APIs and authenticates Git over HTTPS within the installation's approved repositories and scopes.
- Origin sends signed webhook deliveries to the app's registered webhook URL.
Base URL
https://api.cursor.com/v1/originEndpoint paths in the reference include the full /v1/origin prefix.
Protocol conventions
Requests and responses use application/json. JSON field names are camelCase. Timestamps are RFC 3339 strings. Protobuf 64-bit integers, including pull request numbers and version numbers, are encoded as JSON strings.
Responses carry fields that sit at their default value rather than dropping them, so a false boolean, a 0 number, an empty string, and an empty array are all present in the body. Read the value itself instead of treating a missing key as the default. Fields documented as absent or omitted are optional in the contract and stay out of the body when they are unset.
Getting started
Origin access
- Browse Origin at cursor.com/codebase.
- Manage app settings at cursor.com/codebase/settings/apps.
- Generate an app signing key and register only the public key.
Origin CLI
Install the Origin CLI and sign in:
curl -fsSL https://downloads.cursor.com/origin/install.sh | shorigin auth loginClone an existing repository:
origin repo clone '{ownerSlug}/{repoName}'# or use git directlygit clone 'https://origin.cursor.com/{ownerSlug}/{repoName}.git'Apps clone using Git HTTPS authentication with an installation access token, not a user login.
Installation
Send a customer workspace admin to:
https://cursor.com/codebase/apps/install ?client_id=APP_ID &scope=SPACE_SEPARATED_SCOPES &redirect_uri=REGISTERED_CALLBACK &state=RANDOM_ANTI_FORGERY_VALUE &summary=SHORT_REASON_FOR_ACCESS &include_granted_scopes=true| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Origin App ID. |
scope | Yes | Space-separated scopes. repository:metadata:read is added automatically. |
redirect_uri | Yes for partner-initiated installs | Exact registered callback URI. |
state | Strongly recommended | Random anti-forgery value echoed as the state claim of the installation receipt. Generate it before redirecting and verify the claim on callback. |
summary | No | Short explanation displayed during consent. |
include_granted_scopes | No | When true, retain existing grants and request only additions. |
The workspace admin chooses the target owner, approved scopes, and either all repositories or selected repositories. The customer, not the app, controls repository access.
After approval, Origin redirects to the registered callback:
https://ci.example.com/origin/callback?installation_receipt=RECEIPT_JWTVerify the installation receipt, then store the installation ID from its sub claim. You need it whenever you mint an installation access token.
Installations use one of two repository-selection modes:
all: the installation can access every repository owned by the selected target.selected: the installation can access only repositories selected by the workspace admin.
Both modes cover mirrored repositories as well as native Origin ones, so a mirror appears in GET /installation/repos and can be selected. A mirror is read-only until it becomes a stable outbound mirror: see Mirrored repositories.
Use GET /installation/repos with an installation token to discover the repositories available to that installation. App JWT endpoints can list, inspect, and delete the app's installations. Deleting an installation prevents new tokens from being minted.
Installation receipt
installation_receipt is a short-lived compact JWT signed by Origin. It proves the installation approval came from Origin rather than a forged redirect and carries everything the callback needs. Cursor refuses to redirect without one, so external callbacks always carry it.
JOSE header:
{ "alg": "EdDSA", "kid": "origin-key-id", "typ": "origin-installation-receipt+jwt"}Claims:
{ "iss": "https://api.cursor.com/v1/origin", "aud": "app_01...", "sub": "i_01...", "namespace_id": "ns_01...", "iat": 1786465200, "exp": 1786465500, "jti": "RECEIPT_UUID", "installedBy": { "id": "user_01...", "email": "installer@example.com", "displayName": "Jane Doe" }, "state": "ORIGINAL_VALUE"}audis your app ID andsubis the installation ID to use when minting installation access tokens.namespace_idis the stable ID of the namespace the app was installed into.installedByidentifies the user who performed this install or re-consent. It describes the current action, so on a re-consent it can differ from the durableinstalledByon Get App Installation. It carriesdisplayNamewhen the account has a name, and never carrieshandle; read the handle from a REST response or a webhook payload instead.- Receipts expire five minutes after issuance.
jtiis unique per receipt. stateis present only when the install URL carried a non-emptystate, and echoes that value. Match it against the anti-forgery value you generated before redirecting.
Verify the receipt before trusting the callback: resolve the signing key from the JWKS by the kid header, require alg EdDSA and typ origin-installation-receipt+jwt, and validate the signature, iss, aud, and exp. Reject the callback when verification fails.
The receipt is not an installation access token. Never send it as a Bearer credential; mint installation tokens through Create Installation Access Token instead.
Authentication
Send REST credentials with the Bearer scheme. Each endpoint's Auth badges list the credential types it accepts:
curl --request GET \ --url https://api.cursor.com/v1/origin/repos/OWNER_SLUG/REPO_NAME \ --header "Authorization: Bearer $ORIGIN_BEARER_TOKEN"Cursor API keys are not Origin Bearer tokens. For user-authenticated requests, use the Origin CLI, which exchanges a personal user API key for the short-lived access token that Origin accepts. Do not put a Cursor API key directly in the Authorization header.
Generate an app signing key
Origin Apps authenticate with an Ed25519 key pair. Generate the pair locally, then register only the public key at cursor.com/codebase/settings/apps. An app can hold up to 10 active signing keys.
The private key must stay secret. Do not upload it, paste it into app settings, commit it to a repository, or share it. Store it in a secrets manager. Cursor stores only the public key.
Create a PKCS#8 private key and a PEM SPKI public key with OpenSSL:
openssl genpkey -algorithm ED25519 -out origin-app-private.pemopenssl pkey -in origin-app-private.pem -pubout -out origin-app-public.pemThe public key file starts with -----BEGIN PUBLIC KEY-----. Paste that PEM when you add a signing key. Use the matching private key only to sign app JWTs.
App JWT
Sign a short-lived JWT with the Ed25519 private key paired with one of the app's active signing keys. Generate that pair as described in Generate an app signing key.
JOSE header:
{ "alg": "EdDSA", "kid": "app_01...", "typ": "JWT"}Claims:
{ "iss": "app_01...", "aud": "origin-apps", "iat": 1782928800, "exp": 1782929100}Set iss and kid to the app ID. Use a lifetime of approximately five minutes.
Authorization: Bearer APP_JWTUse an app JWT for app-level operations such as reading app metadata, managing installations, minting installation tokens, and recovering webhook deliveries.
Installation access token
Call POST /app/installations/{installationId}/access_tokens with an app JWT. Installation tokens begin with oit_.
Authorization: Bearer oit_...The response includes expiresAt. Mint tokens just in time, refresh them before expiration, treat them like passwords, and never log them.
Removing the installation, or deleting the app, invalidates its installation tokens before expiresAt. The REST API and Git over HTTPS then reject the token with 401. Do not retry with the same token; the app must be reinstalled before it can mint a working one.
An installation token cannot exceed the installation's approved scopes or repository access. You can attenuate a token to fewer scopes or repositoryIds. Empty or omitted arrays inherit the complete installation grant.
Use installation tokens for repository-scoped operations, including pull requests, check-run writes, and Git over HTTPS.
Git HTTPS authentication
Installation access tokens authenticate Git over HTTPS. The Git endpoint uses HTTP Basic authentication: the password is the installation token, and the username is x-access-token. Bearer credentials belong on the REST API; Git HTTPS rejects them.
Mint a token from Create Installation Access Token immediately before the Git operation. Tokens expire after at most 15 minutes.
Clone, fetch, and pull require repository:contents:read. Push requires repository:contents:write. The token must include the target repository in its grant.
Pushing also requires the repository's owner to be eligible to write to Origin, the same requirement Create Repo carries. A user owner must be on a Pro, Pro Student, Pro+, Ultra, or Start plan. A team owner must have an active paid team plan, must not be on Privacy Mode (Legacy), and must not have Origin turned off by a team admin. A push to a repository whose owner is ineligible returns 403. Clone, fetch, and pull do not carry this requirement.
Read cloneUrl from Get Repo or List App Installation Repositories. Both the GitHub-shaped path (https://origin.cursor.com/OWNER_SLUG/REPO_NAME.git) and the legacy /git/ path clone.
git clone "https://x-access-token:${INSTALLATION_TOKEN}@origin.cursor.com/OWNER_SLUG/REPO_NAME.git"Embedding the token in the URL stores it in .git/config. After a successful clone, rewrite the remote so later commands do not reuse an expired secret:
git remote set-url origin "https://origin.cursor.com/OWNER_SLUG/REPO_NAME.git"To keep the token out of the remote URL, supply it through Git's credential helper:
git -c credential.helper="!f() { echo username=x-access-token; echo password=${INSTALLATION_TOKEN}; }; f" \ clone "https://origin.cursor.com/OWNER_SLUG/REPO_NAME.git"The Origin CLI credential helper is for user logins. App integrations pass the installation token as shown here. Treat the token like a password, never log it, and mint a fresh one before expiresAt when a job still needs Git access.
Git over HTTPS meters its own budget, separate from the REST budget in Rate limits. A charged Git response carries the same X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Used headers, with X-RateLimit-Resource set to git rather than core. Over-budget Git requests return 429 with Retry-After and X-RateLimit-Reset. Read the headers to pace a job rather than assuming a number; unmetered requests carry no rate-limit headers.
On a mirrored repository, an installation token clones, fetches, and pulls, and Origin rejects git push with 403 until the mirror becomes a stable outbound mirror. See Mirrored repositories.
User-authenticated CLI requests
Use origin api for user-authenticated requests. For an interactive session, sign in through your browser:
origin auth loginorigin api /repos/OWNER_SLUG/REPO_NAME/pullsFor a non-interactive session, provide a personal user API key from Cursor Dashboard → API Keys:
export CURSOR_API_KEY="YOUR_PERSONAL_USER_API_KEY"origin api /repos/OWNER_SLUG/REPO_NAME/pullsThe CLI exchanges the personal API key for a short-lived user access token, then sends that token in the Authorization header. Do not send the API key itself to an Origin endpoint. App integrations should use app JWTs and installation access tokens instead.
Discovery and signing keys
Origin publishes unauthenticated discovery metadata and its active signing keys. The same keys sign webhook deliveries and installation receipts.
Discovery metadata identifies the issuer and jwks_uri:
curl https://api.cursor.com/v1/origin/.well-known/openid-configuration{ "issuer": "https://api.cursor.com/v1/origin", "jwks_uri": "https://api.cursor.com/v1/origin/keys", "response_types_supported": ["id_token"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["EdDSA"]}/keys returns active Ed25519 JWKs:
curl https://api.cursor.com/v1/origin/keys{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "use": "sig", "alg": "EdDSA", "kid": "origin-key-id", "x": "PUBLIC_KEY_MATERIAL" } ]}Cache the JWKS. /keys sends Cache-Control: public, max-age=600, stale-if-error=600, so reuse a cached response for 10 minutes, then refresh; if a refresh fails, keep the last good keys for at most another 10 minutes before failing verification. Also refresh on a signature that no key verifies, which drops a retired key ID. Keys rotate weekly.
Webhook signatures do not carry a key ID, so verification should try each active Ed25519 key. Installation receipts carry the signing key's kid in their JOSE header, so receipt verification can resolve the key directly.
Scopes
Request only the minimum scopes your app needs. repository:metadata:read and app or installation metadata access are granted automatically and should not be added separately to installation URLs.
| Scope | Allows |
|---|---|
repository:metadata:read | Read repository metadata. Added automatically. |
repository:contents:read | Read commits, branches, contents, comparison files, and low-level Git objects. Search file text. Download a repository archive. Clone, fetch, and pull over Git HTTPS. Sync a mirrored repository from its upstream source. |
repository:contents:write | Push over Git HTTPS. Merge pull requests. Create branches and commit file changes through the Git data endpoints. Re-request a check run. |
repository:pull_requests:read | Read pull requests, changed files, pull request commits, assigned labels, and merge eligibility. |
repository:pull_requests:write | Create and update pull requests. Assign and remove pull request labels. |
repository:pull_requests:reviews:read | Read pull request comments, comment threads, submitted reviews, and requested reviewers. |
repository:pull_requests:reviews:write | Create and update comments; resolve and reopen comment threads; create, update, and dismiss reviews; request and remove reviewers. |
repository:checks:read | Read check suites, runs, and check run annotations. |
repository:checks:write | Create and update check suites and runs. Append check run annotations. |
repository:labels:read | Read the label definitions a repository owns. |
repository:labels:write | Create, update, and delete repository label definitions. |
repository:rulesets:read | Read repository rulesets. |
repository:rulesets:write | Create, update, and delete repository rulesets. |
repository:settings:read | Read the grants held directly on a repository. |
repository:settings:write | Update repository settings: the default branch, visibility, merge methods, and automatic head-branch deletion. Upsert and delete grants on a repository. |
namespace:settings:read | Read the grants held directly on an owner. |
namespace:settings:write | Upsert and delete grants on an owner. |
Requesting a :write scope also grants the matching :read scope, so repository:labels:write covers repository:labels:read and you do not have to list both. The reverse does not hold: a read scope never grants writes.
The installation token can only narrow these grants. It cannot add a scope or repository the workspace admin did not approve.
Mirror-state changes sit outside this table. Transition Repo Mirror, Force Repo Mirror Cutover, and Detach Repo Mirror take repository:mirror:write or repository:mirror:delete, which an app cannot request at installation: they are carried by a Cursor user credential, and the caller must also administer the repository on the mirror's upstream source.
Installation management sits outside it too. Add App Installation Repositories takes namespace:installations:write, which an app cannot request at installation: a namespace admin holds it on a Cursor user credential, and the same credential kind that consented to the installation is the one that can extend it.
App management sits outside it for the same reason. Create App takes namespace:apps:create, List Namespace Apps and Get App take namespace:apps:read, and Update App, Add App Signing Key, and Revoke App Signing Key take app:settings:write. A publisher holds these on a Cursor user credential; an app cannot request them for itself.
The table covers the scopes an app requests at installation. To look up the scope a single operation requires, read its x-origin-scopes extension in the OpenAPI specification. That extension covers every operation, including the app, installation, and namespace scopes that come with the credential itself rather than from an installation grant. An operation whose scopes all come with the credential marks its extension ambient: true: there is nothing to request for it, and presenting the right credential is enough.
Mirrored repositories
An installation uses every scope it holds on a native Origin repository and on a stable outbound mirror. On a repository in any other mirror state, only two scopes apply:
repository:metadata:readrepository:contents:read
Every other scope returns 403 on that repository, whatever the workspace admin approved. Over the REST API, repository and contents reads, commit comparison, and Sync Mirror keep working, and Origin rejects pull requests, reviews, comments, checks, rulesets, and every write. Over Git HTTPS, clone, fetch, pull, and LFS download keep working, and Origin rejects push and LFS upload.
Moving a repository out of that state is a user-credential operation rather than something an installation can do: Transition Repo Mirror advances the mirror direction, Force Repo Mirror Cutover cuts over to the upstream source without pushing divergent refs back, and Detach Repo Mirror disconnects the mirror for good.
The mirror object on a repository does not tell you whether writes are allowed. A mirror partway through a transition can report mirror.status as outbound and still be read-only, so treat the 403 as authoritative rather than branching on mirror.status.
Rate limits
The Origin API uses a shared per-principal point budget that resets on a rolling one-minute window. Each authenticated principal kind has its own budget:
| Principal | Default budget |
|---|---|
| Installation access token | 3,000 points/minute |
| App JWT | 6,000 points/minute |
| Cursor user or service account | 600 points/minute |
Every endpoint charges a fixed cost against that budget before the handler runs. Authentication and authorization failures are not charged.
| Cost | Operations |
|---|---|
| 0 | Get Rate Limit. Status only; does not consume points. |
| 1 | Most read endpoints, plus Create Installation Access Token |
| 5 | Ordinary writes, plus these heavier reads: Get Commit, List Commit Files, List Comparison Files, List Pull Request Files, Get Repo Tarball, and Grep Contents |
| 10 | Create App, Create Repo, Create Commit From Files, Merge Pull Request, Get Pull Request Mergeability, Transition Repo Mirror, and Force Repo Mirror Cutover |
Cursor can raise per-app minute budgets for design partners. Contact Cursor if your integration needs a higher limit.
Response headers
Charged responses and Get Rate Limit include:
| Header | Description |
|---|---|
X-RateLimit-Limit | Points available in the current window for this principal |
X-RateLimit-Remaining | Points left in the current window |
X-RateLimit-Used | Points consumed in the current window |
X-RateLimit-Reset | Unix timestamp (UTC seconds) when the window resets |
X-RateLimit-Resource | Always core for the shared public API budget |
X-RateLimit-Reset advertises a full 60-second window from the response time. The counter's window starts at the first charged request in a burst, not on a calendar-minute boundary.
Exceeding the limit
When a request would exceed the budget, the API returns HTTP 429 with:
Retry-After: seconds to wait before retrying (60)- The same
X-RateLimit-*headers, withX-RateLimit-Remainingset to0
{ "code": 8, "message": "Rate limit exceeded: 3000 points per minute for this installation. Retry after 60s.", "details": []}Wait for Retry-After, or until X-RateLimit-Reset, before retrying. Use backoff with jitter when concurrent callers share one installation token.
Checking remaining quota
Call Get Rate Limit to read the current budget without consuming points. The response body mirrors the X-RateLimit-* headers for the shared core resource.
Common conventions
Pagination
Paginated endpoints accept:
pageSize: defaults to 30 and is capped at 100.pageToken: opaque token returned by the preceding page. Do not inspect or construct it.
Responses use a resource-specific collection field and nextPageToken. It is empty when no next page exists. Public list responses do not include total counts. Page tokens are bound to their originating resource and filters. Restart pagination when filters change. Invalid or mismatched non-empty tokens return 400.
Errors
Errors use a Google RPC-style body:
{ "code": 5, "message": "resource not found", "details": []}Common HTTP statuses are 400, 401, 403, 404, 429, 500, and 503. Some Git-database operations also return 409 for repository-state conflicts. See Rate limits for 429 headers and retry behavior.
Use the HTTP status and code to branch on errors. Treat message as developer-facing text.
A 404 never distinguishes a resource that does not exist from one your app cannot reach. Read it as "not available to this installation" rather than as proof the resource is absent.
details carries typed entries: google.rpc.BadRequest field violations on an invalid argument, and a google.rpc.RequestInfo entry on every error. Origin can add detail types at any time, so ignore entries your integration does not recognize.
Every error response carries the request ID twice: in an X-Request-ID response header, and as a google.rpc.RequestInfo entry in details. Origin echoes the x-request-id you sent, or generates one when you send none. The RequestInfo entry is present even when message is an opaque internal error, so quote the request ID when you contact Cursor about a failed call.
Unmatched paths under /v1/origin, and requests that use the wrong method on a known path, return this same body rather than a generic router error. The message names the method and path and never echoes the query string.
Repository paths
Repository-scoped paths take the owner slug and repository name as {ownerSlug}/{repoName}. Both segments resolve case-insensitively, so any casing addresses the repository. Responses return the stored name and slug rather than the casing you sent, and Git HTTPS URLs resolve the same way. Compare repository names case-insensitively, and read the canonical casing from Get Repo.
Every repository-scoped path also accepts the repository's stable ID in place of the pair: send _ as the owner slug and the ID as the repository name, as in GET /v1/origin/repos/_/REPO_ID. Read the ID from the id field on Get Repo. The sentinel _ cannot be claimed as an owner slug, so the two forms never collide. In a Connect or JSON request, set ownerSlug to _ and name to the ID.
The ID form survives a rename, which makes it the stable way to address a repository. It grants nothing on its own: after Origin resolves the ID to a repository, your app still needs the same scope on that repository. An ID your app cannot reach returns the same 404 body as an ID that does not exist, so a response never confirms that a repository exists. A malformed ID returns 400. Create Repo takes an owner slug alone and rejects _.
Resource references
Resource snapshots contain the resource's current fields. Container context uses compact references instead of duplicating complete resources:
RepositoryReferenceidentifies a repository.PullRequestReferenceidentifies a pull request and nests its repository reference.ThreadReferenceidentifies the thread containing a pull request comment.OriginActoridentifies a public actor as one ofuser,app, orserviceAccount. Exactly one variant is present; read the identity from that variant.
Current limitations
- Namespace-wide repository listing and repository creation are not part of the partner API. Discover repositories through the installation.
- Commit comparison returns summary data rather than an embedded commit list. Changed files have their own paginated endpoint, List Comparison Files.
- Threads are addressable only for resolution. There is no endpoint that lists threads directly; read them from the comments they contain.
- Push webhooks do not include a complete commit list.
- Pull request merge supports native Origin repositories. Mirrored repositories are rejected.
- A mirrored repository is read-only for an installation until it becomes a stable outbound mirror. See Mirrored repositories.
Implementation checklist
- Store the Ed25519 private key in a secrets manager and rotate keys deliberately. See Generate an app signing key.
- Verify the installation receipt on install callbacks and read the installation ID and
statefrom its claims. - Use short-lived app JWTs and mint installation tokens just in time.
- Use installation tokens, not app JWTs, for repository-scoped APIs, check-run writes, and Git HTTPS.
- Request the minimum scopes and repository access.
- Treat page tokens as opaque and restart pagination when filters change.
- Keep check
keyvalues stable and readable. Use a new immutableexternalIdfor each retry and increasingexternalUpdatedAtvalues for updates. - Verify webhook signatures against the raw request body before parsing.
- Deduplicate deliveries with
webhook-idand process asynchronously after returning2xx. - Ignore unknown JSON fields for forward compatibility.
- Honor
Retry-AfterandX-RateLimit-*headers. Use Get Rate Limit to monitor remaining points without consuming them.
Endpoint reference
Download the OpenAPI specification for the complete component schemas. The document declares https://api.cursor.com as its server and a bearerAuth HTTP bearer security scheme, and each operation lists the response codes that operation can return, plus a request and response example. Every operation also carries an x-origin-scopes extension: scopes holds the scope the operation requires, and tokenTypes holds the credential kinds it accepts. Path parameters carry the same names the URLs use, ownerSlug and repoName. Every operation carries a unique operationId; where one operation answers two URL shapes, the second shape's id takes a _2 suffix, as in OriginService_GetRepoTarball_2.
The JSON snippets show schema-shaped placeholder values. Response field descriptions reflect the OpenAPI schema and current platform contract.
Apps and installations
Get Rate Limit
/v1/origin/rate_limitReturns the authenticated principal's current public API rate limit status.
Accessing this endpoint does not consume rate limit points. The response covers the shared per-minute point budget used by other public API endpoints for this principal. See Rate limits.
Response Fields
resources object
resources.core object
resources.core.limit integer
resources.core.remaining integer
resources.core.reset integer
resources.core.used integer
rate object
resources.core. Prefer resources.core in new clients.curl --request GET \ --url 'https://api.cursor.com/v1/origin/rate_limit' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "resources": { "core": { "limit": 6000, "remaining": 5994, "reset": 1785682800, "used": 6 } }, "rate": { "limit": 6000, "remaining": 5994, "reset": 1785682800, "used": 6 }}Get Authenticated App
/v1/origin/appReturns metadata for the authenticated app.
Response Fields
id string
displayName string
webhookUrl string
events array
createdAt string
updatedAt string
installationRedirectUris array
namespaceSlug string
description string
websiteUrl string
defaultScopes array
curl --request GET \ --url 'https://api.cursor.com/v1/origin/app' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "id": "app_01k2ja2000e0080000000000a1", "displayName": "CI Status Bot", "webhookUrl": "https://ci.acme.dev/webhooks/origin", "events": [ "pull_request.created", "pull_request.merged" ], "createdAt": "2026-08-01T09:30:00Z", "updatedAt": "2026-08-02T14:45:00Z", "installationRedirectUris": [ "https://ci.acme.dev/origin/setup" ], "namespaceSlug": "acme", "description": "Posts CI status on pull requests.", "websiteUrl": "https://ci.acme.dev", "defaultScopes": [ "repository:contents:read", "repository:pull_requests:read" ]}List App Installations
/v1/origin/app/installationsLists installations for the authenticated app.
Query Parameters
pageSize integer
pageToken string
next_page_token. Empty for the first page.Response Fields
installations array
installations[].id string
installations[].appId string
installations[].target object
installations[].target.slug string
installations[].target.id string
installations[].target.type string
team, user. Omitted when unknown.installations[].createdAt string
installations[].updatedAt string
installations[].repoSelectionMode string
installations[].scopes array
installations[].installedBy object
installations[].installedBy.id string
user_.installations[].installedBy.email string
installations[].installedBy.displayName string
installations[].installedBy.handle string
@ prefix. Present only while that profile is publicly visible; omitted otherwise.installations[].suspendedAt string
installations[].deletedAt string
installation.deleted webhook snapshot; a deleted installation no longer resolves through the API, so this endpoint never returns it.nextPageToken string
curl --request GET \ --url 'https://api.cursor.com/v1/origin/app/installations' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "installations": [ { "id": "inst_01k2ja2000e0080000000000b2", "appId": "app_01k2ja2000e0080000000000a1", "target": { "slug": "acme", "id": "ns_01k2ja2000e0080000000000p3", "type": "team" }, "createdAt": "2026-08-01T09:30:00Z", "updatedAt": "2026-08-02T14:45:00Z", "repoSelectionMode": "selected", "scopes": [ "repository:contents:read", "repository:pull_requests:read" ] } ]}Get App Installation
/v1/origin/app/installations/{installationId}Returns a single installation for the authenticated app.
repoSelectionMode is all or selected.
Path Parameters
installationId string Required
Response Fields
id string
appId string
target object
target.slug string
target.id string
target.type string
team, user. Omitted when unknown.createdAt string
updatedAt string
repoSelectionMode string
scopes array
installedBy object
installedBy.id string
user_.installedBy.email string
installedBy.displayName string
installedBy.handle string
@ prefix. Present only while that profile is publicly visible; omitted otherwise.suspendedAt string
deletedAt string
installation.deleted webhook snapshot; a deleted installation no longer resolves through the API, so this endpoint never returns it.curl --request GET \ --url 'https://api.cursor.com/v1/origin/app/installations/INSTALLATION_ID' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "id": "inst_01k2ja2000e0080000000000b2", "appId": "app_01k2ja2000e0080000000000a1", "target": { "slug": "acme", "id": "ns_01k2ja2000e0080000000000p3", "type": "team" }, "createdAt": "2026-08-01T09:30:00Z", "updatedAt": "2026-08-02T14:45:00Z", "repoSelectionMode": "selected", "scopes": [ "repository:contents:read", "repository:pull_requests:read" ]}Delete App Installation
/v1/origin/app/installations/{installationId}Deletes an installation that belongs to the authenticated app and prevents new installation tokens from being minted. Already-issued short-lived tokens may remain valid until they expire (at most 15 minutes). The response body is empty.
Path Parameters
installationId string Required
Response Fields
Successful requests return no response body.
curl --request DELETE \ --url 'https://api.cursor.com/v1/origin/app/installations/INSTALLATION_ID' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response:
204 No ContentCreate Installation Access Token
/v1/origin/app/installations/{installationId}/access_tokensCreates an installation access token for the authenticated app.
Requires app signing-JWT authentication, like GetAuthenticatedApp. The token is scoped to the named installation, which must belong to the authenticated app. Callers may attenuate the token to a subset of the installation's accepted scopes and accessible repositories.
repositoryIds can name a mirrored repository. The resulting token carries the installation's scopes, and Origin still applies the mirror ceiling on each request: see Mirrored repositories.
Path Parameters
installationId string Required
Request Body
scopes array
repositoryIds array
Response Fields
token string
expiresAt string
curl --request POST \ --url 'https://api.cursor.com/v1/origin/app/installations/INSTALLATION_ID/access_tokens' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "scopes": [ "repository:contents:read", "repository:pull_requests:read" ], "repositoryIds": [ "repo_01k2ja2000e0080000000000q4" ]}'Response shape:
{ "token": "oit_2v8xkq4m1c7p9t3w5y0z6r4b", "expiresAt": "2026-08-01T10:30:00Z"}List App Installation Repositories
/v1/origin/installation/reposLists repositories accessible to the authenticated app installation.
Requires an installation access token (oit_) minted by CreateInstallationAccessToken.
Partners discover their repositories through this endpoint. List entries are sparse repository summaries; use Get Repo for full timestamps. Get Repo includes the output-only cloneUrl.
Results include mirrored repositories. A mirror is read-only until it becomes a stable outbound mirror: see Mirrored repositories.
Query Parameters
pageSize integer
pageToken string
next_page_token. Empty for the first page. The same filter must be used when requesting subsequent pages.filter string
owner/repo value matches each half against its corresponding field. Leading and trailing whitespace is ignored; an empty value applies no filter.Response Fields
repositories array
repositories[].id string
repositories[].name string
repositories[].fullName string
repositories[].owner object
repositories[].owner.slug string
repositories[].owner.id string
repositories[].owner.type string
team, user. Omitted when unknown.repositories[].defaultBranch string
repositories[].mirror object
repositories[].mirror.source string
github.repositories[].mirror.sourceId string
repositories[].mirror.status string
inbound, outbound.repositories[].visibility string
internal, private.repositories[].allowMergeCommit boolean
repositories[].allowSquashMerge boolean
repositories[].deleteBranchOnMerge boolean
nextPageToken string
repoSelectionMode string
curl --request GET \ --url 'https://api.cursor.com/v1/origin/installation/repos' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "repositories": [ { "id": "repo_01k2ja2000e0080000000000q4", "name": "rocket", "fullName": "acme/rocket", "owner": { "slug": "acme", "id": "ns_01k2ja2000e0080000000000p3", "type": "team" }, "defaultBranch": "main", "createdAt": "2026-08-01T09:30:00Z", "updatedAt": "2026-08-02T14:45:00Z", "pushedAt": "2026-08-02T14:45:00Z", "cloneUrl": "https://origin.cursor.com/git/acme/rocket.git" } ], "repoSelectionMode": "selected"}List Webhook Deliveries
/v1/origin/app/webhook/deliveriesLists webhook deliveries for the authenticated app, newest first.
A delivery is one event owed to one app; its id is the webhook-id header value the receiver sees. delivered=false is the recovery predicate: it selects every delivery that has never received a 2xx, including deliveries whose retry ladder ran out during an outage.
Deliveries are listable for seven days after they are created, and only while your app has an active installation in the delivery's namespace. App-targeted lifecycle events such as installation.deleted stay visible after the uninstall they describe.
Query Parameters
delivered boolean
delivered_at. delivered=false is the recovery predicate: it is evaluated server-side, so it cannot miss a delivery whose retry ladder exhausted mid-outage the way a caller-supplied time window silently does.eventType string
pull_request.created.installationId string
WebhookDelivery.installation.id).createdAfter string
createdBefore string
pageSize integer
pageToken string
next_page_token. Empty for the first page.Response Fields
deliveries array
deliveries[].id string
webhook-id value the receiver sees; use it as the idempotency key.deliveries[].event object
deliveries[].event.id string
deliveries[].event.type string
deliveries[].installation object
id is the current active installation for the target owner; unset when none exists (possible only for app-targeted lifecycle events after an uninstall).deliveries[].installation.id string
deliveries[].installation.target object
deliveries[].installation.target.slug string
deliveries[].installation.target.id string
deliveries[].installation.target.type string
team, user. Omitted when unknown.deliveries[].createdAt string
deliveries[].deliveredAt string
deliveries[].lastAttempt object
deliveries[].lastAttempt.id string
deliveries[].lastAttempt.deliveryId string
deliveries[].lastAttempt.trigger string
automatic, manual.deliveries[].lastAttempt.responseStatusCode integer
deliveries[].lastAttempt.latencyMs integer
deliveries[].lastAttempt.errorMessage string
deliveries[].lastAttempt.attemptedAt string
nextPageToken string
curl --request GET \ --url 'https://api.cursor.com/v1/origin/app/webhook/deliveries' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "deliveries": [ { "id": "whd_01k2ja2000e0080000000000j9", "event": { "id": "evt_01k2ja2000e0080000000000r5", "type": "pull_request.created" }, "installation": { "id": "inst_01k2ja2000e0080000000000b2", "target": { "slug": "acme", "id": "ns_01k2ja2000e0080000000000p3", "type": "team" } }, "createdAt": "2026-08-01T09:30:00Z", "deliveredAt": "2026-08-02T14:45:05Z", "lastAttempt": { "id": "wha_01k2ja2000e0080000000000k0", "deliveryId": "whd_01k2ja2000e0080000000000j9", "trigger": "automatic", "responseStatusCode": 200, "latencyMs": 182, "attemptedAt": "2026-08-02T14:45:05Z" } } ]}Batch Redeliver Webhook Deliveries
/v1/origin/app/webhook/deliveries:batchRedeliverAsks Origin to send deliveries again.
The request means "ensure a send is in flight for each of these", not "add another send". It returns one result per unique input rather than failing the batch on a bad entry, so a single expired ID cannot block the rest of a recovery page. A 202 means the sends are queued; delivery itself is asynchronous, so poll List Webhook Deliveries for outcomes.
Request Body
deliveryIds array Required
pageSize ceiling on List Webhook Deliveries. Duplicates are removed, keeping first-seen order. An empty list, or more than 100 unique entries, returns InvalidArgument (HTTP 400).Response Fields
results array
results[].deliveryId string
results[].outcome string
queued when a send was created, already_in_flight when a send was already running, and not_found otherwise. already_in_flight is a success, not an error. not_found covers unknown IDs, IDs older than the seven-day retention window, and namespaces where your app is no longer installed.curl --request POST \ --url 'https://api.cursor.com/v1/origin/app/webhook/deliveries:batchRedeliver' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "deliveryIds": [ "whd_01k2ja2000e0080000000000j9" ]}'Response shape:
{ "results": [ { "deliveryId": "whd_01k2ja2000e0080000000000j9", "outcome": "queued" } ]}Ping Webhook
/v1/origin/app/webhook/pingsSends a test delivery to the authenticated app's webhook URL and reports what the receiver answered.
Use it to verify a receiver while you set an app up, instead of waiting for a real event. Requires app signing-JWT authentication, like Get Authenticated App.
The receiver sees the production shape: the same headers and v1ed signature, verifiable against the signing keys, with webhook-event-type set to ping and a payload naming the app. A ping belongs to no installation, so the webhook-installation-id header and the envelope's installationId are both absent.
Origin sends the ping once, synchronously, and reports the outcome in the response. There are no retries, and a ping is not a domain event: it never appears in List Webhook Deliveries and cannot be redelivered. A receiver that fails is reported in the response rather than as an error. An app with no webhook URL configured returns FailedPrecondition (HTTP 400).
Request Body
The request takes no fields. Send an empty JSON object.
Response Fields
deliveryId string
webhook-id, matching the header the receiver saw.eventId string
event.id.delivered boolean
2xx status before the delivery timeout. Always present.responseStatusCode integer
0 when no response arrived because the connection failed or timed out. Always present.curl --request POST \ --url 'https://api.cursor.com/v1/origin/app/webhook/pings' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN' \ --header 'Content-Type: application/json' \ --data '{}'Response shape:
{ "deliveryId": "whd_01k2ja2000e0080000000000j9", "eventId": "evt_01k2ja2000e0080000000000r5", "delivered": true, "responseStatusCode": 200}Get App
/v1/origin/apps/{appId}Returns a single app by its identifier. This is the management read for app publishers; Get Authenticated App is the equivalent self-read for the app's own JWT credential.
Path Parameters
appId string Required
app_.Response Fields
id string
app_.displayName string
webhookUrl string
events array
createdAt string
updatedAt string
installationRedirectUris array
namespaceSlug string
description string
websiteUrl string
defaultScopes array
curl --request GET \ --url 'https://api.cursor.com/v1/origin/apps/{appId}' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "id": "app_01k2ja2000e0080000000000a1", "displayName": "CI Status Bot", "webhookUrl": "https://ci.acme.dev/webhooks/origin", "events": [ "pull_request.created", "pull_request.merged" ], "createdAt": "2026-08-01T09:30:00Z", "updatedAt": "2026-08-02T14:45:00Z", "installationRedirectUris": [ "https://ci.acme.dev/origin/setup" ], "namespaceSlug": "acme", "description": "Posts CI status on pull requests.", "websiteUrl": "https://ci.acme.dev", "defaultScopes": [ "repository:contents:read", "repository:pull_requests:read" ]}Update App
/v1/origin/apps/{appId}Updates an app's settings. Omitted fields are left unchanged, and at least one settable field must be provided. Clearing webhookUrl by sending an empty string disables outbound webhook delivery and cancels the app's pending deliveries; setting a URL again does not resurrect cancelled deliveries.
Path Parameters
appId string Required
app_.Request Body
displayName string
webhookUrl string
events object
events.events array
description string
websiteUrl string
installationRedirectUris object
installationRedirectUris.installationRedirectUris array
defaultScopes object
defaultScopes.scopes array
Response Fields
id string
app_.displayName string
webhookUrl string
events array
createdAt string
updatedAt string
installationRedirectUris array
namespaceSlug string
description string
websiteUrl string
defaultScopes array
curl --request PATCH \ --url 'https://api.cursor.com/v1/origin/apps/APP_ID' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "webhookUrl": "https://ci.acme.dev/webhooks/origin-v2", "events": { "events": [ "pull_request.created", "pull_request.merged", "repository.pushed" ] }}'Response shape:
{ "id": "app_01k2ja2000e0080000000000a1", "displayName": "CI Status Bot", "webhookUrl": "https://ci.acme.dev/webhooks/origin-v2", "events": [ "pull_request.created", "pull_request.merged", "repository.pushed" ], "createdAt": "2026-08-01T09:30:00Z", "updatedAt": "2026-08-02T14:45:00Z", "installationRedirectUris": [ "https://ci.acme.dev/origin/setup" ], "namespaceSlug": "acme", "description": "Posts CI status on pull requests.", "websiteUrl": "https://ci.acme.dev", "defaultScopes": [ "repository:contents:read", "repository:pull_requests:read" ]}Add App Signing Key
/v1/origin/apps/{appId}/signing_keysAdds a signing key to an app. Apps hold a bounded set of active signing keys; adding a key beyond the limit returns FailedPrecondition (HTTP 400) until another key is revoked. A key that is already registered returns AlreadyExists (HTTP 409 Conflict).
Path Parameters
appId string Required
app_.Request Body
publicKey string Required
Response Fields
kid string
kid header and to revoke the key.createdAt string
curl --request POST \ --url 'https://api.cursor.com/v1/origin/apps/APP_ID/signing_keys' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAq9zTf3hL6wXe1cVj0bYs5mKR8uDnG2oAaPp4NiEkKlM=\n-----END PUBLIC KEY-----"}'Response shape:
{ "kid": "3q2xW9dK5fJm8vB1nY6cT0aZrQpLh4eGkVsN7uMxOdI", "createdAt": "2026-08-02T14:45:00Z"}Revoke App Signing Key
/v1/origin/apps/{appId}/signing_keys/{kid}Revokes an app signing key by its key ID. App JWTs signed with a revoked key stop authenticating. The last active signing key cannot be revoked; that request returns FailedPrecondition (HTTP 400). The response body is empty.
Path Parameters
appId string Required
app_.kid string Required
Response Fields
Successful requests return no response body.
curl --request DELETE \ --url 'https://api.cursor.com/v1/origin/apps/{appId}/signing_keys/{kid}' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response:
204 No ContentList Namespace Apps
/v1/origin/namespaces/{namespaceSlug}/appsLists the apps a namespace owns, newest first. Responses carry display metadata only; read one app's webhook configuration with Get App.
Path Parameters
namespaceSlug string Required
Query Parameters
pageSize integer
pageToken string
next_page_token. Empty for the first page.Response Fields
apps array
apps[].id string
app_.apps[].displayName string
apps[].description string
nextPageToken string
curl --request GET \ --url 'https://api.cursor.com/v1/origin/namespaces/{namespaceSlug}/apps' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "apps": [ { "id": "app_01k2ja2000e0080000000000a1", "displayName": "CI Status Bot", "description": "Posts CI status on pull requests." }, { "id": "app_01k2ja2000e0080000000000a2", "displayName": "Deploy Bot", "description": "" } ],