Origin Migration API
Origin is in Early Beta and subject to change. Review the OpenAPI specification when updating an integration.
The Origin Migration API is for operators migrating repositories between GitHub and Origin mirrors. Call these endpoints from a migration script you run as a Cursor user, not from an Origin App.
Sign in with origin auth login, or pass a Cursor API key through origin api. See User-authenticated CLI requests.
The endpoints use the Origin API base URL, https://api.cursor.com/v1/origin, and the same error model.
Scopes
repository:mirror:write, repository:mirror:delete, and repository:metadata:read are user policy scopes. Transition Repo Mirror and Force Repo Mirror Cutover require Write (repository:mirror:write). Detach Repo Mirror requires Admin (repository:mirror:delete). Get Mirror Transition Job and Get Active Mirror Transition Job require repository:metadata:read. You must also administer the repository on its upstream GitHub source.
These scopes are not requestable during an Origin App installation. An Origin App cannot call these endpoints.
Endpoint reference
Transition Repo Mirror
/v1/origin/repos/{ownerSlug}/{repoName}/mirror:transitionStarts a mirror-state transition on a mirrored repository and returns the job tracking it. The repository enters a transitioning mirror status while the job runs, so poll Get Active Mirror Transition Job or Get Mirror Transition Job until the job reaches a terminal status.
A repository that is not in the transition's expected start state, or that already has an active transition job, returns FailedPrecondition (HTTP 400). The caller must administer the repository on the mirror's upstream source; a caller without that access returns 403.
Path Parameters
ownerSlug string Required
repoName string Required
Request Body
transition string Required
initial_to_inbound, inbound_to_outbound, outbound_to_inbound.Response Fields
repository object
job object
curl --request POST \ --url 'https://api.cursor.com/v1/origin/repos/OWNER_SLUG/REPO_NAME/mirror:transition' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "transition": "inbound_to_outbound"}'Response shape:
{ "repository": { "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-02T15:00:00Z", "pushedAt": "2026-08-02T14:45:00Z", "cloneUrl": "https://origin.cursor.com/git/acme/rocket.git", "mirror": { "source": "github", "sourceId": "R_kgDOAbc123", "status": "inbound" } }, "job": { "id": "rmt_01k2ja2000e0080000000000m3", "transition": "inbound_to_outbound", "status": "running", "phase": "draining-writes", "attemptCount": 1, "drainUntil": "2026-08-02T15:05:00Z", "startedAt": "2026-08-02T15:00:00Z", "createdAt": "2026-08-02T14:59:30Z", "updatedAt": "2026-08-02T15:01:00Z" }}Force Repo Mirror Cutover
/v1/origin/repos/{ownerSlug}/{repoName}/mirror:forceCutoverForces an outbound_to_inbound cutover without pushing this host's divergent state back to the upstream source. The source is adopted as the source of truth as it stands, and refs that exist only on this host are snapshotted and abandoned. Returns the job tracking the forced cutover.
Accepted only for a repository in outbound status, or one stuck in an outbound-to-inbound transition whose active job reports requires_attention, in which case that job is superseded. Any other state, including a queued or running transition job, returns FailedPrecondition (HTTP 400). The caller must administer the repository on the mirror's upstream source; a caller without that access returns 403.
Path Parameters
ownerSlug string Required
repoName string Required
Request Body
The request takes no fields. Send an empty JSON object.
Response Fields
repository object
job object
curl --request POST \ --url 'https://api.cursor.com/v1/origin/repos/OWNER_SLUG/REPO_NAME/mirror:forceCutover' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN' \ --header 'Content-Type: application/json' \ --data '{}'Response shape:
{ "repository": { "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-02T15:00:00Z", "pushedAt": "2026-08-02T14:45:00Z", "cloneUrl": "https://origin.cursor.com/git/acme/rocket.git", "mirror": { "source": "github", "sourceId": "R_kgDOAbc123", "status": "outbound" } }, "job": { "id": "rmt_01k2ja2000e0080000000000m4", "transition": "outbound_to_inbound", "status": "running", "phase": "snapshotting-refs", "attemptCount": 1, "startedAt": "2026-08-02T15:00:00Z", "createdAt": "2026-08-02T14:59:30Z", "updatedAt": "2026-08-02T15:01:00Z" }}Detach Repo Mirror
/v1/origin/repos/{ownerSlug}/{repoName}/mirrorPermanently disconnects a mirrored repository from its upstream source. The repository keeps its current contents and becomes a native repository, syncing stops in both directions, and the mirror's deploy credential is deleted. The response body is empty.
Detaching is not reversible through this API. A repository that never had a mirror returns FailedPrecondition (HTTP 400); detaching an already-detached repository succeeds without effect.
Path Parameters
ownerSlug string Required
repoName string Required
Response Fields
Successful requests return no response body.
curl --request DELETE \ --url 'https://api.cursor.com/v1/origin/repos/OWNER_SLUG/REPO_NAME/mirror' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response:
204 No ContentGet Mirror Transition Job
/v1/origin/repos/{ownerSlug}/{repoName}/mirror/transition-jobs/{jobId}Returns one mirror transition job by id. An unknown job id returns 404.
Path Parameters
ownerSlug string Required
repoName string Required
jobId string Required
job.id.Response Fields
id string
transition string
initial_to_inbound, inbound_to_outbound, outbound_to_inbound.status string
queued, running, succeeded, failed_rolled_back, requires_attention, superseded. succeeded, failed_rolled_back, and superseded are terminal. requires_attention needs operator intervention or a forced cutover.phase string
status, for display and debugging. One of queued, starting, draining-writes, initializing-mirror-fetch, finalizing-mirror-fetch, finalizing-mirror-push, snapshotting-refs, verifying-integrity, reopening-inbound-mirror, committing-target-status, rolling-back, or completed. New phases can appear as the transition process evolves, so poll status for completion rather than matching on phases.attemptCount integer
drainUntil string
lastErrorCode string
InboundMirrorDrainTimeout or MirrorIntegrityMismatch. Absent while the job has not failed.lastErrorMessage string
lastErrorCode. Absent while the job has not failed.startedAt string
completedAt string
createdAt string
updatedAt string
curl --request GET \ --url 'https://api.cursor.com/v1/origin/repos/OWNER_SLUG/REPO_NAME/mirror/transition-jobs/JOB_ID' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "id": "rmt_01k2ja2000e0080000000000m3", "transition": "inbound_to_outbound", "status": "succeeded", "phase": "completed", "attemptCount": 1, "startedAt": "2026-08-02T15:00:00Z", "completedAt": "2026-08-02T15:12:00Z", "createdAt": "2026-08-02T14:59:30Z", "updatedAt": "2026-08-02T15:12:00Z"}Get Active Mirror Transition Job
/v1/origin/repos/{ownerSlug}/{repoName}/mirror/transition-jobs:activeReturns the repository's currently active mirror transition job and its most recent terminal one. Both fields are optional, so a repository that has never transitioned returns an empty object. Poll this endpoint to follow a transition: once activeJob disappears, lastJob tells you how it ended.
Path Parameters
ownerSlug string Required
repoName string Required
Response Fields
activeJob object
lastJob object
curl --request GET \ --url 'https://api.cursor.com/v1/origin/repos/OWNER_SLUG/REPO_NAME/mirror/transition-jobs:active' \ --header 'Authorization: Bearer YOUR_ORIGIN_TOKEN'Response shape:
{ "activeJob": { "id": "rmt_01k2ja2000e0080000000000m4", "transition": "outbound_to_inbound", "status": "running", "phase": "draining-writes", "attemptCount": 1, "drainUntil": "2026-08-02T15:05:00Z", "startedAt": "2026-08-02T15:00:00Z", "createdAt": "2026-08-02T14:59:30Z", "updatedAt": "2026-08-02T15:01:00Z" }, "lastJob": { "id": "rmt_01k2ja2000e0080000000000m3", "transition": "inbound_to_outbound", "status": "succeeded", "phase": "completed", "attemptCount": 1, "startedAt": "2026-08-01T10:00:00Z", "completedAt": "2026-08-01T10:12:00Z", "createdAt": "2026-08-01T09:59:30Z", "updatedAt": "2026-08-01T10:12:00Z" }}