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.
Open your terminal and run QRouter
Node 18.17 or newer is the only requirement. Nothing is installed —
npxfetches and runs it.$ npx qrouter.appGet 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 keyPaste 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.
❯ Paste your QRouter API key: •••••••••••• ✓ local workspace · $25.00 credit · 12 backends ❯ Run 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.
qrouter run ./bell.qasm --shots 1024 --yesSubmit a circuit file or repository URL with no prompts.qrouter status <job-id> --waitCheck a job, or block until it reaches a terminal state.qrouter backends --jsonList every target this key can reach, machine-readable.qrouter 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_xxxxxxxxxxxxEvery 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 · defaultThe environment applied when a key is created without an explicit choice. May route to simulators and QPUs.
qci_test_…test · opt-inSelected 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+jsonThe 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.
- 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.
- 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. - Issue one key per deployment and per environment so a single revocation never takes down every service at once.
- Prefer
qci_test_keys for CI and staging — they cannot pin QPUs. Issue a live key only when a run must hit real hardware. - Rotate on any suspicion of exposure: create a replacement, deploy it, then revoke the old key.
- Log the
x-request-idof a failed call rather than the key or the request headers when you need support. - 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 source | Inlined in every job request. | Uploaded once as a circuit resource and referenced by id. |
| Executions per request | One. | 1 – 25, each with its own target, shots, and constraints. |
| Success envelope | The 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-Key | Optional on job creation. | Required on circuit and job creation, 8–255 characters. |
| Cancellation | Per job. | Per execution, leaving siblings running. |
| Data retention control | Job records persist. | Explicit release and delete on the circuit. |
| Repository deploys and webhooks | Supported, 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-onlyEnough to list repositories, walk the git tree, and read .qasm and qrouter.json blobs.
Callback URLhttps://your-domain/api/integrations/github/callbackWhere GitHub returns after an installation.
Request user authorization (OAuth) during installationenabledRequired — the callback verifies that the person completing the install actually owns it.
WebhookoptionalNot 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_xxxxxxxxxxxxxxxxxxxxThis 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 · requiredOpenQASM 2 or supported OpenQASM 3 source, up to 256 KB.
shotsinteger · 1–1,000,000Measurement repetitions. Default: 1024.
targetbackend id | autoPin a target or let the QCI Engine select one. Default: auto.
routing_modeenumbalanced, cost, speed, or quality.
optimization_levelinteger · 0–3Compiler optimization level. Default: 2.
failoverbooleanTry another quoted compatible backend after failure. Default: true.
timeout_secondsinteger · 60–604,800Cancel an overdue provider job before failover. Default: 7200.
constraintsobjectCost, 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.
/api/v1/sessionVerify a key and read its workspace, credits, scopes, and runnable backends./api/v1/repositories/inspectDiscover OpenQASM entrypoints in a connected GitHub repository./api/v1/projectsImport a repository, production branch, entrypoint, and routing defaults./api/v1/repository-jobsDeploy a commit-pinned circuit from a connected repository./api/v1/backendsList compute targets and current routing inputs./api/v1/transpileAnalyze, route, compile, verify, and quote without execution./api/v1/jobsCompile, reserve credits, and submit a workload./api/v1/jobs/{id}Read normalized job state and route metadata./api/v1/jobs/{id}/resultRetrieve normalized counts and probabilities./api/v1/jobs/{id}/transpiledDownload the provider-targeted OpenQASM artifact./api/v1/jobs/{id}/advanceDrive one of your own jobs forward when no fleet scheduler is deployed./api/v1/jobs/{id}/cancelRequest cancellation and release reserved credits./api/v1/webhooksRegister an HTTPS endpoint and issue a signing secret./api/v1/webhooks/deliveriesInspect delivery attempts, retries, and terminal failures.Hardware-aware transpilation
Compilation is part of execution, not an optional preview step.
Validate QASM and derive width, depth, gate counts, and complexity.
Resolve native gates, connectivity, provider backend, and current calibration.
Map and optimize against the selected target with a reproducible seed.
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.
35% cost · 25% queue · 25% fidelity · 15% reliability
70% cost with queue, fidelity, and reliability as tie-breakers.
65% queue priority for latency-sensitive workloads.
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.
provider cost+transpiler fee+platform feeA 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.
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": "..." } }authentication_errorMissing, invalid, expired, or revoked key.insufficient_creditsThe quote is valid but the workspace cannot reserve it.invalid_circuitQASM parsing or validation failed.routing_errorNo target satisfies circuit and policy constraints.rate_limit_errorPer-key minute budget exhausted. Honour the retry-after header.server_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.
OpenQASM stored once with its hash and static analysis. Reusable by any number of jobs.
An execution group referencing one circuit, carrying metadata and 1–25 execution targets.
One circuit run with its own target, shots, routing mode, constraints, quote, and status.
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.
/api/v2/circuitsStore an OpenQASM circuit as a reusable resource. Idempotency-Key required./api/v2/circuits/{id}Read circuit metadata, static analysis, and release state./api/v2/circuits/{id}/releasePurge circuit source, results, attempt/event/webhook payloads, and encrypted artifacts./api/v2/circuits/{id}Same purge as release, then remove the circuit resource. Responds 204 with no body./api/v2/jobsCreate an execution group of 1–25 executions. Idempotency-Key required./api/v2/jobs/{id}Read group status with every execution, quote, and route decision./api/v2/executions/{id}/resultRetrieve one normalized result as application/json./api/v2/executions/{id}/transpiledDownload one compiled OpenQASM artifact as text/plain./api/v2/executions/{id}/cancelCancel a single execution without touching its siblings./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
| Field | Type | Default | Notes |
|---|---|---|---|
| circuit | string · required | — | 1 – 256,000 characters of OpenQASM source. Parsed and analyzed synchronously. |
| format | enum | openqasm2 | openqasm2 or openqasm3. |
| name | string | null | Trimmed, 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
| Field | Type | Default | Notes |
|---|---|---|---|
| circuit_id | string · required | — | UUID of a circuit in this workspace that has not been released. |
| executions | array · required | — | 1 – 25 execution targets. Keys must be unique within the array. |
| metadata | object | {} | Up to 50 string entries. Keys up to 64 characters, values up to 500. |
Execution target
| Field | Type | Default | Notes |
|---|---|---|---|
| key | string · required | — | Trimmed, 1–64 characters. Must be unique inside the job; duplicates are rejected. |
| target | string | auto | Backend id (1–120 characters) or auto to let the QCI Engine select one. |
| shots | integer | 1024 | 1 – 1,000,000 measurement repetitions. |
| routing_mode | enum | balanced | balanced, cost, speed, or quality. |
| optimization_level | integer | 2 | 0 – 3 compiler optimization level. |
| failover | boolean | true | Try another quoted compatible backend after a failure. |
| max_attempts | integer | 3 | 1 – 5 dispatch attempts for this execution. |
| timeout_seconds | integer | 7200 | 60 – 604,800 seconds before an overdue provider job is cancelled. |
| constraints | object | {} | 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.
| Field | Type | Bounds and behaviour |
|---|---|---|
| maxCost | number | Greater than 0. Rejects any candidate quoted above this total. |
| maxQueueSeconds | integer | 0 or greater. Rejects candidates with a longer expected queue. |
| minFidelity | number | Between 0 and 1 inclusive. Rejects lower-fidelity candidates. |
| kind | enum | qpu or simulator. |
| providers | string[] | Up to 25 provider ids. Only these providers stay eligible. |
| excludeProviders | string[] | 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| Endpoint | Idempotency-Key | Replay response |
|---|---|---|
| POST /api/v2/circuits | Required | 200 with the stored circuit instead of 201. |
| POST /api/v2/jobs | Required | 200 with the existing job instead of 202. |
| All other v2 routes | Ignored | Reads, 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.
| Job status | Terminal | Meaning |
|---|---|---|
| queued | No | The group row exists and its executions are being quoted and funded. |
| running | No | Credits are reserved and at least one execution has not reached a terminal state. |
| awaiting_payment | No | The workspace balance could not cover the combined quote. Nothing was submitted. |
| completed | Yes | Every execution is terminal and at least one completed. |
| failed | Yes | Every execution is terminal, at least one failed, and none completed. |
| cancelled | Yes | Every 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.
| Operation | What it purges | What survives |
|---|---|---|
| POST /circuits/{id}/release | Circuit 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.
| Status | Code | Cause |
|---|---|---|
| 400 | invalid_request | The body is not valid JSON, or it failed the request schema. |
| 400 | invalid_idempotency_key | Idempotency-Key is absent or outside 8–255 characters. |
| 401 | authentication_error | Missing, malformed, revoked, or expired API key. |
| 403 | insufficient_scope | The API key is missing a required scope, or a test key pinned a QPU. |
| 404 | circuit_not_found | No circuit with that id belongs to this workspace. |
| 404 | job_not_found | No execution group with that id belongs to this workspace. |
| 404 | execution_not_found | No v2 execution with that id belongs to this workspace. |
| 409 | idempotency_conflict | The key was already used with a different request body. |
| 409 | circuit_released | The circuit source was released and cannot start new jobs. |
| 409 | circuit_active | A job for this circuit is still queued, running, or awaiting payment. |
| 409 | result_not_available | The execution has no stored result, or it was released. |
| 409 | transpiled_not_available | No transpiled artifact is stored for this execution. |
| 409 | not_cancellable | The execution is already completed, failed, or cancelled. |
| 409 | execution_changed | The execution changed state while cancellation was requested. |
| 422 | invalid_circuit | OpenQASM parsing or validation failed. |
| 429 | rate_limit_error | The per-key minute budget is exhausted. Honour retry-after. |
| 500 | internal_error | Unhandled 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.
- Apply the Supabase schema and QRouter migrations.
- Configure Supabase, Stripe, artifact encryption, and provider credentials.
- Create the GitHub App, set its callback URL, and configure the matching app credentials from
.env.local.example. - Deploy the authenticated Qiskit compiler/worker and point the app at its URL.
- Configure authenticated external schedulers for job polling, provider health checks, the daily index refresh, and the Stripe webhook.
- Run credentialed smoke jobs against every enabled paid provider.
- Run lint, typecheck, Node tests, Python worker tests, SDK builds, and the production web build.
Ready to deploy a repository circuit?
Open deployments