Loha API
The Loha REST API lets the Loha CLI and your own tools drive the
platform programmatically: list and trigger builds, validate .loha.yml, manage
repositories, and read logs.
The versioned surface lives under /api/v1. Explore it interactively in the
API reference explorer (powered by
Scalar), or consume the machine-readable OpenAPI 3.1 document directly:
GET https://app.loha.longtumlab.com/api/v1/openapi.json
Authentication
Every request authenticates with a workspace-scoped bearer token: either a personal access token (PAT) (acts as you; create under your workspace's Settings → API Keys) or a service account key (acts as a workspace-owned machine identity; the right choice for CI/CD). Both are bound to a single workspace and carry explicit scopes; the API treats them identically. See Personal access tokens for the create / scope / revoke guide.
The principal field returned by GET /api/v1/whoami tells you which kind you
authenticated as (user vs service_account).
Send it as a bearer token (or the x-api-key header):
curl https://app.loha.longtumlab.com/api/v1/whoami \
-H "Authorization: Bearer loha_xxxxxxxxxxxxxxxxxxxx"
GET /api/v1/whoami is the cheapest way to verify a token. It returns the bound
workspace, the token's scopes, and the acting principal.
Scopes
Tokens carry explicit scopes. There is no all-access token. A request to an
endpoint whose scope the token lacks returns 403.
| Scope | Grants |
|---|---|
builds:read | Read builds and logs |
builds:write | Mutate builds (e.g. cancel a running build) |
triggers:run | Fire a pipeline run (manual / deploy) |
triggers:write | Create / update triggers |
pipelines:read | Read triggers, validate .loha.yml |
repos:read | List and read repositories |
repos:code:read | Read source code (diffs and file contents) |
variables:read | Read variable metadata (never values) |
variables:values:read | Read plain variable values in a build plan (never secrets) |
variables:write | Create / update / delete variables |
audit:read | Read audit logs |
webhooks:read | Read outbound webhooks and delivery history |
webhooks:write | Create / update / rotate / delete webhooks |
A token can never reach outside the workspace it was issued for, regardless of its scopes. The workspace is bound to the token, not to your session.
Rate limits
API-token requests are limited per token. When you exceed the limit you get 429
with Retry-After and X-RateLimit-* headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1716742800
Retry-After: 42
Errors
Errors use a consistent envelope:
{ "error": "API key does not have required scope" }
| Status | Meaning |
|---|---|
401 | Missing, invalid, expired, or revoked token |
403 | Token lacks the required scope, or its owner lost workspace access |
404 | Resource not found (or not in this workspace) |
422 | Validation failed (e.g. an invalid .loha.yml) |
429 | Rate limit exceeded |
Endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /api/v1/whoami | — | Verify token, resolve workspace |
| GET | /api/v1/builds | builds:read | List builds (filterable) |
| GET | /api/v1/builds/{id} | builds:read | Build detail |
| DELETE | /api/v1/builds/{id} | builds:write | Cancel a build |
| GET | /api/v1/builds/{id}/logs | builds:read | Build logs (JSON) |
| GET | /api/v1/repos | repos:read | List repositories |
| GET | /api/v1/triggers | pipelines:read | List triggers |
| POST | /api/v1/triggers/create | triggers:write | Create a trigger. See Scheduled triggers |
| POST | /api/v1/triggers/{id}/run | triggers:run | Run a trigger (deploy) |
| POST | /api/v1/pipelines/validate | pipelines:read | Validate a .loha.yml |
| POST | /api/v1/pipelines/plan | pipelines:read | Preview what would run. See loha plan |
| — | /api/v1/webhooks … | webhooks:* | Manage outbound webhooks. See Webhooks |
See the full request/response shapes in the OpenAPI document.
Example: trigger a deploy
TOKEN="loha_xxxxxxxxxxxxxxxxxxxx"
curl -X POST https://app.loha.longtumlab.com/api/v1/triggers/$TRIGGER_ID/run \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"branch":"main"}'
A deploy runs against a committed git ref. Loha builds from your repository and ships through its GitOps path. Commit your changes, then trigger the run.
Scheduled triggers
POST /api/v1/triggers/create accepts type: "schedule" alongside manual and
webhook. A schedule trigger takes two extra body fields:
| Field | Type | Notes |
|---|---|---|
scheduleCron | string | Five-field cron, evaluated in Asia/Bangkok. Required for type: "schedule" and refused on any other type (400). |
scheduleSkipIfUnchanged | boolean | Optional, default false. Skip a scheduled run when the branch has no new commits since the last scheduled run. Needs a repo_file source; true without a repository returns 400. |
curl -X POST https://app.loha.longtumlab.com/api/v1/triggers/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly main",
"type": "schedule",
"scheduleCron": "0 2 * * *",
"scheduleSkipIfUnchanged": true,
"source": { "kind": "repo_file", "repositoryId": "'"$REPO_ID"'", "branch": "main" }
}'
Scheduled triggers are rolled out per workspace, and their number is capped by the
workspace's limit preset. A refused create
returns 403 with a code:
code | Meaning |
|---|---|
FEATURE_DISABLED | Scheduled triggers are not enabled for this workspace |
SCHEDULED_TRIGGER_LIMIT | The workspace already has as many enabled scheduled triggers as it may have |
A cron that runs more often than the preset's minimum interval returns 400.
The create response's trigger object, and every item from GET /api/v1/triggers,
carry five schedule fields. On a non-schedule trigger they are null (false for
scheduleSkipIfUnchanged), except scheduleTimezone, which is always Asia/Bangkok:
| Field | Type | Meaning |
|---|---|---|
scheduleCron | string | null | The cron expression |
scheduleTimezone | string | null | The timezone the cron is evaluated in: Asia/Bangkok |
scheduleNextRunAt | string | null | ISO time of the next scheduled run; null if it cannot be computed |
scheduleLastFiredAt | string | null | ISO time of the last scheduled firing, run or skipped |
scheduleSkipIfUnchanged | boolean | Whether runs are skipped when the branch has no new commits |