The Qubit Desk API
Everything the page does, you can do from a script. Base URL: https://api.skillsafe.ai/v1/app-api. Every response is the same envelope.
The envelope
{"ok": true, "data": { ... }}
{"ok": false, "error": {"code": "VALIDATION_ERROR", "message": "...", "details": { ... }}}
Check ok before reading data. A job that fails is still ok: true — the failure is in data.status.
Error codes
| Code | HTTP | What it means |
|---|---|---|
UNAUTHORIZED | 401 | No token, a malformed token, or a token minted for a different app. |
FORBIDDEN | 403 | The token is valid but not allowed to do this. |
NOT_FOUND | 404 | No such app, job or record. |
VALIDATION_ERROR | 400 | The body is not the shape the endpoint wants. error.details names the field. |
INSUFFICIENT_CREDITS | 402 | The balance is below min_credits for this run. /estimate is free, so a well-behaved client never sees this. |
RATE_LIMITED | 429 | Too many requests. Back off; do not tight-loop. |
JOB_FAILED | 200 | The job reached a terminal failed state. The envelope is still {data} -- read data.status. |
INTERNAL | 500 | A platform fault. Retry with the same Idempotency-Key: the key is what stops a retry double-billing. |
1. The task field comes first
Qubit Desk is one app with one system prompt and three lanes. Every request must carry a task; it selects the lane, and it is part of the idempotency key because two lanes over one circuit are two distinct runs.
task | What it answers | Credited to |
|---|---|---|
audit | Will it run on this device at all? Width, gate set, layout and routing, measurements, transpiler settings, what must change before submitting. | @k-dense-ai/qiskit |
decohere | Does the signal survive? The duration against T1/T2, the dominant Lindblad channel, what the error budget predicts, which mitigation applies here. | @k-dense-ai/qutip |
budget | What does it cost? The shot plan and its statistical floor, the parameter-shift budget, trainability at this width, cross-vendor portability. | @k-dense-ai/pennylane |
An absent or unrecognised task does not error: the model picks the closest lane and names the lane it chose in lane and in headline. Do not rely on that — send the field.
One worked body per lane
All three use the same circuit and the same device, because that is the point: one work object, three questions. The body IS the input object. Do NOT wrap it in an input key: a wrapped body still answers 200 with a plausible hold, and the model then never sees a single field.
task: "audit" — Will this Bell pair run on the heavy-hex device?
{
"task": "audit",
"circuit": "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q[0] -> c[0];\nmeasure q[1] -> c[1];",
"device": "name: superconducting heavy-hex, 27 qubits\nqubits: 27\nnative_gates: rz, sx, x, ecr\ncoupling: 0-1, 1-2, 1-4, 2-3, 3-5, 4-7, 5-8, 6-7, 7-10\nt1_us: 180\nt2_us: 120\ngate_error_1q: 0.00024\ngate_error_2q: 0.0072\nreadout_error: 0.011\ngate_time_1q_ns: 32\ngate_time_2q_ns: 440\nreadout_time_ns: 1400\nshots_per_second: 4200\nmax_shots: 100000\nmid_circuit_measurement: yes\ndynamical_decoupling: yes\nsession_seconds: 600",
"objective": "sampling",
"precision": "",
"shots": "4096",
"iterations": "",
"context": "I have a 10-minute session on Thursday.",
"prescan_facts": "<the object the browser computes -- see below>",
"operation_sample": "<up to 400 operations -- see below>"
}
task: "decohere" — Does its signal survive 1,872 ns on a device whose T2 is 120 us?
{
"task": "decohere",
"circuit": "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q[0] -> c[0];\nmeasure q[1] -> c[1];",
"device": "name: superconducting heavy-hex, 27 qubits\nqubits: 27\nnative_gates: rz, sx, x, ecr\ncoupling: 0-1, 1-2, 1-4, 2-3, 3-5, 4-7, 5-8, 6-7, 7-10\nt1_us: 180\nt2_us: 120\ngate_error_1q: 0.00024\ngate_error_2q: 0.0072\nreadout_error: 0.011\ngate_time_1q_ns: 32\ngate_time_2q_ns: 440\nreadout_time_ns: 1400\nshots_per_second: 4200\nmax_shots: 100000\nmid_circuit_measurement: yes\ndynamical_decoupling: yes\nsession_seconds: 600",
"objective": "sampling",
"precision": "",
"shots": "4096",
"iterations": "",
"context": "",
"prescan_facts": "<the object the browser computes -- see below>",
"operation_sample": "<up to 400 operations -- see below>"
}
task: "budget" — What does 4,096 shots of it cost in device time?
{
"task": "budget",
"circuit": "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q[0] -> c[0];\nmeasure q[1] -> c[1];",
"device": "name: superconducting heavy-hex, 27 qubits\nqubits: 27\nnative_gates: rz, sx, x, ecr\ncoupling: 0-1, 1-2, 1-4, 2-3, 3-5, 4-7, 5-8, 6-7, 7-10\nt1_us: 180\nt2_us: 120\ngate_error_1q: 0.00024\ngate_error_2q: 0.0072\nreadout_error: 0.011\ngate_time_1q_ns: 32\ngate_time_2q_ns: 440\nreadout_time_ns: 1400\nshots_per_second: 4200\nmax_shots: 100000\nmid_circuit_measurement: yes\ndynamical_decoupling: yes\nsession_seconds: 600",
"objective": "sampling",
"precision": "",
"shots": "4096",
"iterations": "",
"context": "",
"prescan_facts": "<the object the browser computes -- see below>",
"operation_sample": "<up to 400 operations -- see below>"
}
The input object, field by field
| Field | Type | What it is |
|---|---|---|
task | string, required | The lane: audit, decohere or budget. |
circuit | string, required | OpenQASM 2.0 or 3.0 source. Clipped to 42,000 characters on the middle, with the cut announced in-band as a comment. |
device | string | The target device's calibration data as key: value lines. Clipped to 8,000 characters. May be empty, in which case nine of the fourteen checks report not_run. |
objective | string | One of expectation, sampling, variational, benchmark. |
precision | string | Target standard error, as a decimal string. Empty defaults to 0.01. |
shots | string | Shots per circuit. Empty means "derive it from the precision". |
iterations | string | Optimiser steps. Variational runs only. Empty defaults to 100. |
context | string | Free text. Clipped to 6,000 characters. |
prescan_facts | object, required | Everything the browser computed. The lane is told to reconcile against it and not to contradict it. |
operation_sample | object | Up to 400 operations, with their index in the circuit, gate name, qubits and line. When sampled is true this is a golden-ratio draw plus one operation of every gate name -- never a fixed stride. |
retry_note | string | Set only by the client's own reformat retry, when a previous reply did not parse. |
prescan_facts is the part that matters. The browser computes it before any request, the lane is instructed to reconcile against it, and every flag in it must come back answered. If you are driving the API yourself you can build it however you like, but the prompt will hold your reply to whatever you put in it — so put real numbers there or send {"readable": false, "why": "..."} and let the lane say it had no arithmetic to work from.
2. The output contract
Every lane returns the same envelope; only body differs. This is the shape the page's own normalize() parses, so it is the shape to code against.
{
"lane": "audit | decohere | budget",
"title": "string",
"verdict": "runnable | runnable_with_changes | needs_rework | not_viable",
"headline": "string",
"summary": "string",
"findings": [
{
"id": "F-001",
"severity": "blocking | high | medium | low | info",
"area": "width | gateset | connectivity | measurement | coherence | noise | mitigation | shots | trainability | reproducibility",
"title": "string",
"detail": "string",
"evidence": "string",
"line": 42,
"fix": "string"
}
],
"reconciliation": [
{
"flag_uid": "coherence-1",
"status": "confirmed | adjusted | set_aside | not_applicable",
"note": "string"
}
],
"context_notes": [
{
"claim": "string",
"status": "honoured | contradicted | unverifiable",
"note": "string"
}
],
"unassessable": [
{
"item": "string",
"why": "string"
}
],
"body": {
"...": "one of the three shapes below"
}
}
Normalisation rules worth knowing, because the client applies them rather than rejecting the reply: an unrecognised verdict falls back to runnable_with_changes; an unrecognised severity falls back to medium; a line that is null, empty or non-numeric stays null and is not printed — it never becomes line 0; and a reply with neither findings nor summary is treated as a parse failure and triggers one reformat retry that reuses an idempotency key derived from the same input.
body for task: "audit"
{
"device_fit": {
"qubits_needed": 12,
"qubits_available": 27,
"statement": "string"
},
"gateset_plan": [
{
"gate": "cz",
"uses": 33,
"action": "decompose into rz/sx/x/ecr",
"entanglers_after": 33,
"note": "string"
}
],
"routing_plan": {
"statement": "string",
"layout_advice": "string",
"estimated_entanglers_after": 240,
"worst_pair": "4-5, 4 hops apart"
},
"measurement_review": {
"statement": "string",
"unmeasured": "none",
"dynamic_needed": false,
"dynamic_supported": "yes | no | unknown"
},
"transpile_settings": [
{
"setting": "optimization_level",
"value": "3",
"why": "string"
}
],
"blocking_changes": [
"string"
]
}
body for task: "decohere"
{
"duration_review": {
"statement": "string",
"critical_path": "9.1 us",
"coherence_limit": "T2 = 120 us",
"fraction": "83%"
},
"dominant_channel": {
"channel": "relaxation | dephasing | gate_error | readout | crosstalk | leakage | unassessable",
"why": "string"
},
"fidelity_review": {
"statement": "string",
"estimate": "15.0%",
"what_it_omits": "string"
},
"mitigation_plan": [
{
"technique": "dynamical decoupling",
"applies": true,
"why": "string",
"overhead": "none in shots"
}
],
"simulation_check": {
"statement": "string",
"model": "string",
"what_to_simulate": "string"
}
}
body for task: "budget"
{
"objective_review": {
"statement": "string",
"what_is_measured": "string"
},
"shot_plan": {
"shots_per_circuit": 40000,
"why": "string",
"statistical_floor": "0.005",
"bias_note": "string"
},
"gradient_plan": {
"circuits_per_step": 97,
"steps": 150,
"total_executions": 582000000,
"wall_clock": "38.5 h",
"why": "string"
},
"trainability": {
"risk": "low | medium | high | unassessable",
"why": "string",
"what_to_measure": "string"
},
"portability": [
{
"device_family": "trapped ion, all-to-all",
"native_entangler": "MS",
"what_changes": "string",
"cost_delta": "string"
}
]
}
gradient_plan is null, not an empty object, when the circuit has no free parameters.
The fourteen free checks
Each is pass, warn, fail or not_run. not_run is not a pass — it means the device profile did not carry the field the check needs, and the prompt requires the lane to say so rather than treat it as clean.
key | What it checks |
|---|---|
determinism | The circuit is fully enumerable |
width | The circuit fits the device's qubit count |
qubit_range | Every operand names a qubit that exists |
gateset | Every gate is native or decomposable |
connectivity | Every entangling pair is adjacent or routable |
measurement | Every qubit that is used is read out |
clbit | No two measurements share a classical bit |
midcircuit | Mid-circuit measurement and classical control are supported |
coherence | The circuit finishes inside the coherence time |
idle | Idle time is not the dominant term |
fidelity | The estimated success probability is usable |
binding | No unbound parameter reaches the hardware |
shots | The shot budget reaches the requested precision |
runtime | The run fits the session budget |
3. Numbered steps
Pick a language once; the choice applies to every block on the page and is remembered.
Step 1 — a tiny client
Two headers on every call: Content-Type: application/json and Authorization: Bearer <token>. That is the whole authentication story — there is no X-App-Slug header, and sending one changes nothing.
# There is no helper in cURL; every call below repeats the two headers.
# The base URL is the same for every endpoint:
BASE=https://api.skillsafe.ai/v1/app-api
TOKEN=YOUR_TOKEN
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # paste it from /tokens.html
def call(path, body=None, method=None):
data = None if body is None else json.dumps(body).encode()
req = urllib.request.Request(BASE + path, data=data,
method=method or ("POST" if data else "GET"))
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
with urllib.request.urlopen(req) as r:
env = json.loads(r.read().decode())
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"].get("message", ""))
return env["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // paste it from /tokens.html
async function call(path, body, method) {
const res = await fetch(BASE + path, {
method: method || (body ? "POST" : "GET"),
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + TOKEN },
body: body ? JSON.stringify(body) : undefined
});
const env = await res.json();
if (!env.ok) throw new Error(env.error.code + ": " + (env.error.message || ""));
return env.data;
}
package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any, method string) (json.RawMessage, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
if method == "" {
method = "POST"
}
} else if method == "" {
method = "GET"
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, errors.New(env.Error.Code + ": " + env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
class QubitDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody, String method) throws Exception {
HttpRequest.BodyPublisher pub = jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody);
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.method(method != null ? method : (jsonBody != null ? "POST" : "GET"), pub)
.build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
// res.body() is the {ok, data, error} envelope -- check ok before reading data
return res.body();
}
}
require "json"
require "net/http"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN"
def call(path, body = nil, method = nil)
uri = URI(BASE.to_s + path)
klass = (method || (body ? "POST" : "GET")) == "POST" ? Net::HTTP::Post : Net::HTTP::Get
req = klass.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
function call(string $path, ?array $body = null, ?string $method = null) {
$ch = curl_init(BASE . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer " . TOKEN,
]);
$verb = $method ?? ($body !== null ? "POST" : "GET");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $verb);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new RuntimeException($env["error"]["code"] . ": " . ($env["error"]["message"] ?? ""));
}
return $env["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static class QubitDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static string Token = "YOUR_TOKEN";
static readonly HttpClient Http = new HttpClient();
static async Task<JsonElement> Call(string path, object body = null, string method = null)
{
var verb = new HttpMethod(method ?? (body != null ? "POST" : "GET"));
var req = new HttpRequestMessage(verb, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (body != null)
req.Content = new StringContent(JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!env.GetProperty("ok").GetBoolean())
throw new Exception(env.GetProperty("error").GetProperty("code").GetString());
return env.GetProperty("data");
}
}
Step 2 — get a token
The slug travels in the POST /guest body, and that call answers 201. A guest token is enough for /me and /estimate; a metered lane needs a personal token, which you can copy from the token page without opening a developer console.
# A GUEST token is enough for /me and /estimate. Note the slug travels in the BODY --
# there is no X-App-Slug header, and a bogus one would be ignored.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
-H "Content-Type: application/json" \
-d '{"slug":"qubit-desk"}'
# -> 201 {"ok":true,"data":{"token":"aut_...","guest_id":"...","expires_at":"..."}}
#
# Running a lane is metered, so it needs a PERSONAL token: sign in at
# https://qubit-desk.skillsafe.ai/tokens.html and copy it from there.
def guest_token():
req = urllib.request.Request(
BASE + "/guest",
data=json.dumps({"slug": "qubit-desk"}).encode(),
method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r: # answers 201
return json.loads(r.read().decode())["data"]["token"]
TOKEN = guest_token() # enough for /me and /estimate; a run needs a personal token
async function guestToken() {
const res = await fetch(BASE + "/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "qubit-desk" }) // the slug goes in the BODY
});
const env = await res.json(); // 201
return env.data.token;
}
TOKEN = await guestToken();
func guestToken() (string, error) {
b, _ := json.Marshal(map[string]string{"slug": "qubit-desk"})
res, err := http.Post(base+"/guest", "application/json", bytes.NewReader(b))
if err != nil {
return "", err
}
defer res.Body.Close()
var env struct {
Data struct{ Token string } `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return "", err
}
return env.Data.Token, nil
}
// POST /guest with {"slug":"qubit-desk"} -- the slug travels in the body, not a header.
// Answers 201 with {"ok":true,"data":{"token":"aut_...","guest_id":...,"expires_at":...}}.
static String guestToken() throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"qubit-desk\"}"))
.build();
String body = HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
// parse body.data.token with your JSON library of choice
return body;
}
def guest_token
uri = URI(BASE.to_s + "/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.dump({ slug: "qubit-desk" }) # the slug goes in the body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)["data"]["token"] # 201
end
<?php
function guest_token(): string {
$ch = curl_init(BASE . "/guest");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => "qubit-desk"]));
$env = json_decode(curl_exec($ch), true); // 201
curl_close($ch);
return $env["data"]["token"];
}
static async Task<string> GuestToken()
{
var body = new StringContent("{\"slug\":\"qubit-desk\"}", Encoding.UTF8, "application/json");
var res = await Http.PostAsync(Base + "/guest", body); // 201
var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
return env.GetProperty("data").GetProperty("token").GetString();
}
Step 3 — /me
Returns only {subject_type, subject_id, credits}. There is no user_id and no is_guest field anywhere in this API — branch on subject_type.
curl -s https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $TOKEN"
# -> {"ok":true,"data":{"subject_type":"guest","subject_id":"...","credits":0}}
#
# There is no user_id and no is_guest field. Branch on subject_type.
me = call("/me")
# {"subject_type": "user" | "guest", "subject_id": "...", "credits": 12345}
signed_in = me["subject_type"] == "user" # NOT me.get("is_guest") -- no such field
print(me["credits"], "credits")
const me = await call("/me");
// { subject_type: "user" | "guest", subject_id, credits }
const signedIn = me.subject_type === "user"; // branch on subject_type
console.log(me.credits, "credits");
raw, err := call("/me", nil, "")
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits int64 `json:"credits"`
}
json.Unmarshal(raw, &me)
signedIn := me.SubjectType == "user"
_ = signedIn
String me = QubitDesk.call("/me", null, "GET");
// data.subject_type is "user" or "guest"; data.credits is the balance.
// There is no is_guest field -- branch on subject_type.
System.out.println(me);
me = call("/me")
signed_in = me["subject_type"] == "user" # branch on subject_type
puts "#{me['credits']} credits"
<?php
$me = call("/me");
$signedIn = $me["subject_type"] === "user"; // branch on subject_type
echo $me["credits"], " credits\n";
var me = await QubitDesk.Call("/me");
var signedIn = me.GetProperty("subject_type").GetString() == "user";
Console.WriteLine($"{me.GetProperty("credits").GetInt64()} credits");
Step 4 — /estimate (free)
Free, creates no job, but authenticated: call it before you hold a token and you get a 401. Assert model_alias is gpt-terra and markup_bps is 1000 — that is the authoritative proof you are wired to the right model at the right markup. hold_credits is a reservation, not a price; it differs per lane, so re-estimate when you switch.
# /estimate is FREE and creates no job. It is also AUTHENTICATED, so mint the token first --
# calling it before you have one returns 401.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @body.json
# -> {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,
# "hold_credits":3142,"min_credits":210,"sponsor_enabled":false}}
#
# body.json is the input object ITSELF -- see "The input object" above. Never {"input": {...}}.
body = {
"task": "audit", # or "decohere" / "budget"
"circuit": open("bell.qasm").read(),
"device": open("device.txt").read(),
"objective": "sampling",
"precision": "", "shots": "4096", "iterations": "",
"context": "I have a 10-minute session on Thursday.",
"prescan_facts": prescan_facts, # see the section below
"operation_sample": operation_sample,
}
est = call("/estimate", body) # free, no job, but authenticated
assert est["model_alias"] == "gpt-terra" and est["markup_bps"] == 1000
print(est["hold_credits"], "reserved;", est["min_credits"], "minimum")
const body = {
task: "audit", // or "decohere" / "budget"
circuit: qasmText,
device: deviceText,
objective: "sampling",
precision: "", shots: "4096", iterations: "",
context: "I have a 10-minute session on Thursday.",
prescan_facts: prescanFacts, // see the section below
operation_sample: operationSample
};
const est = await call("/estimate", body); // free, no job, but authenticated
console.log(est.hold_credits, "reserved of", est.min_credits, "minimum");
body := map[string]any{
"task": "audit",
"circuit": qasmText,
"device": deviceText,
"objective": "sampling",
"precision": "", "shots": "4096", "iterations": "",
"context": "I have a 10-minute session on Thursday.",
"prescan_facts": prescanFacts,
"operation_sample": operationSample,
}
raw, err := call("/estimate", body, "") // free, no job
if err != nil {
panic(err)
}
var est struct {
ModelAlias string `json:"model_alias"`
MarkupBps int `json:"markup_bps"`
HoldCredits int64 `json:"hold_credits"`
MinCredits int64 `json:"min_credits"`
}
json.Unmarshal(raw, &est)
// The body is the input object itself. Never wrap it in an "input" key.
String body = """
{"task":"audit","circuit":"OPENQASM 2.0; ...","device":"qubits: 27\\n...",
"objective":"sampling","precision":"","shots":"4096","iterations":"",
"context":"I have a 10-minute session on Thursday.",
"prescan_facts":{...},"operation_sample":{...}}
""";
String est = QubitDesk.call("/estimate", body, "POST"); // free, no job
// assert data.model_alias == "gpt-terra" and data.markup_bps == 1000
System.out.println(est);
body = {
task: "audit",
circuit: File.read("bell.qasm"),
device: File.read("device.txt"),
objective: "sampling",
precision: "", shots: "4096", iterations: "",
context: "I have a 10-minute session on Thursday.",
prescan_facts: prescan_facts,
operation_sample: operation_sample
}
est = call("/estimate", body) # free, no job, but authenticated
puts "#{est['hold_credits']} reserved, #{est['min_credits']} minimum"
<?php
$body = [
"task" => "audit",
"circuit" => file_get_contents("bell.qasm"),
"device" => file_get_contents("device.txt"),
"objective" => "sampling",
"precision" => "", "shots" => "4096", "iterations" => "",
"context" => "I have a 10-minute session on Thursday.",
"prescan_facts" => $prescanFacts,
"operation_sample" => $operationSample,
];
$est = call("/estimate", $body); // free, no job, but authenticated
echo $est["hold_credits"], " reserved\n";
var body = new {
task = "audit",
circuit = qasmText,
device = deviceText,
objective = "sampling",
precision = "", shots = "4096", iterations = "",
context = "I have a 10-minute session on Thursday.",
prescan_facts = prescanFacts,
operation_sample = operationSample
};
var est = await QubitDesk.Call("/estimate", body); // free, no job
Console.WriteLine(est.GetProperty("hold_credits").GetInt64());
Step 5 — /run and poll (metered)
Send an Idempotency-Key on every run. Hash (task, input, attempt): the lane must be inside the key, because two lanes over one circuit are two distinct runs and must not collide. If you retry a malformed reply, reuse a key derived from the same input with a new attempt suffix — that is what stops a reformat retry double-billing.
# METERED. Pass an Idempotency-Key: it is what stops a retry double-billing.
# The key should hash (task, input, attempt) -- two lanes over one circuit are two runs.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: qubit-desk:audit:d75af416:1422:a1" \
-d @body.json
# -> {"ok":true,"data":{"job_id":"job_..."}}
# then poll until terminal
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/job_..." \
-H "Authorization: Bearer $TOKEN"
# -> data.status is queued | running | succeeded | failed
# data.output.output is the JSON string the model produced
# data.charged_credits is the ACTUAL cost, usually far below hold_credits
# data.truncated is true when the balance forced a reduced output cap
import time
key = "qubit-desk:audit:" + input_hash + ":a1" # include the LANE in the key
req = urllib.request.Request(BASE + "/run", data=json.dumps(body).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.loads(r.read().decode())["data"]["job_id"]
while True:
job = call("/jobs/" + job_id)
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
if job["status"] == "failed":
raise RuntimeError(job.get("error") or "the job failed")
result = json.loads(job["output"]["output"]) # the envelope described below
print(result["verdict"], "-", result["headline"])
print("charged", job["charged_credits"], "credits")
const key = `qubit-desk:audit:${inputHash}:a1`; // include the LANE in the key
const res = await fetch(BASE + "/run", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
"Idempotency-Key": key
},
body: JSON.stringify(body)
});
const { data: { job_id } } = await res.json();
let job;
do {
await new Promise((r) => setTimeout(r, 1500));
job = await call("/jobs/" + job_id);
} while (job.status !== "succeeded" && job.status !== "failed");
if (job.status === "failed") throw new Error(job.error || "the job failed");
const result = JSON.parse(job.output.output);
console.log(result.verdict, result.headline, "charged", job.charged_credits);
req, _ := http.NewRequest("POST", base+"/run", bytes.NewReader(mustJSON(body)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "qubit-desk:audit:"+inputHash+":a1")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
// read data.job_id, then poll GET /jobs/{id} until data.status is succeeded or failed.
// data.output.output is the JSON string; data.charged_credits is the real cost.
// POST /run with the Idempotency-Key header, then poll GET /jobs/{id}.
// The key must include the lane: two lanes over one circuit are two distinct runs.
String key = "qubit-desk:audit:" + inputHash + ":a1";
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
String started = HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
// then poll /jobs/{data.job_id} until data.status is terminal
uri = URI(BASE.to_s + "/run")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req["Idempotency-Key"] = "qubit-desk:audit:#{input_hash}:a1"
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]
job = nil
loop do
sleep 1.5
job = call("/jobs/#{job_id}")
break if %w[succeeded failed].include?(job["status"])
end
raise "the job failed" if job["status"] == "failed"
result = JSON.parse(job["output"]["output"])
puts "#{result['verdict']} - charged #{job['charged_credits']}"
<?php
$ch = curl_init(BASE . "/run");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer " . TOKEN,
"Idempotency-Key: qubit-desk:audit:" . $inputHash . ":a1",
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);
do {
sleep(2);
$job = call("/jobs/" . $jobId);
} while (!in_array($job["status"], ["succeeded", "failed"], true));
if ($job["status"] === "failed") { throw new RuntimeException("the job failed"); }
$result = json_decode($job["output"]["output"], true);
echo $result["verdict"], " - charged ", $job["charged_credits"], "\n";
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", $"qubit-desk:audit:{inputHash}:a1");
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var started = await Http.SendAsync(req);
var jobId = JsonDocument.Parse(await started.Content.ReadAsStringAsync())
.RootElement.GetProperty("data").GetProperty("job_id").GetString();
JsonElement job;
do {
await Task.Delay(1500);
job = await QubitDesk.Call("/jobs/" + jobId);
} while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"));
Step 6 — /run-stream (metered, SSE)
The same body and the same key rules, delivered as Server-Sent Events. Accumulate the delta frames; the done frame carries charged_credits (the real cost, usually far below the hold) and truncated.
# METERED, Server-Sent Events. Same Idempotency-Key rules as /run.
curl -N -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: qubit-desk:budget:e7819654:1288:a1" \
-d @body.json
# event: job data: {"job_id":"job_..."}
# event: delta data: {"text":"{\"lane\":\"budget\","}
# event: delta data: {"text":"\"verdict\":\"needs_rework\","}
# event: done data: {"status":"succeeded","charged_credits":1731,"truncated":false}
#
# Accumulate every delta's text. If the stream dies mid-flight, repair the longest
# parseable prefix rather than discarding it -- see "Recovering a truncated reply".
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps(body).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", "qubit-desk:budget:" + input_hash + ":a1")
raw, done = "", None
with urllib.request.urlopen(req) as r:
event = None
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
payload = json.loads(line[6:])
if event == "delta":
raw += payload["text"]
elif event == "done":
done = payload
result = json.loads(raw) # or repair the longest parseable prefix
print(result["verdict"], "charged", done["charged_credits"])
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
"Idempotency-Key": `qubit-desk:budget:${inputHash}:a1`
},
body: JSON.stringify(body)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null, done = null;
for (;;) {
const { value, done: fin } = await reader.read();
if (fin) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ")) {
const p = JSON.parse(line.slice(6));
if (event === "delta") raw += p.text;
else if (event === "done") done = p;
}
}
}
const result = JSON.parse(raw);
console.log(result.verdict, "charged", done.charged_credits);
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(mustJSON(body)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "qubit-desk:budget:"+inputHash+":a1")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
var raw strings.Builder
event := ""
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
case strings.HasPrefix(line, "data: ") && event == "delta":
var p struct{ Text string }
json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &p)
raw.WriteString(p.Text)
}
}
// POST /run-stream and read the response as lines. `event: delta` frames carry
// {"text": "..."} fragments to concatenate; `event: done` carries the settlement.
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", "qubit-desk:budget:" + inputHash + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<java.util.stream.Stream<String>> res =
HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
StringBuilder raw = new StringBuilder();
res.body().forEach(line -> {
if (line.startsWith("data: ")) {
// append the "text" field of the delta frames
raw.append(line.substring(6));
}
});
uri = URI(BASE.to_s + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req["Idempotency-Key"] = "qubit-desk:budget:#{input_hash}:a1"
req.body = JSON.dump(body)
raw = ""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ")
event = line[7..]
elsif line.start_with?("data: ") && event == "delta"
raw << JSON.parse(line[6..])["text"]
end
end
end
end
end
result = JSON.parse(raw)
<?php
$ch = curl_init(BASE . "/run-stream");
$raw = "";
$event = null;
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer " . TOKEN,
"Idempotency-Key: qubit-desk:budget:" . $inputHash . ":a1",
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_WRITEFUNCTION,
function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) {
$event = substr($line, 7);
} elseif (str_starts_with($line, "data: ") && $event === "delta") {
$raw .= json_decode(substr($line, 6), true)["text"];
}
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
$result = json_decode($raw, true);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", $"qubit-desk:budget:{inputHash}:a1");
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string ev = null, line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event: ")) ev = line[7..];
else if (line.StartsWith("data: ") && ev == "delta")
raw.Append(JsonDocument.Parse(line[6..]).RootElement
.GetProperty("text").GetString());
}
var result = JsonDocument.Parse(raw.ToString());
Recovering a truncated reply
A stream stops mid-string or mid-key far more often than it stops just after a closing brace, so appending the missing brackets to whatever arrived almost never parses. The page walks the text once, records every offset at which a complete value had just been read together with the bracket stack at that point, then tries those offsets newest-first: truncate there, drop a dangling comma, append the closers that stack needs, parse. The first one that parses is the longest recoverable prefix. Do the same rather than discarding a reply that died at 80%.
Rate limits and costs
/estimate,/meand/guestare free./runand/run-streamare metered. You are chargedcharged_credits, nothold_credits.- If the balance sits between
min_creditsandhold_creditsthe run still executes with a reduced output cap and returnstruncated: true. Surface that rather than presenting a clipped answer as complete. - Back off on a 429. Never tight-loop.
Provenance
A derived work of @k-dense-ai/qiskit (the audit lane), @k-dense-ai/qutip (decohere) and @k-dense-ai/pennylane (budget), each credited on the lane it informs. Not a republication of those skills.