---
url: /sdk-api.md
description: >-
  Multi-language clients for the Kipu Quantum Hub REST APIs - submit quantum
  jobs and manage sessions, list and inspect backends, catalog and share
  services and data pools, and execute managed services.
---

## Kipu Quantum Hub API SDK

> \[!INFO] At a glance
>
> * **Purpose:** multi-language clients for the Kipu Quantum Hub REST APIs - submit quantum jobs and manage sessions, list and inspect backends, catalog and share services and data pools, and execute managed services.
> * **Packages:** [`qhub-api`](https://pypi.org/project/qhub-api/) (Python ≥ 3.9, `pip install qhub-api`), [`@quantum-hub/qhub-api`](https://www.npmjs.com/package/@quantum-hub/qhub-api) (Node ≥ 18 / ESM, `npm install @quantum-hub/qhub-api`).
> * **Entry points:** `HubPlatformClient`, `HubQuantumClient`, `HubServiceClient` - one per API surface, with identical class names in both languages.
> * **Use it when:** you need to talk to the Quantum Hub programmatically instead of through the dashboard or `qhubctl` CLI - for example, orchestrating jobs from a notebook, wiring the Hub into an application, or scripting catalog management.
> * **Pairs with:** the `qhubctl` CLI (shares the same config file and env vars), the [Quantum Hub dashboard](https://hub.kipu-quantum.com), and the public [docs site](https://docs.hub.kipu-quantum.com).
> * **Quick reference:** `HubQuantumClient(api_key=...).jobs.create_job(backend_id, shots, input) -> Job`, then `jobs.get_job_status(id)` / `jobs.get_job_result(id)`.
> * **New here?** Start with [Quickstart](#quickstart), then skim [Concepts](#concepts).

The SDK wraps three independent APIs that together make up the Kipu Quantum Hub.
Each API has its own client class because the three surfaces have distinct auth flows, base URLs, and scopes - you pick the client that matches the work you are doing.

Capabilities exposed by the SDK:

* Submit quantum jobs to any supported backend and stream their results.
* Open sessions for batched or dedicated runs and drive them to completion.
* List and inspect backends, including calibration and configuration data.
* Browse the service catalog, manage service definitions, and trigger managed or workflow service executions.
* Read organization, user, subscription, application, data-pool, grant, and notification resources on the platform.
* Invoke deployed services through the service gateway using short-lived bearer tokens.

Packages shipped:

| Package                                                                        | Language                | Install                             |
| ------------------------------------------------------------------------------ | ----------------------- | ----------------------------------- |
| [`qhub-api`](https://pypi.org/project/qhub-api/)                               | Python                  | `pip install qhub-api`              |
| [`@quantum-hub/qhub-api`](https://www.npmjs.com/package/@quantum-hub/qhub-api) | TypeScript / JavaScript | `npm install @quantum-hub/qhub-api` |

Both packages are generated from the same Fern/OpenAPI definitions and ship the same three clients and the same DTOs; field casing differs per language (see [Reference](#reference)).

## Installation

The Python package targets Python 3.9+ and depends on `httpx` and `pydantic`.
The TypeScript package is published as an ESM-only module with native-`fetch` transport, so it works on Node 18+ and modern browsers.

::: tabs key:pythonTS

\== Python

```bash
pip install --upgrade qhub-api
```

\== TypeScript

```bash
npm install @quantum-hub/qhub-api
```

:::

The TypeScript package exposes one entry point per API via subpath exports, so imports are always `@quantum-hub/qhub-api/platform`, `@quantum-hub/qhub-api/quantum`, or `@quantum-hub/qhub-api/service`; there is no top-level bundle export.

## Concepts

The domain model is shared across both languages; only the casing of field names differs (`backend_id` in Python, `backend_id` in the wire format, still `backend_id` on TypeScript DTOs - the Fern generator preserves the OpenAPI casing for quantum types).

* **Platform API** - the catalog-of-record for organizations, users, subscriptions, services, algorithms, implementations, data pools, applications, and marketplace listings.
* **Quantum API** - the execution surface for running quantum workloads on provider backends.
* **Service API** - the gateway in front of deployed managed/workflow services; each service is reachable under its own base URL.
* **Backend** - a provider-hosted QPU, simulator, or annealer with its own capabilities, queue, and calibration; referenced by `backend_id`.
* **Session** - a time-bounded execution context on a backend in either `batch` or `dedicated` mode, used to group jobs and amortize provider startup cost.
* **Job** - a single execution request (input circuit + `shots` + backend-specific params) that lives either inside a session or stand-alone.
* **Workload** - the union of jobs and sessions, used by listing endpoints that return both.
* **Service** - a managed or workflow service published in the catalog.
* **Service Execution** - an invocation of a published service; has its own status lifecycle, logs, and result files.
* **Organization** - a multi-tenant namespace; requests can be scoped to a specific organization via `organizationId` / `X-OrganizationId`.
* **Context** - a snapshot of the currently-authenticated principal (user or organization) as persisted by `qhubctl login` into the shared config file.

## Authentication

Each client takes a single credential tailored to its API.

* `HubPlatformClient` and `HubQuantumClient` authenticate with a personal access token or service execution token, sent as the `X-Auth-Token` request header.
* `HubServiceClient` authenticates with a short-lived bearer token minted for a specific service execution; it is sent as `Authorization: Bearer <token>`.

Tokens are never cached or refreshed by the SDK itself - the caller is responsible for providing a valid credential, and for rotating service execution tokens before they expire.
The Python SDK ships optional credential *helpers* (in `qhub.api.credentials`) that you can use to pull a token out of the environment or the shared `qhubctl` config file; they are not wired into the client constructors automatically, so you have to pass the resolved token in yourself (see below).

### Python credential helpers

The `qhub.api.credentials` module exposes `CredentialProvider` implementations you can compose.
Every helper returns the access token string; wire it into the client by passing `api_key=<token>` (Platform/Quantum) or `token=<token>` (Service).

| Provider                                        | Where it looks                                                                                                                                                                                                                                         |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `StaticCredential(token)`                       | The value you pass in.                                                                                                                                                                                                                                 |
| `EnvironmentCredential()`                       | `KQH_SERVICE_EXECUTION_TOKEN`, then `KQH_PERSONAL_ACCESS_TOKEN`, then legacy `PLANQK_SERVICE_EXECUTION_TOKEN`, `SERVICE_EXECUTION_TOKEN`, `PLANQK_PERSONAL_ACCESS_TOKEN`.                                                                              |
| `ConfigFileCredential()`                        | JSON file at `KQH_CONFIG_FILE_PATH` env var, else `PLANQK_CONFIG_FILE_PATH`, else `~/.config/qhubctl/config.json` (`%LOCALAPPDATA%\qhubctl\config.json` on Windows), falling back to the legacy planqk path. Expects `{"auth": {"value": "<token>"}}`. |
| `DefaultCredentialsProvider(access_token=None)` | Tries `StaticCredential`, then `EnvironmentCredential`, then `ConfigFileCredential`, in that order.                                                                                                                                                    |

If no credential can be resolved, every helper raises `CredentialUnavailableError`.

```python
from qhub.api.credentials import DefaultCredentialsProvider
from qhub.api.quantum import HubQuantumClient

token = DefaultCredentialsProvider().get_access_token()
client = HubQuantumClient(api_key=token)
```

The TypeScript SDK does not ship credential helpers; pass the token explicitly (commonly via `process.env.KQH_PERSONAL_ACCESS_TOKEN`).

### Organization scoping

Most Platform and Quantum endpoints accept an `X-OrganizationId` header to scope the request to a specific organization.

* On `HubQuantumClient` there is a dedicated `organization_id` / `organizationId` constructor option (and, in TypeScript, a per-request override).
* On `HubPlatformClient` there is no dedicated option - pass `headers={"X-OrganizationId": "<id>"}` at construction time.
* Both clients send the header on every request, so construct one client per organization if you need to switch at runtime.
* The Python `qhub.api.context.ContextResolver` reads the `qhubctl` config file and honours the `KQH_ORGANIZATION_ID` (and legacy `PLANQK_ORGANIZATION_ID`) env vars, but, like credentials, you must feed the resolved id into the client yourself.

## Quickstart

Submit a job, wait for it, and fetch the result.

::: tabs key:pythonTS

\== Python

```python
import time

from qhub.api.quantum import HubQuantumClient
from qhub.api.quantum.jobs import CreateJobRequestInput_AzureIonqSimulator
from qhub.api.quantum.types import AzureIonqJobInputCircuitItem

client = HubQuantumClient(api_key="YOUR_PERSONAL_ACCESS_TOKEN")

job = client.jobs.create_job(
    backend_id="aws.sim.sv1",
    shots=1000,
    input=CreateJobRequestInput_AzureIonqSimulator(
        circuit=[
            AzureIonqJobInputCircuitItem(targets=[0]),
            AzureIonqJobInputCircuitItem(targets=[1], controls=[0]),
        ],
        gateset="qis",
        qubits=2,
    ),
)

while client.jobs.get_job_status(job.id).status in ("PENDING", "RUNNING"):
    time.sleep(2)

print(client.jobs.get_job_result(job.id))
```

\== TypeScript

```ts
import { HubQuantumClient } from "@quantum-hub/qhub-api/quantum";

const client = new HubQuantumClient({
  apiKey: process.env.KQH_PERSONAL_ACCESS_TOKEN!,
});

const job = await client.jobs.createJob({
  backend_id: "aws.sim.sv1",
  shots: 1000,
  input: {
    type: "AZURE_IONQ_SIMULATOR",
    circuit: [{ targets: [0] }, { targets: [1], controls: [0] }],
    gateset: "qis",
    qubits: 2,
  },
});

while (true) {
  const status = await client.jobs.getJobStatus(job.id!);
  if (status.status !== "PENDING" && status.status !== "RUNNING") break;
  await new Promise((r) => setTimeout(r, 2000));
}

console.log(await client.jobs.getJobResult(job.id!));
```

:::

## HubQuantumClient

Use `HubQuantumClient` to run jobs and manage sessions.
It is the most common entry point for users of the SDK.

### Constructor

| Parameter                            | Required         | Description                                                                                                                 |
| ------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `api_key` / `apiKey`                 | yes              | Personal access token or service execution token; sent as `X-Auth-Token`.                                                   |
| `organization_id` / `organizationId` | no               | Value for the `X-OrganizationId` header; sent on every request from this client.                                            |
| `base_url` / `baseUrl`               | no               | Overrides both the default environment and `environment` if supplied.                                                       |
| `environment`                        | no               | `HubQuantumClientEnvironment.DEFAULT` (Python) / `HubQuantumEnvironment.Default` (TypeScript), see [Endpoints](#endpoints). |
| `headers`                            | no               | Additional headers merged into every request.                                                                               |
| `timeout` / `timeoutInSeconds`       | no               | Read timeout in seconds; defaults to 60 when no custom HTTP client is supplied.                                             |
| `max_retries` / `maxRetries`         | no (TS only)     | Number of retries for transient failures; defaults to 2.                                                                    |
| `follow_redirects`                   | no (Python only) | Passed through to `httpx.Client`; defaults to `True`.                                                                       |
| `httpx_client` / `fetch`             | no               | Inject a preconfigured HTTP client (Python) or `fetch` implementation (TypeScript).                                         |
| `logging`                            | no               | Logger instance or `{level, logger, silent}` config dict.                                                                   |

Python also ships `AsyncHubQuantumClient` with the same shape plus an `httpx.AsyncClient` hook.

::: tabs key:pythonTS

\== Python

```python
from qhub.api.quantum import HubQuantumClient

client = HubQuantumClient(
    api_key="YOUR_PERSONAL_ACCESS_TOKEN",
    organization_id="YOUR_ORGANIZATION_ID",
    timeout=120,
)
```

\== TypeScript

```ts
import { HubQuantumClient } from "@quantum-hub/qhub-api/quantum";

const client = new HubQuantumClient({
  apiKey: process.env.KQH_PERSONAL_ACCESS_TOKEN!,
  organizationId: process.env.KQH_ORGANIZATION_ID,
  timeoutInSeconds: 120,
  maxRetries: 3,
});
```

:::

### Namespaces

`HubQuantumClient` exposes four sub-clients, lazily instantiated on first access.

| Namespace   | Purpose                                                         |
| ----------- | --------------------------------------------------------------- |
| `backends`  | Discover backends and read their configuration and calibration. |
| `sessions`  | Open, inspect, and close quantum sessions.                      |
| `jobs`      | Submit, monitor, retrieve, and cancel jobs (session or not).    |
| `workloads` | List jobs and sessions as a single paginated stream.            |

### Fetch passthrough (TypeScript)

The TypeScript client exposes `client.fetch(input, init?, requestOptions?)` which reuses the client's base URL, auth provider, retry policy, and logging to call an endpoint that does not yet have a typed wrapper.
Python does not have an equivalent; for that case, use `client._client_wrapper` at your own risk or drop down to `httpx` directly.

## Backend

Backends describe the hardware or simulators you can target.

### Methods

| Method                                                                                                          | Description                                         |
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `backends.get_backends(provider=?, only_planqk_sdk=?)` / `backends.getBackends({ provider?, onlyPlanqkSdk? })`  | List backends, optionally filtered by provider.     |
| `backends.get_backend(id)` / `backends.getBackend(id)`                                                          | Fetch one backend with full configuration metadata. |
| `backends.get_backend_status(id)` / `backends.getBackendStatus(id)`                                             | Current status (online/paused/offline/retired).     |
| `backends.get_backend_config(id)` / `backends.getBackendConfig(id)`                                             | Raw backend configuration as a JSON object.         |
| `backends.get_backend_calibration(id, effective_at=?)` / `backends.getBackendCalibration(id, { effectiveAt? })` | Calibration, optionally at a historical timestamp.  |

### Example

::: tabs key:pythonTS

\== Python

```python
for backend in client.backends.get_backends():
    print(backend.id, backend.status, backend.queue_size)
```

\== TypeScript

```ts
const backends = await client.backends.getBackends();
for (const backend of backends) {
  console.log(backend.id, backend.status, backend.queue_size);
}
```

:::

## Session lifecycle

Sessions group jobs on the same backend to amortize provider startup cost.

```
OPEN ──► ACTIVE ──► DRAINING ──► CLOSED
  │         │
  │         └──► INACTIVE
  └──────────────► ABORTED
```

`UNKNOWN` is used for any state the server has not yet resolved (for example, right after creation).

### Methods

| Method                                                                           | Description                                                                             |
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `sessions.create_session(backend_id, mode, provider, ttl=?, tags=?, metadata=?)` | Open a new session. `mode` is `batch` or `dedicated`; `ttl` is max lifetime in seconds. |
| `sessions.get_session(id)`                                                       | Full session record.                                                                    |
| `sessions.get_session_status(id)`                                                | Terse `{status}` view for polling.                                                      |
| `sessions.get_jobs_by_session(id)`                                               | Jobs attached to this session.                                                          |
| `sessions.update_session_state(id, accept_jobs=bool)`                            | Stop or resume accepting new jobs.                                                      |
| `sessions.close_session(id)`                                                     | Close the session; subsequent submissions are rejected.                                 |

### Example - batch session on AWS Braket

::: tabs key:pythonTS

\== Python

```python
session = client.sessions.create_session(
    backend_id="aws.sim.sv1",
    mode="batch",
    provider="AWS",
    ttl=900,
)

try:
    for i in range(5):
        client.jobs.create_job(
            backend_id="aws.sim.sv1",
            shots=500,
            input=my_input,
            session_id=session.id,
            tags=[f"iter-{i}"],
        )
finally:
    client.sessions.close_session(session.id)
```

\== TypeScript

```ts
const session = await client.sessions.createSession({
  backend_id: "aws.sim.sv1",
  mode: "batch",
  provider: "AWS",
  ttl: 900,
});

try {
  for (let i = 0; i < 5; i++) {
    await client.jobs.createJob({
      backend_id: "aws.sim.sv1",
      shots: 500,
      input: myInput,
      session_id: session.id,
      tags: [`iter-${i}`],
    });
  }
} finally {
  await client.sessions.closeSession(session.id!);
}
```

:::

## Job lifecycle

```
PENDING ──► RUNNING ──► COMPLETED
    │          │
    │          ├──► FAILED
    │          ├──► CANCELLING ──► CANCELLED
    │          └──► ABORTED
    └──► CANCELLING ──► CANCELLED
```

`UNKNOWN` covers states the server has not yet resolved.
Terminal states are `COMPLETED`, `FAILED`, `ABORTED`, and `CANCELLED`.

### Methods

| Method                                                                                                    | Description                                                       |
| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `jobs.search_jobs(page=?, size=?, sort=?, service_execution_id=?)`                                        | Paginated listing, optionally filtered by service execution.      |
| `jobs.create_job(backend_id, shots, input, name=?, input_format=?, input_params=?, tags=?, session_id=?)` | Submit a new job.                                                 |
| `jobs.get_job(id)`                                                                                        | Full job record.                                                  |
| `jobs.get_job_status(id)`                                                                                 | Terse `{status}` view for polling.                                |
| `jobs.get_job_result(id)`                                                                                 | Parsed result body (JSON).                                        |
| `jobs.get_job_result_stream(id)`                                                                          | Stream the result file in chunks (Python iterator / Node stream). |
| `jobs.get_job_input(id)`                                                                                  | Input file (string).                                              |
| `jobs.get_job_calibration(id)`                                                                            | Calibration snapshot captured at execution time.                  |
| `jobs.cancel_job(id)`                                                                                     | Request cancellation; state transitions to `CANCELLING`.          |

A second family of methods with the `service_exec_` prefix (Python) / `ServiceExec` suffix (TypeScript) addresses jobs that belong to a managed service execution - e.g. `jobs.get_service_exec_job(service_execution_id, job_id)`, `jobs.close_service_exec_session(service_execution_id, session_id)`, `jobs.cancel_service_exec_job(...)`, and so on.
Use the plain methods when you created the job yourself; use the service-execution-scoped methods when the job was created by a managed service on your behalf.

### Polling for completion

::: tabs key:pythonTS

\== Python

```python
import time

TERMINAL = {"COMPLETED", "FAILED", "ABORTED", "CANCELLED"}

def wait(job_id: str, poll_interval: float = 2.0) -> str:
    while True:
        status = client.jobs.get_job_status(job_id).status
        if status in TERMINAL:
            return status
        time.sleep(poll_interval)

final = wait(job.id)
if final == "COMPLETED":
    result = client.jobs.get_job_result(job.id)
```

\== TypeScript

```ts
const TERMINAL = new Set(["COMPLETED", "FAILED", "ABORTED", "CANCELLED"]);

async function wait(jobId: string, pollMs = 2000): Promise<string> {
  for (;;) {
    const { status } = await client.jobs.getJobStatus(jobId);
    if (status && TERMINAL.has(status)) return status;
    await new Promise((r) => setTimeout(r, pollMs));
  }
}

const final = await wait(job.id!);
if (final === "COMPLETED") {
  const result = await client.jobs.getJobResult(job.id!);
}
```

:::

### Streaming the result file

Large results are better consumed as a byte stream; the `*_stream` / `*Stream` variants hand you chunks rather than parsing the full JSON into memory.

::: tabs key:pythonTS

\== Python

```python
with open("result.json", "wb") as out:
    for chunk in client.jobs.get_job_result_stream(job.id):
        out.write(chunk)
```

\== TypeScript

```ts
import { Writable } from "node:stream";
import { createWriteStream } from "node:fs";

const stream = await client.jobs.getJobResultStream(job.id!);
await stream.pipeTo(Writable.toWeb(createWriteStream("result.json")));
```

:::

## Workloads

`workloads.get_workloads(page=?, size=?, sort=?)` / `workloads.getWorkloads({ page?, size?, sort? })` returns a single paginated list mixing jobs and sessions, tagged with a `type` discriminator (`JOB` or `SESSION`).
Use it when you want a uniform "show me everything running" view; otherwise reach for `jobs.search_jobs` or list sessions individually.

`sort` follows `field,asc|desc` Spring-style syntax (for example `"created_at,desc"`); pass a single string or a sequence to sort by multiple fields.

## HubPlatformClient

`HubPlatformClient` covers the catalog surface: services, applications, organizations, users, data pools, subscriptions, marketplace listings, and more.
The constructor has the same shape as `HubQuantumClient` *minus* the `organization_id` option - to scope Platform requests to an organization, pass `headers={"X-OrganizationId": "<id>"}`.

### Namespaces

| Namespace            | Purpose                                                                    |
| -------------------- | -------------------------------------------------------------------------- |
| `algorithms`         | Algorithms in the catalog.                                                 |
| `applications`       | Applications (projects that consume services).                             |
| `authentication`     | Access tokens and keys (API tokens, APIM / gateway credentials).           |
| `data_pools`         | Data pools (shared datasets).                                              |
| `data_pool_grants`   | Grants that let services read from a data pool.                            |
| `data_pool_shares`   | Visibility grants of data pools to users or organizations.                 |
| `external_services`  | Externally-hosted services registered in the catalog.                      |
| `git_integrations`   | Git provider integrations used by services.                                |
| `implementations`    | Implementations attached to algorithms.                                    |
| `managed_services`   | Managed services (Hub-hosted).                                             |
| `marketplace`        | Marketplace listings.                                                      |
| `organizations`      | Organizations you belong to.                                               |
| `quantum_workloads`  | Cross-organization view of quantum workloads (reporting surface).          |
| `service_executions` | Service execution records, inputs, outputs, cancellation.                  |
| `service_jobs`       | Service jobs (the subset of execution records that are long-running jobs). |
| `service_shares`     | Visibility grants of services to users or organizations.                   |
| `services`           | Services in the catalog (the published, versioned artefacts).              |
| `subscriptions`      | Subscriptions (for services that require opt-in).                          |
| `use_cases`          | Use cases exposed in the catalog / marketplace.                            |
| `user_notifications` | Per-user notifications.                                                    |
| `users`              | User profiles (including `get_me` / `getMe`).                              |
| `workflow_services`  | Workflow services (multi-step managed services).                           |

::: tabs key:pythonTS

\== Python

```python
from qhub.api.platform import HubPlatformClient

platform = HubPlatformClient(
    api_key="YOUR_PERSONAL_ACCESS_TOKEN",
    headers={"X-OrganizationId": "YOUR_ORGANIZATION_ID"},
)

me = platform.users.get_me()
services = platform.services.list_services(page=0, size=20)
```

\== TypeScript

```ts
import { HubPlatformClient } from "@quantum-hub/qhub-api/platform";

const platform = new HubPlatformClient({
  apiKey: process.env.KQH_PERSONAL_ACCESS_TOKEN!,
  headers: { "X-OrganizationId": process.env.KQH_ORGANIZATION_ID! },
});

const me = await platform.users.getMe();
const services = await platform.services.listServices({ page: 0, size: 20 });
```

:::

## HubServiceClient

`HubServiceClient` talks to deployed services through the service gateway.
It takes a short-lived bearer `token` and, in Python, the async client additionally accepts `async_token: Callable[[], Awaitable[str]]` for async-driven refresh.

The default environment is a URL template - `https://gateway.hub.kipu-quantum.com/<context>/<service>/<version>` - that you **must** override by passing the concrete gateway URL for the service you are invoking (the placeholders do not get substituted by the SDK).

### Service Execution lifecycle

```
PENDING ──► RUNNING ──► SUCCEEDED
    │          │
    │          ├──► FAILED
    │          └──► CANCELLED
    └──► CANCELLED
```

### Methods

| Method                                      | Description                                                         |
| ------------------------------------------- | ------------------------------------------------------------------- |
| `service_api.get_service_executions()`      | List executions started through this gateway.                       |
| `service_api.start_execution(request=body)` | Start a new execution; body is a JSON `dict` (reserved keys below). |
| `service_api.get_status(id)`                | Lightweight `ServiceExecution` snapshot.                            |
| `service_api.get_result(id)`                | Result response (HAL-style: `_links` + `_embedded`).                |
| `service_api.get_result_file(id, file)`     | Download a single result file as a byte stream.                     |
| `service_api.get_logs(id)`                  | Chronological list of log entries (oldest first).                   |
| `service_api.cancel_execution(id)`          | Request cancellation of a pending or running execution.             |

### Example

::: tabs key:pythonTS

\== Python

```python
from qhub.api.service import HubServiceClient

service = HubServiceClient(
    base_url="https://gateway.hub.kipu-quantum.com/acme/vqe/v1",
    token=gateway_token,
)

execution = service.service_api.start_execution(request={"input": {"molecule": "H2"}})
for entry in service.service_api.get_logs(execution.id) or []:
    print(entry.timestamp, entry.severity, entry.message)
```

\== TypeScript

```ts
import { HubServiceClient } from "@quantum-hub/qhub-api/service";

const service = new HubServiceClient({
  baseUrl: "https://gateway.hub.kipu-quantum.com/acme/vqe/v1",
  token: gatewayToken,
});

const execution = await service.serviceApi.startExecution({
  request: { input: { molecule: "H2" } },
});
const logs = (await service.serviceApi.getLogs(execution.id)) ?? [];
for (const entry of logs) {
  console.log(entry.timestamp, entry.severity, entry.message);
}
```

:::

### Reserved request keys

`start_execution` takes a free-form JSON body, but two top-level keys are reserved by the gateway:

* `input` - the service-specific payload; every deployed service expects this shape.
* `inputDataRefs` - references to mounted data-pool files, used when the input is too large to inline or lives in a shared data pool.

Everything else in the body is forwarded verbatim to the service.

## Advanced usage

### Running N jobs in parallel

::: tabs key:pythonTS

\== Python

```python
import asyncio

from qhub.api.quantum import AsyncHubQuantumClient

async def run_batch(inputs):
    client = AsyncHubQuantumClient(api_key="YOUR_TOKEN")
    jobs = await asyncio.gather(*(
        client.jobs.create_job(backend_id="aws.sim.sv1", shots=1000, input=i)
        for i in inputs
    ))
    return [j.id for j in jobs]
```

\== TypeScript

```ts
import { HubQuantumClient } from "@quantum-hub/qhub-api/quantum";

async function runBatch(client: HubQuantumClient, inputs: unknown[]) {
  const jobs = await Promise.all(
    inputs.map((input) =>
      client.jobs.createJob({ backend_id: "aws.sim.sv1", shots: 1000, input }),
    ),
  );
  return jobs.map((j) => j.id!);
}
```

:::

### Polling with exponential backoff

::: tabs key:pythonTS

\== Python

```python
import random, time

def wait_with_backoff(job_id, initial=1.0, cap=30.0):
    delay = initial
    while True:
        status = client.jobs.get_job_status(job_id).status
        if status in ("COMPLETED", "FAILED", "ABORTED", "CANCELLED"):
            return status
        time.sleep(delay + random.uniform(0, delay * 0.1))
        delay = min(delay * 2, cap)
```

\== TypeScript

```ts
async function waitWithBackoff(jobId: string, initialMs = 1000, capMs = 30000) {
  let delay = initialMs;
  for (;;) {
    const { status } = await client.jobs.getJobStatus(jobId);
    if (
      status &&
      ["COMPLETED", "FAILED", "ABORTED", "CANCELLED"].includes(status)
    ) {
      return status;
    }
    await new Promise((r) =>
      setTimeout(r, delay + Math.random() * delay * 0.1),
    );
    delay = Math.min(delay * 2, capMs);
  }
}
```

:::

## Reference

### Job

| Field                                    | Type              | Description                                         |
| ---------------------------------------- | ----------------- | --------------------------------------------------- |
| `id`                                     | `str?`            | Unique job identifier.                              |
| `name`                                   | `str?`            | Optional human-readable name.                       |
| `backend_id`                             | `str?`            | Backend the job runs on.                            |
| `provider`                               | `JobProvider?`    | Provider enum (see below).                          |
| `provider_job_id`                        | `str?`            | Provider-assigned job id.                           |
| `input_params`                           | `JsonNode?`       | Backend-specific parameters supplied at submission. |
| `input_format`                           | `JobInputFormat?` | Format of the submitted input.                      |
| `tags`                                   | `List[str]?`      | Free-form tags.                                     |
| `status`                                 | `JobStatus?`      | See [Job lifecycle](#job-lifecycle).                |
| `created_at` / `started_at` / `ended_at` | `str?` (ISO 8601) | Lifecycle timestamps.                               |
| `runtime`                                | `int?`            | Runtime in milliseconds.                            |
| `shots`                                  | `int?`            | Number of shots.                                    |
| `session_id`                             | `str?`            | Parent session, if any.                             |
| `sdk_provider`                           | `JobSdkProvider?` | SDK that produced the input (see below).            |

### Session

| Field                                                    | Type                  | Description                                                           |
| -------------------------------------------------------- | --------------------- | --------------------------------------------------------------------- |
| `id`                                                     | `str?`                | Unique session identifier.                                            |
| `backend_id`                                             | `str?`                | Backend the session runs on.                                          |
| `provider`                                               | `SessionProvider?`    | Provider enum.                                                        |
| `status`                                                 | `SessionStatus?`      | See [Session lifecycle](#session-lifecycle).                          |
| `mode`                                                   | `SessionMode?`        | `"batch"` or `"dedicated"`.                                           |
| `created_at` / `started_at` / `closed_at` / `expires_at` | `str?`                | Lifecycle timestamps.                                                 |
| `usage_time_millis`                                      | `int?`                | Billed usage time.                                                    |
| `provider_id`                                            | `str?`                | Provider-assigned session id.                                         |
| `tags`                                                   | `List[str]?`          | Free-form tags.                                                       |
| `metadata`                                               | `JsonNode?`           | Free-form metadata supplied at creation.                              |
| `sdk_provider`                                           | `SessionSdkProvider?` | SDK that opened the session.                                          |
| `final`                                                  | `bool?`               | True once the session is in a terminal state.                         |
| `final_not_aborted`                                      | `bool?`               | True when the session reached a terminal state without being aborted. |

### Backend

| Field                   | Type                       | Description                                                               |
| ----------------------- | -------------------------- | ------------------------------------------------------------------------- |
| `id`                    | `str?`                     | Backend identifier (e.g. `aws.sim.sv1`).                                  |
| `internal_id`           | `str?`                     | Hub-internal id.                                                          |
| `provider`              | `BackendProvider?`         | Azure, AWS, IBM, QRYD, QUDORA, QUANDELA, IQM.                             |
| `hardware_provider`     | `BackendHardwareProvider?` | Actual hardware vendor.                                                   |
| `name` / `display_name` | `str?`                     | Machine-readable / human-readable name.                                   |
| `type`                  | `BackendType?`             | `QPU`, `SIMULATOR`, `ANNEALER`, `UNKNOWN`.                                |
| `technology`            | `BackendTechnology?`       | `SUPERCONDUCTING`, `TRAPPED_ION`, `PHOTONIC`, `NEUTRAL_ATOMS`, `UNKNOWN`. |
| `status`                | `BackendStatus?`           | `ONLINE`, `PAUSED`, `OFFLINE`, `RETIRED`, `UNKNOWN`.                      |
| `queue_size`            | `int?`                     | Current queue length.                                                     |
| `updated_at`            | `str?`                     | Last-updated timestamp.                                                   |
| `access_type`           | `BackendAccessType?`       | How access is billed (e.g. pay-per-use).                                  |
| `documentation`         | `Documentation?`           | Doc links and per-SDK guidance.                                           |
| `configuration`         | `Configuration?`           | Native gate set, qubit count, supported formats.                          |
| `availability`          | `List[AvailabilityTimes]?` | Operating windows.                                                        |
| `costs`                 | `List[Cost]?`              | Pricing per provider.                                                     |
| `has_calibration`       | `bool?`                    | True if calibration data is available.                                    |
| `free_of_charge`        | `bool?`                    | True if execution is not billed.                                          |

### ServiceExecution (Service API)

| Field                 | Python / wire name                              | Type                     | Description                           |
| --------------------- | ----------------------------------------------- | ------------------------ | ------------------------------------- |
| id                    | `id`                                            | `str`                    | Service execution identifier.         |
| created\_at            | `created_at` / `createdAt`                      | `str`                    | When the execution was created.       |
| started\_at            | `started_at` / `startedAt`                      | `str?`                   | When processing started.              |
| ended\_at              | `ended_at` / `endedAt`                          | `str?`                   | When processing ended.                |
| status                | `status`                                        | `ServiceExecutionStatus` | See lifecycle above.                  |
| type                  | `type`                                          | `ServiceExecutionType?`  | `MANAGED` or `WORKFLOW`.              |
| service\_id            | `service_id` / `serviceId`                      | `str?`                   | Catalog service this run belongs to.  |
| service\_definition\_id | `service_definition_id` / `serviceDefinitionId` | `str?`                   | Service definition (version).         |
| application\_id        | `application_id` / `applicationId`              | `str?`                   | Application that invoked the service. |
| tags                  | `tags`                                          | `List[str]?`             | Free-form tags.                       |

### Pagination wrappers

All paginated endpoints return a `PageResponse*` object with the shape `{content: List[T]?, page: int?, size: int?, total_elements: int?, total_pages: int?}`.
`page` is zero-based.

### Enums

* `JobStatus` - `UNKNOWN | PENDING | ABORTED | RUNNING | COMPLETED | FAILED | CANCELLING | CANCELLED`
* `SessionStatus` - `UNKNOWN | ABORTED | OPEN | ACTIVE | INACTIVE | DRAINING | CLOSED`
* `BackendStatus` - `UNKNOWN | ONLINE | PAUSED | OFFLINE | RETIRED`
* `SessionMode` - `batch | dedicated`
* `JobInputFormat` - `OPEN_QASM_V1 | OPEN_QASM_V2 | OPEN_QASM_V3 | QIR_V1 | BRAKET_OPEN_QASM_V3 | BRAKET_AHS_PROGRAM | IONQ_CIRCUIT_V1 | QISKIT_QPY | QOQO | PERCEVAL | IQM_JOB_INPUT_V1`
* `JobProvider` / `SessionProvider` / `WorkloadResponseProvider` - `AZURE | AWS | IBM | QRYD | QUDORA | QUANDELA | IQM`
* `JobSdkProvider` / `SessionSdkProvider` / `WorkloadResponseSdkProvider` - `QISKIT | BRAKET | PERCEVAL | CLIENT`
* `BackendType` - `QPU | SIMULATOR | ANNEALER | UNKNOWN`
* `BackendTechnology` - `SUPERCONDUCTING | TRAPPED_ION | PHOTONIC | NEUTRAL_ATOMS | UNKNOWN`
* `ServiceExecutionStatus` - `UNKNOWN | PENDING | RUNNING | SUCCEEDED | CANCELLED | FAILED`
* `ServiceExecutionType` - `MANAGED | WORKFLOW`
* `WorkloadResponseType` - `JOB | SESSION`

### Field casing

Python DTOs use snake\_case (`backend_id`, `created_at`) and so do the Quantum-API TypeScript DTOs - the generator keeps the wire casing for this API.
Platform-API TypeScript DTOs use camelCase (`createdAt`, `currentUserPermission`).
Service-API TypeScript DTOs use camelCase for most fields and preserve HAL wire names (`_links`, `_embedded`) on `ResultResponse`.

### Environment variables (Python credential helpers and `qhubctl`)

| Variable                                                                                      | Purpose                                           |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `KQH_SERVICE_EXECUTION_TOKEN`                                                                 | Service execution token; preferred token env var. |
| `KQH_PERSONAL_ACCESS_TOKEN`                                                                   | Personal access token.                            |
| `KQH_CONFIG_FILE_PATH`                                                                        | Override the path to the shared config file.      |
| `KQH_ORGANIZATION_ID`                                                                         | Default organization id for `ContextResolver`.    |
| `PLANQK_SERVICE_EXECUTION_TOKEN` / `SERVICE_EXECUTION_TOKEN` / `PLANQK_PERSONAL_ACCESS_TOKEN` | Legacy token env vars, still honoured.            |
| `PLANQK_CONFIG_FILE_PATH`                                                                     | Legacy config-file env var, still honoured.       |
| `PLANQK_ORGANIZATION_ID`                                                                      | Legacy organization-id env var, still honoured.   |

None of these are auto-consumed by client constructors - use `DefaultCredentialsProvider` (Python) or read `process.env` yourself (TypeScript).

## Error Handling

HTTP errors are raised as typed exceptions.
All Python errors extend `ApiError` (in `qhub.api.{platform,quantum,service}.core.api_error`); all TypeScript errors extend `HubPlatformError`, `HubQuantumError`, or `HubServiceError`.

| Status | Python class                              | TypeScript class                          | Meaning                                              |
| ------ | ----------------------------------------- | ----------------------------------------- | ---------------------------------------------------- |
| 400    | `BadRequestError`                         | `BadRequestError`                         | Malformed request.                                   |
| 401    | `UnauthorizedError`                       | `UnauthorizedError`                       | Missing or invalid credentials.                      |
| 403    | `ForbiddenError`                          | `ForbiddenError`                          | Authenticated but not permitted.                     |
| 404    | `NotFoundError`                           | `NotFoundError`                           | Resource does not exist.                             |
| 409    | `ConflictError` (Platform only)           | `ConflictError` (Platform only)           | Conflict with current state, e.g. publication races. |
| 422    | `UnprocessableEntityError` (Quantum only) | `UnprocessableEntityError` (Quantum only) | Validation failure on the request body.              |
| 500    | `InternalServerError`                     | `InternalServerError`                     | Server-side failure.                                 |

Other failure modes:

* **Auth misconfiguration (TypeScript)** - constructing `HubPlatformClient` / `HubQuantumClient` without `apiKey` throws `HubPlatformError` / `HubQuantumError` with message `"Please provide 'apiKey' when initializing the client"`; `HubServiceClient` without `token` throws `HubServiceError` with `"Please provide 'token' when initializing the client"`.
* **Credential resolution (Python, opt-in)** - `EnvironmentCredential`, `ConfigFileCredential`, `StaticCredential`, and `DefaultCredentialsProvider` raise `qhub.api.credentials.CredentialUnavailableError` when they cannot produce a token.
* **Timeouts (TypeScript)** - hitting `timeoutInSeconds` raises `HubPlatformTimeoutError` / `HubQuantumTimeoutError` / `HubServiceTimeoutError`. In Python, timeouts surface as the underlying `httpx.TimeoutException`.
* **Missing base URL** - if you pass `environment=None` *and* omit `base_url` to the Python constructor, `_get_base_url` raises a plain `Exception("Please pass in either base_url or environment to construct the client")`.

All HTTP error instances expose `status_code`, `headers`, and `body` (Python) / `statusCode`, `rawResponse`, and `body` (TypeScript) so you can inspect the server's response detail.

::: tabs key:pythonTS

\== Python

```python
from qhub.api.quantum.errors import NotFoundError, UnauthorizedError

try:
    client.jobs.get_job("does-not-exist")
except NotFoundError as e:
    print("missing", e.status_code, e.body)
except UnauthorizedError:
    print("refresh your token")
```

\== TypeScript

```ts
import { HubQuantumError } from "@quantum-hub/qhub-api/quantum";

try {
  await client.jobs.getJob("does-not-exist");
} catch (err) {
  if (err instanceof HubQuantumError) {
    console.error(err.statusCode, err.body);
  } else {
    throw err;
  }
}
```

:::

## Endpoints

| API      | Default base URL                                                     | Override                                                                                                                                                     |
| -------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Platform | `https://api.hub.kipu-quantum.com/qc-catalog`                        | `base_url=...` / `baseUrl: ...` or `environment=HubPlatformClientEnvironment.DEFAULT` (Python) / `environment: HubPlatformEnvironment.Default` (TypeScript). |
| Quantum  | `https://api.hub.kipu-quantum.com/quantum`                           | Same options as above with `HubQuantumClientEnvironment` / `HubQuantumEnvironment`.                                                                          |
| Service  | `https://gateway.hub.kipu-quantum.com/<context>/<service>/<version>` | **Always override** - the template placeholders are not substituted by the SDK; pass the full concrete URL via `base_url` / `baseUrl`.                       |

`base_url` / `baseUrl` always wins over `environment`.
To target a staging or on-prem deployment, set `base_url` / `baseUrl` at construction time.
