REST API
The conventions every operation of the Harthelm API follows, and where each operation is documented.
The console and hart use the same API as you do. Every installation serves it under /api/v1 on its console host,
for example https://console.example.com/api/v1. It also serves its OpenAPI document without credentials at
GET /api/v1/openapi.yaml, so you can generate a client for the exact
version your installation runs.
The pages in this section, one per operation and grouped by area in the sidebar, are generated from that OpenAPI document of the documented release (v1.5.0). This page explains the rules they all share. For a walk-through, see Call the API.
Authenticate
Create an API key under Settings › API keys in the console, or with
hart login, which stores one for you. A key looks like hart_<id>_<secret> and acts as
the user who created it, with that user's roles.
Send it in either header:
curl -H "Authorization: Bearer $HART_TOKEN" https://console.example.com/api/v1/mecurl -H "Harthelm-Authorization: Bearer $HART_TOKEN" https://console.example.com/api/v1/meUse Harthelm-Authorization when the Authorization header is taken, for example by your own app behind a workspace
address: the router reads it and removes it, and leaves Authorization alone. hart curl
does this for you.
The console itself uses a session cookie. A state-changing request with that cookie must also send the header
Harthelm-CSRF: 1 and, when the browser sends one, an Origin equal to the console's; otherwise the answer is
403. Requests with an API key need neither.
Operations marked as needing no credentials, such as the version and the
schema of harthelm.yml, work without a key.
Keep keys secret
A key can do everything its user can. Store it in a secret store or in hart's contexts file, never in a repository.
Delete a key you no longer need under API keys, or regenerate it to replace the secret.
Read errors
Every error is an RFC 9457 problem with the content type application/problem+json:
{
"type": "urn:harthelm:problem:plan-changed",
"title": "Conflict.",
"status": 409,
"detail": "The file changed since the plan was made. Plan again."
}| Field | What it holds |
|---|---|
type | about:blank, or a URN urn:harthelm:problem:<name>. Branch on this, not on the text. All types are on Problem types. |
title, detail | Text for people. detail says what happened in this case. |
status | The HTTP status. |
errors | On validation problems: one entry per problem, with pointer (a JSON Pointer into the request body, or into harthelm.yml), message, and for harthelm.yml also line, column and code. |
The status codes:
| Status | When |
|---|---|
400 | The request is malformed. |
401 | No credentials, or the key is invalid or deleted. |
403 | Your role does not allow this, or a cookie request lacks the CSRF header. |
404 | The object does not exist, or you cannot see it. Harthelm does not tell the two apart. |
409 | The change is not possible in the current state; type says why. |
412 | If-Match does not match the current version. |
413 | The body or a file is over its limit. |
422 | The input is invalid; errors names the fields. |
428 | If-Match is missing on a change that needs it. |
429 | Too many requests; wait for the seconds in Retry-After. |
501 | The operation is not available on this installation. |
503 | A dependency is down, or a sleeping workspace could not be woken; Retry-After says when to try again. |
Change a resource safely
Resources that can change carry an ETag header. Send it back in If-Match when you change or delete the resource:
etag=$(curl -s -o /dev/null -D - -H "Authorization: Bearer $HART_TOKEN" \
https://console.example.com/api/v1/workspaces/42 | awk -F': ' 'tolower($1)=="etag" {print $2}' | tr -d '\r')
curl -X PATCH -H "Authorization: Bearer $HART_TOKEN" -H "If-Match: $etag" \
-H "Content-Type: application/json" -d '{"name": "api-server"}' \
https://console.example.com/api/v1/workspaces/42If someone changed it in between, the answer is 412: read it again and repeat your change. Without If-Match the
answer is 428. The few operations that take no If-Match say so on their page.
Some GET operations, such as the OpenAPI document, also accept If-None-Match with an ETag you already have and
answer 304 without a body while it is current; their pages say so.
Retry creates with an idempotency key
POST requests that create something accept an Idempotency-Key header of up to 255 characters. Use a new random
value per intended create and the same value for its retries:
curl -X POST -H "Authorization: Bearer $HART_TOKEN" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"team_id": 7, "name": "api-server", "plan": "ide-s", "base_image": "harthelm"}' \
https://console.example.com/api/v1/workspaces- The first request runs. Its successful answer is kept for 24 hours, per user.
- A repeat with the same key, method, path and body returns that answer again, with
Idempotent-Replayed: true. - A repeat while the first request still runs gets
409withRetry-After: 1. - The same key with another method, path or body gets
422. - A failed attempt is not kept, so a retry runs again.
- A body above 1 MiB with a key gets
413.
Follow long-running actions
Actions that outlive the request, such as creating a workspace or a Sync, answer 202 Accepted with an operation and
its address in Location. Poll GET /operations/{id}, or follow
operation.updated events:
{
"id": "b6a7c1d2-8e4f-4a3b-9c10-5d2e7f8a9b01",
"kind": "workspace.create",
"state": "running",
"target": "/workspaces/42",
"detail": "Cloning repository"
}state is pending, running, succeeded or failed. A failed operation carries a problem in error. Operations
stay readable for 7 days after they finish, for their creator and for everyone who can view the team.
Page through lists
Lists use cursor pagination. Ask with limit (1 to 200, default 50) and get items and next_cursor. Pass
next_cursor as cursor to get the next page; when it is absent, you have everything.
curl -H "Authorization: Bearer $HART_TOKEN" \
"https://console.example.com/api/v1/workspaces?team=7&limit=100"The cursor is opaque. Do not build or change it.
Watch live events
GET /events is a stream of server-sent events. Each event names what changed
(workspace.updated, landscape.updated, stage.updated, operation.updated and more) with ids only; read the
object again to get its new state. The first event of every stream is ready, and it comes again after the server
reconnected internally: read everything you show again when you get it.
Check the version
Every answer carries the header Harthelm-Version with the server's version, and
GET /api/v1/version returns it. hart warns when its own version differs from
the server's in the major or minor number.
The API has its own version, in the info.version of its OpenAPI document. New releases add fields and operations;
ignore fields you do not know. The release notes name every change to the API.