API reference
Start a run
POST/v1/runs
Starts a run on a tracker issue. The issue picks the project, and the key must be allowed on it. Like a run started by hand in the portal, it starts as soon as there’s a slot, and doesn’t wait for anyone to approve it.
curl --fail-with-body -sS https://<workspace>.nightlab.io/v1/runs \
-H "Authorization: Bearer $NIGHTLAB_API_KEY" \
-H "Idempotency-Key: ci-8812-build" \
-H "Content-Type: application/json" \
-d '{"issue": "SHOP-1431", "external_ref": "ci-8812"}'
const res = await fetch(
"https://<workspace>.nightlab.io/v1/runs",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NIGHTLAB_API_KEY}`,
"Idempotency-Key": "ci-8812-build",
"Content-Type": "application/json",
},
body: JSON.stringify({"issue": "SHOP-1431", "external_ref": "ci-8812"}),
},
);
if (!res.ok) throw new Error(await res.text());
const answer = await res.json();
import os, requests
answer = requests.post(
"https://<workspace>.nightlab.io/v1/runs",
headers={"Authorization": f"Bearer {os.environ['NIGHTLAB_API_KEY']}", "Idempotency-Key": "ci-8812-build"},
json={"issue": "SHOP-1431", "external_ref": "ci-8812"},
timeout=30,
)
answer.raise_for_status()
Body
issuestringRequiredThe tracker key, like
SHOP-1431. It picks the project: there’s no project field.action"build" or "improve", default "build"build writes the change and opens a draft pull request once your checks pass. improve only scopes the issue: it reads the issue and the code, and posts what done means back on the issue, writing no code.
check_duplicatesboolean, default falseWith build, first check the change isn’t already in the code.
dry_runboolean, default falseRead the git host and the tracker, and write to neither: no branch, pull request or comment. While the workspace’s writes are off, every run is a dry run.
external_refstring, up to 200Your own id, like the CI job’s. It comes back on the run and on every event about it, and
GET /v1/runs?external_ref=finds the run by it.repostringOnly when the issue’s project has several repositories and you need one: its name, as List projects gives it.
Idempotency
Send an Idempotency-Key header, up to 255 characters. For 24 hours, a retry with the same key and the same body gets the first answer back, marked Idempotent-Replayed: true, and starts nothing. The same key with another body is refused with 422, and while the first request is still being answered, with 409. Keys are per API key: a rotated key starts afresh. A refused start keeps nothing, so retrying it runs anew.
The answer
201 with the run, in state queued. Keep its id to read it, or match its external_ref on the events.
201 Created
{
"id": "run-42",
"project": "shop",
"issue": "SHOP-1431",
"action": "build",
"state": "queued",
"outcome": null,
"no_pr_reason": null,
"pr_url": null,
"pr_state": null,
"external_ref": "ci-8812",
"summary": null,
"created_at": "2026-11-01T10:00:00Z",
"updated_at": "2026-11-01T10:00:00Z",
"finished_at": null,
"links": {
"api": "/v1/runs/run-42",
"portal": "https://app.nightlab.io/runs/run-42"
}
}
Refusals
Each is application/problem+json, with a sentence saying what to change.
| Status | When |
|---|---|
402 | The budget is used up, until it renews or an admin raises it. |
403 | The key lacks runs:write, the issue’s project isn’t one of the key’s, the call came from outside its allow-list, or the workspace isn’t active. |
409 | Setup isn’t finished, the project has no repository nightlab can reach, the issue’s key is on more than one tracker site, the issue already has a run in progress (the answer names it), or the same Idempotency-Key is still being answered. |
422 | A field is wrong (errors lists each), repo isn’t one of the issue’s project’s, or the Idempotency-Key was used with another body. |
429 | The key’s rate limit, with Retry-After; or no run slot is free, in the workspace or on nightlab. |