Harthelm Docs

REST API

The conventions every operation of the Harthelm API follows, and where each operation is documented.

Last updated 2026-10-19

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:

Terminal
curl -H "Authorization: Bearer $HART_TOKEN" https://console.example.com/api/v1/me
Terminal
curl -H "Harthelm-Authorization: Bearer $HART_TOKEN" https://console.example.com/api/v1/me

Use 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:

409 Conflict
{
  "type": "urn:harthelm:problem:plan-changed",
  "title": "Conflict.",
  "status": 409,
  "detail": "The file changed since the plan was made. Plan again."
}
FieldWhat it holds
typeabout:blank, or a URN urn:harthelm:problem:<name>. Branch on this, not on the text. All types are on Problem types.
title, detailText for people. detail says what happened in this case.
statusThe HTTP status.
errorsOn 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:

StatusWhen
400The request is malformed.
401No credentials, or the key is invalid or deleted.
403Your role does not allow this, or a cookie request lacks the CSRF header.
404The object does not exist, or you cannot see it. Harthelm does not tell the two apart.
409The change is not possible in the current state; type says why.
412If-Match does not match the current version.
413The body or a file is over its limit.
422The input is invalid; errors names the fields.
428If-Match is missing on a change that needs it.
429Too many requests; wait for the seconds in Retry-After.
501The operation is not available on this installation.
503A 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:

Terminal
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/42

If 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:

Terminal
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 409 with Retry-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:

202 Accepted
{
  "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.

Terminal
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.

On this page