The Hangar SDK
The Hangar SDK (software development kit) is a Python package, gethangar, that lets your product run your application on Hangar for each of your customers. You pay from your prepaid credit for the jobs and sessions that your server starts, and you set a spend limit on each request key.
Your server stores request keys and keeps each customer as a tenant. A request key lets your server start jobs for your organisation, or for one tenant only. You choose which when you make the key. With the package, your server can do these tasks for a tenant:
- Start jobs of your releases and CPU sessions.
- Follow each job.
- Download the artefacts and the return of the job. The return is the signed record that ends a job.
- Verify the return.
The package has one supported API (the set of calls that your code can use): sessions, execute, inspect, retrieve and verify. The package uses only the Python standard library. The hangar command line is a second client of the same API routes. The Hangar app shows the same jobs, releases, tenants and keys.
1. Install
pip install gethangar
The package is gethangar on PyPI, and you import hangar_sdk. The package runs on CPython 3.11 and later. The package imports nothing outside the Python standard library, at runtime or at build time. The package connects to Hangar over TLS 1.3. The package checks the connection against the certificate authorities of your system, or against the file that HANGAR_CA_FILE names.
TALOS Defence builds each release of the package twice and compares the two builds. The sdk-release-v1 manifest lists the digest of the Python wheel (the installable file) and the digest of the source archive. TALOS Defence signs the manifest with its release root.
import hangar_sdk
print(hangar_sdk.__version__) # 2.1.0
2. Request keys
Your server signs its requests with a request key. An owner of your organisation makes the key in the Hangar app or with the CLI. The owner signs in with a passkey. Every organisation whose sign-up checks have passed can make organisation keys with jobs, sessions, uploads and usage. A key with jobs can also start, follow and stop GPU jobs now for your organisation (see A GPU now, below). A key never books, tops up, approves or makes a key. A tenant key, and the tenants and releases capabilities, need a vendor organisation. For any other organisation, the answer is vendor_not_enabled. An organisation whose sign-up is held or declined gets account_not_active.
$ hangar keys create --name build-server --capability jobs --capability uploads
Key hgk_syd_7c41e0b3d9f2a81c5e604b2d made for TALOS Defence Pty Ltd ✓
capabilities jobs, uploads
scope the organisation
ends Thu 21 Jan 2027
Secret 4c1f0a9e7b2d5c83e6f190a4d7b2c58e19f3a6d0b4e72c158a9d3f6e0b1c47a2
Hangar shows the secret this once. Keep it in your secret store.
A key has these parts:
| Part | What it is |
|---|---|
| Key id | hgk_, the cell that issued the key (syd for Sydney, mel for Melbourne), _ and 24 hexadecimal digits. The key id is not secret. The SDK and the CLI send each request to the API of the cell the key names. |
| Secret | 32 random bytes, shown once as 64 hexadecimal digits. Hangar stores the secret sealed under the key of your organisation. |
| Capabilities | One or more of jobs, sessions, uploads, tenants, releases and usage. Hangar refuses any other call with key_capability_missing. |
| Scope | Your organisation, or one tenant. A tenant key works for that tenant only, and has only jobs, sessions, uploads and usage. Packages, releases, channels and tenants belong to your organisation. Only an organisation key manages them. |
| Expiry | 1 to 365 days, 90 by default. |
| Source ranges | Optional address ranges in CIDR form. Hangar refuses a request from another address with key_source_refused. |
| Spend limit | Optional: the most that the jobs of the key may spend, in Australian cents including GST. When a key reaches the limit, Hangar starts no job for the key (key_spend_limit_reached). A job that the key starts ends when the rest of the limit runs out. |
You can have two active keys for each scope. This lets you rotate keys without a pause. Make the new key, move your servers to the new key, then revoke the old key. hangar keys revoke takes effect on the next request, including a request that the package is retrying. Hangar adds each create and each revoke to the record chain of your organisation. GET /v1/keys shows the last use of each key.
| Call | What it does |
|---|---|
POST /v1/keys | Makes a key. Only owners, signed in with a passkey, can make a key. The answer contains the secret one time. |
GET /v1/keys | Lists the keys. Each key shows the prefix, capabilities, scope, expiry, last use, spend limit, spend so far, jobs started and images pushed. |
GET /v1/keys/{id} | Gets one key. The answer has the same fields as the list. |
PUT /v1/keys/{id}/spend-limit | Sets, changes or removes the spend limit of a key: {"spend_limit": 20000} in cents, or null. Only owners, signed in with a passkey, can change a limit. A key cannot change its own limit. |
POST /v1/keys/{id}/revoke | Revokes a key at once. |
The package reads the key id from HANGAR_KEY_ID. The package reads the secret from the file that HANGAR_KEY_FILE names. The file must have mode 0600. hangar keys create --save FILE writes the file.
3. Tenants
A tenant is one of your customers. Hangar stores the jobs, sessions, uploads and usage of each tenant separately. The return of each job shows the tenant.
tenant = client.tenants.create(reference="harbour-17", display_name="Harbour Water")
print(tenant["id"]) # tnt_0192f3a1c7d84e6b9a0c5d2e1f4b7a93
| Field | What it holds |
|---|---|
id | tnt_ and 32 hexadecimal digits. |
reference | Your own reference for the customer, 1 to 128 characters, unique among your tenants. |
display_name | The name that the returns and the usage report show, 1 to 100 characters. |
abn | The ABN of the customer, 11 digits, or none. |
state | active, suspended or closed. |
policy | The policy of the tenant: the destinations that the tenant can reach and the release limits. The tenant policy can narrow the policy of your account. The tenant policy never widens the policy of your account. |
When you suspend a tenant, Hangar refuses new jobs and sessions for the tenant with tenant_suspended. Hangar also revokes the session tokens of the tenant. Running attempts finish in their window. When you resume the tenant, the suspension ends. When you close a tenant, Hangar cancels the jobs of the tenant that have not ended, stops the sessions, and revokes the session tokens and the request keys of the tenant. Hangar also schedules the data of the tenant for deletion when the retention period ends.
Hangar stores the objects of each tenant under that tenant. Hangar encrypts the objects with a tenant key that the key of your organisation wraps. Content addressing (naming an object by its digest) works for each tenant separately. If you upload the same bytes for two tenants, Hangar stores two objects. This stops a tenant from learning what another tenant has. You cannot use an artefact that you uploaded for one tenant as an input for another tenant. A tenant key cannot read an object of another tenant. In both cases, Hangar answers not found, as Hangar does for bytes that Hangar does not store (data_not_found for an input, object_not_found for a read).
Tenant routes: POST /v1/tenants, GET /v1/tenants, GET /v1/tenants/{id}, PUT /v1/tenants/{id}/policy, and POST /v1/tenants/{id}/suspend, /resume and /close.
4. The five calls
from hangar_sdk import Client
client = Client.from_env()
Client(endpoint, key_id, secret, *, tenant=None, ca_file=None, timeout=30.0) makes a client. Client.from_env() reads HANGAR_KEY_ID, HANGAR_KEY_FILE, HANGAR_TENANT and HANGAR_CA_FILE, and sends requests to the API of the cell the key names, or to HANGAR_API when it is set. When a cell answers wrong_cell, the client sends the request once to the cell its key names. client.for_tenant(tnt) makes a client for one tenant. client.instant.start(...) and client.execute(...) take region_policy (one of the five settings) or region (sydney or melbourne): the job's own Region setting, refused with region_policy_refused when the organisation's setting is an only setting or is locked and this one differs. A job placed in the other Region runs there as a visiting job, and its start answer carries visit: client.visits.list(), get(id), stop(id) and token(id) work in the home cell, and the token reads the job in its own cell as Authorization: Hangar-Token hgj_<cell>_....
| Call | What it does |
|---|---|
| Sessions | client.sessions.start(...) starts a CPU session. run(id, argv) runs a command in the session. output(id, command_id, after=0) reads the output of a command from an offset. attach, stop and certificate reattach to the session, end the session and get its certificate. |
| Execute | client.execute(release=..., or package=... and channel=..., inputs=..., pool=None, narrowing=None, client_ref=None) starts a job of a release. If you give no pool, the default pool of the endpoint runs the release. On the hosted service, the default pool is hosted-cpu-syd. In a customer runtime, the default pool is the pool of the runtime. |
| Inspect | client.inspect(job_id) gets the job. client.events(job_id, after=0) streams the events of the job. client.wait(job_id) waits until the job returns. client.cancel(job_id) cancels the job. |
| Retrieve | client.retrieve(job_id, dest) downloads the return and every artefact. The call checks the digest of each artefact against the return. |
| Verify | client.verify(certificate) checks the signatures of the return with the hangar CLI, and reports every check. |
A front end or a browser of a tenant follows one job with a job token, not with a request key. client.job_token(job_id, ["read", "stream"]) returns {"token": "hgm_...", "expires": ...} one time. The browser sends the token as Authorization: Hangar-Token hgm_... to the routes of that job only. The read scope allows the job, the return and the list of artefacts. The stream scope allows the events. The cancel scope allows the browser to cancel the job. A token lasts at most 12 hours, and never longer than the envelope of the job. A token stops working in these cases: the job is cancelled, the tenant is suspended or closed, or the key that made the token is revoked. A stopped token stays stopped. A token cannot make another token and cannot start a job. Any other route answers job_token_scope. A browser on another origin follows the job through the web service of the vendor. That web service sends the token for the browser.
The same client also covers uploads, tenants and usage. client.uploads.put_file(path) and put_bytes(data) return the sha384: label of the object. The package returns the label only when Hangar has stored the object, so a job never names data that is not stored yet. After the last chunk, the package asks Hangar once to complete the upload, with the header Prefer: respond-async. Hangar checks the digest of every chunk, then joins the chunks into one object. The package reads the state of the check with GET /v1/uploads/{id} 2 seconds after the request. The wait between two reads grows to 15 seconds. When the check fails, the package raises the error of the check: upload_digest_mismatch when the file changed during the upload, or upload_completion_failed when Hangar could not finish storing the upload. After 6 hours, the package stops the wait and raises upload_completing, an Unavailable error. The detail of the error has the upload id. client.uploads.status(upload) reads the state of the check again. client.tenants manages tenants. client.usage(month=...) reads usage. client.versions() reads GET /v1/versions.
A worked example, from upload to verify
from hangar_sdk import Client
client = Client.from_env()
harbour = client.for_tenant("tnt_0192f3a1c7d84e6b9a0c5d2e1f4b7a93")
# 1. Upload the input. The package sends it in chunks and returns its label.
repository = harbour.uploads.put_file("repository.tar")
# 2. Start a job of the release on the channel stable.
job = harbour.execute(
package="hangar_patch_agent",
channel="stable",
inputs={"repository": {"artefact": repository}},
client_ref="harbour-17/2026-10-23/1",
)
# 3. Follow the job. The stream reconnects by itself and never repeats an event.
for event in harbour.events(job.id):
if event["kind"] == "connection":
print(event["destination"], event["outcome"])
# 4. Retrieve the artefacts and the return, each checked by digest.
job = harbour.wait(job.id)
got = harbour.retrieve(job.id, "out/")
print(job.outcome, [a.name for a in got.artefacts])
# 5. Verify the return offline.
check = harbour.verify(got.return_path)
print(check.valid)
retrieve downloads each artefact through GET /v1/objects/{digest}/content. This route returns the plain bytes over TLS. retrieve compares each artefact with the digest in the return. If a digest differs, the call stops with an error. Then retrieve saves nothing under the name of the artefact.
verify runs hangar verify --json when the hangar command line is on the search path. verify returns the report of that command: valid, each check with its result, and the facts. The signature checks need ML-DSA-87 and P-256, and the hangar command line provides both. If the hangar command line is not installed, verify reports each signature check as not_run and reports valid as false. hangar_sdk.returns.facts(data) decodes the facts of a return from the canonical CBOR of the return.
Sessions
session = harbour.sessions.start(shape="c", vcpus=2, mode="commands", allowlist=["pypi.org"])
command = harbour.sessions.run(session["id"], ["python3", "-c", "print('ready')"])
for pipe, chunk in harbour.sessions.output(session["id"], command["command_id"]):
print(pipe, chunk.decode())
harbour.sessions.stop(session["id"])
Sessions use the CPU session routes that the page Sandbox sessions describes. The client sets the tenant.
A GPU now
client = hangar_sdk.Client.from_env()
session = client.instant.start("h100", [{"image": "trainer:v3", "command": ["python", "train.py"]}], limit_minutes=60)
job = client.instant.jobs(session.id)[0]
print(client.instant.get(session.id).status, client.instant.job(job["id"])["status"])
client.instant.stop(session.id)
A key with jobs starts a GPU job now, with no booking, as hangar run --gpu does. For h100 or trn2, the job uses a live block of Hangar and the price is by the started minute. For l4 and l4-fraction, the job starts on demand and the price is by the second. client.instant.get reads a session. client.instant.jobs lists the jobs of a session. client.instant.job and client.instant.logs read one job. client.instant.stop stops a session. A key spends credit only on the sessions that the key starts, inside the limits of your organisation. The session and its jobs show the key in started_by. These jobs belong to your organisation, so Hangar refuses a client made for a tenant with tenant_mismatch.
When a key has a spend limit, ends_by of a new session is the time when the rest of the limit runs out. When a key reaches the limit, the call raises LimitReached with the code key_spend_limit_reached. The detail of the error has limit_cents, spent_cents and held_cents.
Images
client = hangar_sdk.Client.from_env()
pull = client.images.pull("docker.io/library/python@sha256:<64 hex>")
print(pull["status"], pull["pushed_by"])
print(client.images.resolve("trainer:v1")["digest"])
A key with uploads can use three calls. client.images.pull registers a one-time pull of a public image by its digest. client.images.list lists the images. client.images.resolve finds the image for a name and tag. A key with jobs can list and resolve too. To push a local image, run hangar upload --image ARCHIVE --name NAME:TAG with the same key. pushed_by shows who last pushed, registered or tagged each image. The registry belongs to your organisation, so Hangar refuses a client made for a tenant with tenant_mismatch.
5. Signing
Each request has three headers:
Authorization: Hangar-Key hgk_syd_7c41e0b3d9f2a81c5e604b2d
Hangar-Signature: t=1792900000,n=<64 hex>,s=<96 hex>
Hangar-Tenant: tnt_0192f3a1c7d84e6b9a0c5d2e1f4b7a93
s is HMAC-SHA-384 under the secret of the key. The message has seven lines joined by a line feed: hangar-request-v1, the method, the path and query exactly as sent, t, n, the SHA-384 of the body in lower-case hexadecimal, and the tenant id or -. Hangar accepts a signature only when the time in the signature is within 60 seconds of the clock of Hangar. Hangar accepts each nonce one time. A tenant key must send Hangar-Tenant, and the header must contain the tenant of the key. The package also sends Hangar-SDK: python/<version>.
The page "The Hangar-Signature header" (docs/api/hangar-signature.md) gives the full message and test vectors. You can write a client in another language from that page alone.
6. Retries, Idempotency-Key and client_ref
Every call that changes state sends an Idempotency-Key. The key is a version 4 UUID. The package makes one key for each call, and sends the same key on each retry. Hangar returns the first answer and does the work one time. Hangar refuses the same key with a different body with idempotency_key_reused.
The package retries a connection failure and a code marked retryable in the catalogue. The package retries up to five times, with full-jitter backoff (a random wait) from 0.5 to 30 seconds. The package obeys Retry-After. The package never retries any other code.
A job also has your client_ref. The client_ref is unique in your organisation and tenant. The client_ref lasts longer than the 24-hour idempotency store. If your process restarts after it sent a job, the process finds the job and does not start a second job. The same client_ref and body return the existing job. Hangar refuses a different body with client_ref_conflict.
Streams resume by offset. client.events remembers the last offset that client.events read. After each answer and after a dropped connection, client.events calls again and reads on from that offset. There is no gap and no duplicate. When the job has returned, client.events reads the end line, and the stream stops. Command output in a session resumes in the same way with client.sessions.output(session, command, after=offset). client.sessions.attach returns the state of the session and the offsets of each command. A second client can then continue where the first client stopped.
7. Errors
Every error has three parts from the catalogue in docs/api/errors.json: what failed, the next step, and what to do if the next step fails. Each entry has code, status, sentence, next_step, if_that_fails and retryable. The package raises a HangarError. The error has the code, the HTTP status, the sentence, the next step, if_that_fails and the request id. The error is a subclass that groups the codes:
| Class | For |
|---|---|
AuthError | A missing, stale, replayed or invalid signature, and an expired or revoked key. |
NotFound | A package, release, job, tenant, key or pool that is not in your organisation. |
Conflict | A state that does not allow the call, such as a held channel or a job that has returned. |
LimitReached | A limit, such as two active keys for one scope, or a key at its spend limit (key_spend_limit_reached). |
Refused | A call that TALOS Warrant or a policy refuses, such as envelope_denied, widening_not_approved or input_contract. |
Unavailable | A retryable code that stayed after the retries of the package. |
A code from a newer Hangar that this version of the package does not know is a plain HangarError. The error has the sentence from the server.
8. The compatibility promise
- Within
/v1, Hangar removes, renames and retypes no route or field. Every addition is optional. - A deprecation has 12 months of notice.
- Every
1.xversion of the package works with every Hangar runtime release in support. - A runtime release raises the minimum version of the package only with 12 months of notice, or at once to close a security defect. Hangar refuses a version below the minimum with
sdk_too_old. The detail of the error shows the minimum version. - Support lasts until the end-of-life date, 5 November 2031.
GET /v1/versions gives the API version, the runtime release, the certificate format, the minimum and current versions of the package, and the digest of the published release key set. For each new runtime, the check runs the tests of the previous release of the package.
9. Usage
$ hangar credit usage --month 2026-10
October 2026, TALOS Defence Pty Ltd
tnt_0192f3a1 Harbour Water 12 jobs 3 sessions 4.2 vCPU-hours A$75.00
tnt_0192f3c4 Northern Ports 4 jobs 0 sessions 1.1 vCPU-hours A$20.00
Total 16 jobs 3 sessions 5.3 vCPU-hours A$95.00
GET /v1/usage?month=YYYY-MM&tenant= gives the metered use of each tenant for a month. The report has jobs of a release, sessions, vCPU-seconds, the amount charged in cents including GST, and the bytes stored. Use this report to bill your own customers. The figures of a tenant appear only in the report of the vendor of that tenant. Hangar meters usage in jobs, compute and storage.