QRouter documentation

Run a quantum job in three steps.

QRouter routes a circuit to the cheapest, fastest or most accurate machine that can actually run it, across every provider you have enabled. The quickest way in is the terminal — one command, one key, then plain English. The HTTP API is underneath it when you need to automate.

Run your first job

No SDK, no install, no scaffolding. About a minute end to end.

  1. Open your terminal and run QRouter

    Node 18.17 or newer is the only requirement. Nothing is installed — npx fetches and runs it.

    terminal
    $ npx qrouter.app
  2. Get your API key

    Open API keys in the console and create one. It is shown once, so copy it before closing the dialog.

    Create an API key
  3. Paste the key, then just ask

    QRouter asks for the key on first run and remembers it. After that, describe the run in plain English — including the GitHub repository to take the circuit from — and it finds the circuit, picks the machine, and shows you the price before anything is charged.

    terminal
    ❯ Paste your QRouter API key: ••••••••••••
    ✓ local workspace · $25.00 credit · 12 backendsRun this IBM quantum task on the cheapest
      IonQ model from github repo:
      acme-labs/bell-state-demo
    
    Found circuits/bell.qasm — 2 qubits, depth 3
    Routing under "cheapest" … IonQ Aria-1
    Quote  $2.14 · 1024 shots · ~4 min queue
    
    ❯ type run to execute, or ask for something else

Nothing runs and nothing is charged until you type run. The price on the confirmation line is QRouter's own quote for the backend it selected — never the assistant's estimate of one. Finished runs land in your Downloads folder as JSON with a readable .txt companion.

More CLI commands

For scripts and CI, where a conversation is the wrong shape.

runqrouter run ./bell.qasm --shots 1024 --yesSubmit a circuit file or repository URL with no prompts.
statusqrouter status <job-id> --waitCheck a job, or block until it reaches a terminal state.
backendsqrouter backends --jsonList every target this key can reach, machine-readable.
loginqrouter loginStore or replace the saved API key.

The key is stored at ~/.config/qrouter/config.json with 0600 permissions. Setting QROUTER_API_KEY overrides the stored value and is never written to disk — that is the form to use in CI. Prefer a permanent install to npx? curl -fsSL https://qrouter.app/install | sh.

While it waits, the client calls POST /api/v1/jobs/{id}/advance for its own job, so a run finishes even on a deployment with no execution scheduler configured. The endpoint is scoped to the caller's workspace and refuses any job without a quote and a matching credit reservation.

Call it over HTTP

The same key, the same router. One request submits a circuit; poll it, then read the result.

Send the key you created in step 2 as a bearer token against https://api.qrouter.dev. Only circuit is required — every routing input has a default, and "target": "auto" lets the router choose the machine exactly as the CLI does.

curl https://api.qrouter.dev/api/v1/jobs \
  -H "Authorization: Bearer $QROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: bell-001" \
  -d '{
    "circuit": "OPENQASM 2.0; include \"qelib1.inc\"; qreg q[2]; creg c[2]; h q[0]; cx q[0],q[1]; measure q -> c;",
    "shots": 1024,
    "target": "auto",
    "routing_mode": "balanced",
    "optimization_level": 2
  }'

To store a circuit once and run it on several backends in one call, use the v2 quickstart instead.

Authentication

One unified workspace key authenticates every request to both /api/v1 and /api/v2.

Platform keys authenticate to QRouter. Provider credentials remain encrypted server-side and are never returned to client applications.

Authorization: Bearer qci_live_xxxxxxxxxxxx

Every key is issued as qci_{environment}_{secret}, where the secret is 24 random bytes in base64url. Send it as a bearer token on every request; a token that does not begin with qci_ is rejected before any lookup. There is no separate v2 credential and no per-version scope — the key resolves to a workspace, and every circuit, job, and execution is filtered by that workspace.

QRouter stores only a SHA-256 hash of the key plus its first 17 characters, which is the prefix shown in the console. The full value is returned exactly once, in the response that creates it.

Environments

qci_live_…live · default

The environment applied when a key is created without an explicit choice. May route to simulators and QPUs.

qci_test_…test · opt-in

Selected in the create-key form. Restricted to simulators — useful for CI and staging.

A qci_test_ key shares the same workspace and credit balance as a live key, but it can only run on simulators: pinning a QPU returns 403, and "target": "auto" stays on simulators. Key scopes such as jobs:read and jobs:write are enforced for API-key principals; console sessions remain full-privilege.

Creating, rotating, and revoking

Create and revoke keys in Console → API keys. Keys can only be minted from a signed-in console session: calling the key endpoint with an API key returns 403, so a leaked key can never mint another one. Revoking takes effect immediately and the next request with that key fails 401. A key that carries an expiry stops authenticating at that timestamp with the same status. There is no in-place rotation — create the replacement, deploy it, confirm the Last used column has gone quiet on the old key, then revoke it.

Rate limits

Each key consumes one unit per request from a budget of QROUTER_RATE_LIMIT_PER_MINUTE requests, default 120, counted inside the current calendar minute. Exceeding it returns 429 with a retry-after header holding the whole seconds left in that minute, never less than one. Back off for that long rather than retrying immediately; a retry inside the same window consumes budget again.

HTTP/1.1 429 Too Many Requests
retry-after: 23
content-type: application/problem+json

The 429 body follows the error contract of the version you called, carrying the code rate_limit_error in the v2 problem document under /api/v2 and in the v1 envelope under /api/v1.

Local development

A deployment with no Supabase configuration that is not running in production accepts the fixed key qci_test_local_development and resolves it to an in-memory demo workspace whose circuits and jobs are lost on restart. Production deployments reject it exactly like any other unknown key, so it can never be used against live data.

Key security

A key is a bearer credential for the whole workspace: it spends credits and reads results.

  1. Call QRouter from server-side code only. A key shipped in a browser bundle, mobile binary, or published notebook is compromised the moment it ships.
  2. Read the key from an environment variable or secret manager, for example $QROUTER_API_KEY. Never commit it, and keep it out of build logs, crash reports, and error payloads.
  3. Issue one key per deployment and per environment so a single revocation never takes down every service at once.
  4. Prefer qci_test_ keys for CI and staging — they cannot pin QPUs. Issue a live key only when a run must hit real hardware.
  5. Rotate on any suspicion of exposure: create a replacement, deploy it, then revoke the old key.
  6. Log the x-request-id of a failed call rather than the key or the request headers when you need support.
  7. Provider credentials for IBM, AWS Braket, Vultr, and every other configured backend stay encrypted server-side. No endpoint returns them, and your key never grants direct access to a provider account.

Choosing v1 or v2

Both versions are live, share one base URL, and accept the same key. v2 is additive: nothing in v1 was deprecated or removed.

Capability/api/v1/api/v2
Circuit sourceInlined in every job request.Uploaded once as a circuit resource and referenced by id.
Executions per requestOne.1 – 25, each with its own target, shots, and constraints.
Success envelopeThe resource at the top level.{ "object", "data" }, or the raw artifact for result and transpiled.
Error envelope{ "error": { "type", "message" } }application/problem+json with a stable code.
Idempotency-KeyOptional on job creation.Required on circuit and job creation, 8–255 characters.
CancellationPer job.Per execution, leaving siblings running.
Data retention controlJob records persist.Explicit release and delete on the circuit.
Repository deploys and webhooksSupported, alongside the transpile-only preview.Not part of the v2 surface — keep using v1.

Both surfaces share one base URL, https://api.qrouter.dev, and the version is carried in the path. Analysis, transpilation, routing policy, quoting, and credit settlement are the same engine underneath, so routing modes and pricing behave identically in both.

Reach for v2 when you want to compare backends for one circuit, re-run stored work without re-uploading it, cancel or fetch artifacts per execution, or purge circuit source on a schedule. Stay on v1 for repository-sourced deployments, signed webhooks, and the transpile-only preview.

Connecting GitHub

Public repositories need no setup at all. A connection is only required to list your own repositories and to read private ones.

Public repositories work immediately. In Console → Repositories, paste any GitHub URL (https://github.com/owner/name or just owner/name), click Scan repository, pick the .qasm entrypoint, and add it to QRouter. QRouter reads the repository through the anonymous GitHub API, which is rate-limited to 60 requests per hour per IP — if scanning starts failing with a rate-limit error, that is the cause, and either option below removes the cap.

Option A — GitHub App (production, per-organization)

This is the path that supports private repositories and scopes access to each workspace separately. Register an App at github.com/settings/apps/new with:

Repository permissionsContents: Read-only, Metadata: Read-only

Enough to list repositories, walk the git tree, and read .qasm and qrouter.json blobs.

Callback URLhttps://your-domain/api/integrations/github/callback

Where GitHub returns after an installation.

Request user authorization (OAuth) during installationenabled

Required — the callback verifies that the person completing the install actually owns it.

Webhookoptional

Not needed for import or deploy.

Then set these environment variables and redeploy. GITHUB_APP_PRIVATE_KEY is the downloaded .pem; keep it on one line with literal \n escapes, which the server converts back to newlines.

GITHUB_APP_ID=123456
GITHUB_APP_SLUG=your-app-slug          # the URL name, github.com/apps/<slug>
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
GITHUB_APP_CLIENT_ID=Iv1.xxxxxxxxxxxx
GITHUB_APP_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GITHUB_OAUTH_STATE_SECRET=$(openssl rand -base64 32)

All three of GITHUB_APP_ID, GITHUB_APP_SLUG and GITHUB_APP_PRIVATE_KEY must be present or the console reports that the App still needs server configuration. Once they are set, click Connect GitHub in Repositories, install the App on the accounts and repositories you want QRouter to see, and those repositories become searchable by name in both Repositories and Deploy.

Option B — personal token (local development only)

For running the console on your own machine, a classic or fine-grained personal access token with repo read access is enough:

# .env.local
GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxxxx

This token is process-wide, so it deliberately does not serve tenants in production — a request from any workspace would otherwise read the server account's private repositories. In a production deployment, tenants without an App connection fall back to public, unauthenticated access.

Private repositories are only ever read through an installation token scoped to that workspace's own connection. There is no configuration in which one workspace can read another's private source.

Repository deployments

Production workloads are sourced from connected GitHub repositories, not typed into the console.

# qrouter.json
{
  "circuit": "circuits/bell.qasm",
  "shots": 1024,
  "target": "auto",
  "routing_mode": "balanced",
  "optimization_level": 2
}

Install the QRouter GitHub App for the workspace, import a repository, and select its production branch and .qasm entrypoint. Each deployment fetches the source server-side and records the exact blob SHA before routing. CI can call POST /api/v1/repository-jobs with a stable deployment_id to make retries idempotent.

Job request

Only circuit is required. Every routing input has a deterministic default.

circuitstring · required

OpenQASM 2 or supported OpenQASM 3 source, up to 256 KB.

shotsinteger · 1–1,000,000

Measurement repetitions. Default: 1024.

targetbackend id | auto

Pin a target or let the QCI Engine select one. Default: auto.

routing_modeenum

balanced, cost, speed, or quality.

optimization_levelinteger · 0–3

Compiler optimization level. Default: 2.

failoverboolean

Try another quoted compatible backend after failure. Default: true.

timeout_secondsinteger · 60–604,800

Cancel an overdue provider job before failover. Default: 7200.

constraintsobject

Cost, queue, fidelity, compute type, and provider allow/deny filters.

Include a stable Idempotency-Key header when creating a job. Repeating a request with the same key returns the original workspace job instead of spending twice. In v1 the header is optional and a job that ended failed or cancelled releases its key so the same deployment can be retried; v2 enforces a stricter contract.

v1 endpoint reference

The original HTTP surface, versioned under /api/v1.

GET/api/v1/sessionVerify a key and read its workspace, credits, scopes, and runnable backends.
GET/api/v1/repositories/inspectDiscover OpenQASM entrypoints in a connected GitHub repository.
POST/api/v1/projectsImport a repository, production branch, entrypoint, and routing defaults.
POST/api/v1/repository-jobsDeploy a commit-pinned circuit from a connected repository.
GET/api/v1/backendsList compute targets and current routing inputs.
POST/api/v1/transpileAnalyze, route, compile, verify, and quote without execution.
POST/api/v1/jobsCompile, reserve credits, and submit a workload.
GET/api/v1/jobs/{id}Read normalized job state and route metadata.
GET/api/v1/jobs/{id}/resultRetrieve normalized counts and probabilities.
GET/api/v1/jobs/{id}/transpiledDownload the provider-targeted OpenQASM artifact.
POST/api/v1/jobs/{id}/advanceDrive one of your own jobs forward when no fleet scheduler is deployed.
POST/api/v1/jobs/{id}/cancelRequest cancellation and release reserved credits.
POST/api/v1/webhooksRegister an HTTPS endpoint and issue a signing secret.
GET/api/v1/webhooks/deliveriesInspect delivery attempts, retries, and terminal failures.

Hardware-aware transpilation

Compilation is part of execution, not an optional preview step.

01Parse

Validate QASM and derive width, depth, gate counts, and complexity.

02Target

Resolve native gates, connectivity, provider backend, and current calibration.

03Compile

Map and optimize against the selected target with a reproducible seed.

04Verify

Record before/after metrics, layout, and equivalence status.

POST /api/v1/transpile performs the full route and compile pipeline without provider submission. Physical QPU compilation fails closed when the hardware-aware compiler service is unavailable.

Routing policy

QRouter removes incompatible targets, then scores the remaining candidates.

Balanced

35% cost · 25% queue · 25% fidelity · 15% reliability

Cost

70% cost with queue, fidelity, and reliability as tie-breakers.

Speed

65% queue priority for latency-sensitive workloads.

Quality

65% fidelity plus 15% historical reliability.

Constraints are hard filters. A target is rejected when it exceeds maxCost or maxQueueSeconds, falls below minFidelity, lacks circuit width, or is not connected.

Pricing and settlement

Quotes use the compiled circuit and a versioned QCI rate snapshot.

Total=provider cost+transpiler fee+platform fee

A quote expires after 15 minutes. QRouter reserves the quoted total before submission, records the provider-rate inputs in rateSnapshot, records settlement after completion, and releases reserved credits after cancellation or failure.

Unit compatibility: public copy uses QC-hour. Legacy response fields named pricePerNqh and estimatedNqh represent that same normalized QC-hour unit and remain in v1 for API compatibility.

Lifecycle and artifacts

Every provider maps into one observable state machine.

quotedqueueddispatchingsubmittedprocessingcompleted

Poll GET /api/v1/jobs/{id} until completed, failed, or cancelled. The response includes the attempt and event history. Results and transpiled OpenQASM use dedicated artifact endpoints so clients do not need provider-specific storage APIs.

Signed webhooks

Receive asynchronous job transitions over HTTPS.

POST /api/v1/webhooks
{ "url": "https://example.com/qrouter/events" }

The endpoint returns its signing secret once. Store it outside source control and validate each delivery before processing its payload. Failed deliveries retry with exponential backoff and are visible through GET /api/v1/webhooks/deliveries.

v1 error contract

Errors under /api/v1 use stable machine-readable types and human-readable messages.

{ "error": { "type": "invalid_circuit", "message": "..." } }
401authentication_errorMissing, invalid, expired, or revoked key.
402insufficient_creditsThe quote is valid but the workspace cannot reserve it.
422invalid_circuitQASM parsing or validation failed.
422routing_errorNo target satisfies circuit and policy constraints.
429rate_limit_errorPer-key minute budget exhausted. Honour the retry-after header.
500server_errorCompiler, provider, or platform execution failed.

/api/v2 uses a different, richer envelope — see the v2 error contract.

The v2 resource model

A circuit is uploaded once and becomes a reusable resource. A job fans it out across independent executions.

01Circuit

OpenQASM stored once with its hash and static analysis. Reusable by any number of jobs.

02Job

An execution group referencing one circuit, carrying metadata and 1–25 execution targets.

03Execution

One circuit run with its own target, shots, routing mode, constraints, quote, and status.

04Artifacts

Result JSON and compiled OpenQASM fetched per execution once it completes.

In v1 a job is a single-shot envelope: the source, the routing inputs, and the run all arrive together, and comparing two backends means sending the same QASM twice as two unrelated jobs. v2 separates the circuit from the work. Uploading returns a circuit_id you can reference for as long as you keep it, and one POST /api/v2/jobs can quote and dispatch up to 25 executions of that circuit at once.

Executions inside a job are independent — each one selects its own backend, holds its own quote, and reaches a terminal state on its own — but they are funded together. QRouter reserves the sum of every execution quote in a single transaction, so a comparison can never start half-way because the balance moved mid-request.

Each execution carries a caller-chosen key, unique inside the job. Results come back keyed by it, so you can match an execution to the variant it represents without tracking generated ids.

v2 quickstart

Create a circuit, fan it out, poll the job, and read each result.

# 1 · Store the circuit once. The response carries the reusable circuit id.
curl https://api.qrouter.dev/api/v2/circuits \
  -H "Authorization: Bearer $QROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: bell-circuit-001" \
  -d '{
    "name": "bell",
    "format": "openqasm2",
    "circuit": "OPENQASM 2.0; include \"qelib1.inc\"; qreg q[2]; creg c[2]; h q[0]; cx q[0],q[1]; measure q -> c;"
  }'
# 201 { "object": "circuit", "data": { "id": "...", "source_hash": "...", "analysis": { ... } } }

# 2 · Fan that circuit out across up to 25 executions in one job.
#     Replace circuit_id with data.id from step 1.
curl https://api.qrouter.dev/api/v2/jobs \
  -H "Authorization: Bearer $QROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: bell-compare-001" \
  -d '{
    "circuit_id": "00000000-0000-0000-0000-000000000000",
    "metadata": { "experiment": "bell-baseline" },
    "executions": [
      { "key": "recommended", "target": "auto", "shots": 1024 },
      { "key": "sv1", "target": "aws-sv1", "shots": 1024, "routing_mode": "cost" }
    ]
  }'
# 202 { "object": "job", "data": { "id": "...", "status": "running", "executions": [ ... ] } }

# 3 · Poll the job until it is terminal, then read each execution artifact.
curl https://api.qrouter.dev/api/v2/jobs/$JOB_ID \
  -H "Authorization: Bearer $QROUTER_API_KEY"

curl https://api.qrouter.dev/api/v2/executions/$EXECUTION_ID/result \
  -H "Authorization: Bearer $QROUTER_API_KEY"

Install @qrouter/sdk for TypeScript or qrouter for Python. The v2 clients are exported as QRouterV2 alongside the v1 QRouter client, so both surfaces can share one key in the same process. Both constructors take the key first and default to https://api.qrouter.dev.

When you omit the idempotency key argument, both SDKs generate a fresh UUID per call, which means an automatic retry after a network timeout creates a second job. Pass a key derived from your own workload identity whenever a retry must not spend twice.

v2 endpoint reference

Ten operations under /api/v2, all authenticated with the same workspace key.

POST/api/v2/circuitsStore an OpenQASM circuit as a reusable resource. Idempotency-Key required.
GET/api/v2/circuits/{id}Read circuit metadata, static analysis, and release state.
POST/api/v2/circuits/{id}/releasePurge circuit source, results, attempt/event/webhook payloads, and encrypted artifacts.
DELETE/api/v2/circuits/{id}Same purge as release, then remove the circuit resource. Responds 204 with no body.
POST/api/v2/jobsCreate an execution group of 1–25 executions. Idempotency-Key required.
GET/api/v2/jobs/{id}Read group status with every execution, quote, and route decision.
GET/api/v2/executions/{id}/resultRetrieve one normalized result as application/json.
GET/api/v2/executions/{id}/transpiledDownload one compiled OpenQASM artifact as text/plain.
POST/api/v2/executions/{id}/cancelCancel a single execution without touching its siblings.
GET/api/v2/backendsList targets with capability metadata and the current QCI snapshot.

JSON responses are wrapped as { "object": "circuit" | "job" | "execution" | "list", "data": … }. The two artifact endpoints are the exception: they return the stored artifact directly, as application/json for results and text/plain; charset=utf-8 for transpiled OpenQASM, both with cache-control: no-store. Deleting a circuit answers 204 with an empty body.

Every v2 response carries an x-request-id header. Send your own x-request-id to have it echoed back — the first 128 characters are kept — otherwise QRouter generates one and reuses it as the request_id in any error document.

Backends and capabilities

GET /api/v2/backends returns the routing catalog under data, each entry extended with a capabilities object declaring its accepted input formats, that execution is asynchronous, and that results are reported as counts, probabilities, and shots. The response also carries a qci block with the snapshot timestamp, source, index level, and current price per QC-hour.

v2 request and response schema

Both creation endpoints reject unknown fields, so a typo fails loudly instead of being ignored.

A body that misses a required field, breaks a bound, or carries an unrecognised key is rejected with 400 invalid_request. The problem document reports which endpoint rejected it rather than which field failed, so validate against the bounds below before sending.

POST /api/v2/circuits

FieldTypeDefaultNotes
circuitstring · required1 – 256,000 characters of OpenQASM source. Parsed and analyzed synchronously.
formatenumopenqasm2openqasm2 or openqasm3.
namestringnullTrimmed, 1 – 120 characters. May be sent as null to leave it unset.

A stored circuit responds with id, organization_id, name, format, source_hash (SHA-256 of the exact source you sent), the static analysis, created_at, expires_at, and released_at. New circuits return 201; invalid OpenQASM returns 422 invalid_circuit.

POST /api/v2/jobs

FieldTypeDefaultNotes
circuit_idstring · requiredUUID of a circuit in this workspace that has not been released.
executionsarray · required1 – 25 execution targets. Keys must be unique within the array.
metadataobject{}Up to 50 string entries. Keys up to 64 characters, values up to 500.

Execution target

FieldTypeDefaultNotes
keystring · requiredTrimmed, 1–64 characters. Must be unique inside the job; duplicates are rejected.
targetstringautoBackend id (1–120 characters) or auto to let the QCI Engine select one.
shotsinteger10241 – 1,000,000 measurement repetitions.
routing_modeenumbalancedbalanced, cost, speed, or quality.
optimization_levelinteger20 – 3 compiler optimization level.
failoverbooleantrueTry another quoted compatible backend after a failure.
max_attemptsinteger31 – 5 dispatch attempts for this execution.
timeout_secondsinteger720060 – 604,800 seconds before an overdue provider job is cancelled.
constraintsobject{}Hard filters applied before scoring. See the table below.

Constraints

Every field is optional, and the object itself defaults to {}. Constraints are hard filters applied before scoring, exactly as described under routing policy.

FieldTypeBounds and behaviour
maxCostnumberGreater than 0. Rejects any candidate quoted above this total.
maxQueueSecondsinteger0 or greater. Rejects candidates with a longer expected queue.
minFidelitynumberBetween 0 and 1 inclusive. Rejects lower-fidelity candidates.
kindenumqpu or simulator.
providersstring[]Up to 25 provider ids. Only these providers stay eligible.
excludeProvidersstring[]Up to 25 provider ids to remove from the candidate set.

Job and execution responses

A job responds with id, circuit_id, organization_id, status, metadata, created_at, updated_at, completed_at, error, and the ordered executions array. Each execution carries id, your key, status, target, selected_backend_id, shots, routing_mode, analysis, route_decision, error, result_available, its timestamps, and a quote once one exists.

{
  "object": "job",
  "data": {
    "id": "…",
    "circuit_id": "…",
    "status": "running",
    "metadata": { "experiment": "bell-baseline" },
    "executions": [
      { "id": "…", "key": "recommended", "status": "queued", "target": "auto",
        "selected_backend_id": "aws-sv1", "shots": 1024, "result_available": false }
    ]
  }
}

result_available is simply whether that execution has reached completed; use it to decide when to call the result endpoint.

Idempotency

Both v2 creation endpoints require a key, and both compare the payload behind it.

Idempotency-Key: bell-compare-2026-08-06
EndpointIdempotency-KeyReplay response
POST /api/v2/circuitsRequired200 with the stored circuit instead of 201.
POST /api/v2/jobsRequired200 with the existing job instead of 202.
All other v2 routesIgnoredReads, cancellation, release, and deletion never inspect the header.

The key is trimmed and must be between 8 and 255 characters. Anything shorter, longer, or absent is rejected with 400 invalid_idempotency_key before the request is processed. Keys are scoped to your workspace and, separately, to each resource type — the same string can address one circuit and one job without colliding.

QRouter fingerprints each creation request with a SHA-256 hash of the validated body after defaults have been applied, which means omitting a field and sending its documented default are treated as the same request. Replaying an identical request returns the original resource with 200 and the header idempotent-replayed: true, and never charges twice.

Reusing a key with a different body returns 409 idempotency_conflict and creates nothing. Unlike v1, a v2 key is never released — a job that ends failed keeps its key, so retrying the same work needs a new one.

Execution lifecycle

A job rolls up the state of its executions. Poll the job, then fetch artifacts per execution.

queuedrunningcompleted · failed · cancelled
Job statusTerminalMeaning
queuedNoThe group row exists and its executions are being quoted and funded.
runningNoCredits are reserved and at least one execution has not reached a terminal state.
awaiting_paymentNoThe workspace balance could not cover the combined quote. Nothing was submitted.
completedYesEvery execution is terminal and at least one completed.
failedYesEvery execution is terminal, at least one failed, and none completed.
cancelledYesEvery execution is terminal and all of them were cancelled.

Creation quotes every execution, then reserves their combined total in one transaction. When the balance covers it, the executions become queued, the job becomes running, and the API answers 202. When it does not, every execution and the job itself are parked as awaiting_payment, nothing is submitted, and the API answers 402. Parked jobs are repriced and released automatically once credits arrive.

Per-execution status

Executions move through the platform job states: quoted, awaiting_payment, funds_reserved, queued, dispatching, submitted, processing, and then completed, failed, or cancelled. A cancellation of an already-submitted execution first reports cancellation_requested while the provider job is being stopped.

The job status is derived from its executions: it stays running while any execution is still live, reports completed when at least one execution completed, failed when none completed and at least one failed, and cancelled when every execution was cancelled. A job that failed outright also carries an error object.

Polling and artifacts

Poll GET /api/v2/jobs/{id} — the SDK helpers default to two-second intervals — until the job status is completed, failed, or cancelled. An execution’s result is available as soon as that execution reaches completed, so a long comparison can start reading fast backends while slower ones are still running; wait on the individual execution instead of the whole job when that matters.

A job parked as awaiting_payment is not terminal, but it will not advance on its own — stop polling and add credits rather than waiting it out. Requesting an artifact before it exists returns 409, either result_not_available or transpiled_not_available. Cancelling an execution that already reached a terminal state returns 409 not_cancellable, and cancellation answers 202 because the provider stop is asynchronous.

Release and deletion

Two explicit ways to stop storing quantum source and results.

OperationWhat it purgesWhat survives
POST /circuits/{id}/releaseCircuit source and analysis payload, every related job’s source/result/analysis, job_attempts rows, job_events.payload, webhook_deliveries.payload for those jobs, and the encrypted result and transpiled artifacts. released_at is stamped first.The circuit record with released_at set, plus job, execution, quote, and webhook delivery history (without circuit content).
DELETE /circuits/{id}The same content scrub as release, then the circuit resource itself.Nothing you can address by circuit id — reading it afterwards returns 404 circuit_not_found.

Both operations require that every job for the circuit is already terminal. While any job is queued, running, or awaiting_payment they return 409 circuit_active; cancel or wait for those executions first. Release is idempotent — a circuit that is already released keeps its original released_at.

After release the circuit can no longer start new jobs: referencing it from POST /api/v2/jobs returns 409 circuit_released, and its artifact endpoints return 409. Reading the circuit itself still works, so released_at remains observable.

Prefer release when a circuit has already run and you still need its job, quote, and ledger history for accounting. Reserve deletion for circuits you no longer need to account for at all.

v2 error contract

Every failure under /api/v2 is an RFC-style problem document.

content-type: application/problem+json
x-request-id: 6f1d2c9a-…

{
  "type": "https://api.qrouter.dev/problems/idempotency_conflict",
  "title": "Idempotency Conflict",
  "status": 409,
  "detail": "Idempotency key was already used for a different job.",
  "instance": "/api/v2/jobs",
  "code": "idempotency_conflict",
  "request_id": "6f1d2c9a-…"
}

Branch on code: it is the stable identifier, type is that code as a URL, and title is the same code in title case. instance is the path you called and detail is a human-readable sentence that may change. Log request_id, which always matches the x-request-id response header, and quote it when contacting support.

StatusCodeCause
400invalid_requestThe body is not valid JSON, or it failed the request schema.
400invalid_idempotency_keyIdempotency-Key is absent or outside 8–255 characters.
401authentication_errorMissing, malformed, revoked, or expired API key.
403insufficient_scopeThe API key is missing a required scope, or a test key pinned a QPU.
404circuit_not_foundNo circuit with that id belongs to this workspace.
404job_not_foundNo execution group with that id belongs to this workspace.
404execution_not_foundNo v2 execution with that id belongs to this workspace.
409idempotency_conflictThe key was already used with a different request body.
409circuit_releasedThe circuit source was released and cannot start new jobs.
409circuit_activeA job for this circuit is still queued, running, or awaiting payment.
409result_not_availableThe execution has no stored result, or it was released.
409transpiled_not_availableNo transpiled artifact is stored for this execution.
409not_cancellableThe execution is already completed, failed, or cancelled.
409execution_changedThe execution changed state while cancellation was requested.
422invalid_circuitOpenQASM parsing or validation failed.
429rate_limit_errorThe per-key minute budget is exhausted. Honour retry-after.
500internal_errorUnhandled platform error. Quote request_id when reporting it.

Insufficient credit is the one exception. POST /api/v2/jobs answers 402 with the ordinary job envelope plus an error object of { "code": "insufficient_credits" }, not a problem document, because the job was created and parked rather than rejected. Add credits and it resumes on its own.

Production checklist

Required before enabling paid physical backends.

  1. Apply the Supabase schema and QRouter migrations.
  2. Configure Supabase, Stripe, artifact encryption, and provider credentials.
  3. Create the GitHub App, set its callback URL, and configure the matching app credentials from .env.local.example.
  4. Deploy the authenticated Qiskit compiler/worker and point the app at its URL.
  5. Configure authenticated external schedulers for job polling, provider health checks, the daily index refresh, and the Stripe webhook.
  6. Run credentialed smoke jobs against every enabled paid provider.
  7. Run lint, typecheck, Node tests, Python worker tests, SDK builds, and the production web build.

Ready to deploy a repository circuit?

Open deployments