---
title: REST API
description: Work a tzTrack space from a script or a CI job over plain HTTPS, with each act's request and answer, the token, the refusals, and a curl example.
slug: /reference/rest-api
order: 6
visibility: public
redirects_from: []
---

# REST API

The REST API is how a program with no MCP client, such as a script or a CI job, works a tzTrack space over HTTPS. It takes the same token as the MCP server at `https://tztrack.ai/mcp`, admits you at the same level, and applies the same rules. It also makes acts the MCP server's tools do not, such as `land`, `check`, and `configure`. Read this page when your caller speaks plain HTTP rather than MCP.

## Requests and answers

Every call is a `POST` to `https://tztrack.turnzero.ai/v1/<act>`, such as `/v1/list`. The body is one JSON object: the act's request, as the table in the section The acts lists it, with `space` beside it. An empty body counts as `{}`.

Leave `space` out where your account owns or is a member of one space: that space is the default. Where it holds several, name one by its id. The project's [setting file](/reference/the-setting-file) holds the id a project works in.

A body is at most 65,536 bytes. `submit` and `comment` carry images, so their bound is 3,145,728 bytes.

A call that works answers 200 and a JSON object with three members:

- `space`, the id of the space the act worked;
- `act`, the act's name;
- `result`, the act's own answer, as the table lists it.

A call that fails answers an error status and a JSON object with two members: `error`, the refusal's name, and `detail`, one sentence on what to do next. Branch on `error`, never on `detail`, since the sentence can change. A refused call changes nothing.

## The token

Send a tzTrack token in the `authorization` header as `Bearer <token>`. It is the token Turn Zero signs for tzTrack, the same token an AI tool holds after you sign in.

A script or a CI job gets its token one of two ways. It can mint one with the action `mint_token`, the grant `tztrack`, and a label you give, from your account's own signed-in session on Turn Zero Cloud. The mint answers a `token claim` line, which you run in a terminal of your own, so the value never passes through the chat. [Mint a token](https://turnzero.ai/cloud/guides/mint-a-token/) gives the steps. Or it can sign in by itself with a code you allow in a browser, the device sign-in, naming `https://tztrack.ai/mcp` as the resource.

[Sign in from a script](https://turnzero.ai/cloud/guides/sign-in-from-a-script/) gives every answer of the device sign-in. The [Quick start](/getting-started/quick-start#connect-a-script-with-no-ai-tool) gives both ways step by step. Keep the token as a secret, as you keep a password. The REST API verifies it as it verifies the token an AI tool holds after sign-in.

Any other credential is refused `credential_refused`, a token for another Turn Zero product among them. A call with no `authorization` header is refused `credential_required`. Both answers carry a `www-authenticate` header naming where the sign-in starts.

In a space of your own you hold the owner's level, and every act works. In a space shared with you, you hold the level its owner gave you: `report`, `contribute`, or `owner`. An act past your level is refused `grant_required`.

## The acts

The table names each act, the lowest level that makes it, the members its request takes, and what `result` holds. `key`, where an act takes it, is your own idempotency key: repeat a write with the same key and it is made once. A write is recorded under your account and your token's label, whatever actor a body names.

A member that names an issue takes its number, as `12` or `"#12"`, or its id, as the MCP tools take it. Those members are `issue`, a settle's `target`, a relate's `target` other than an `arising_from` reference, and follow's `repeat_of`.

| Act | Level | Request | `result` |
| --- | --- | --- | --- |
| `submit` | report | `origin`, `kind`, `title`; where you have them, `text`, `impact`, `workaround`, `proposed_resolution`, `seen_in`, `evidence`, `evidence_basis`, `restricted`, `component`, `problem_key`, `attachments`, `amends`, `withdraws`, `key`; from `contribute`, `requirements`, `labels`, and `priority`, each refused `grant_required` below it | `report`, `issue`, `outcome`, `candidates`, `masked`, `personal_removed` |
| `follow` | report | `report`, `repeat_of`, `key` | `report`, `issue`, `outcome` |
| `list` | report | the filters `state`, `outcome`, `kind`, `component`, `path`, `requirement`, `label`, `priority`, `level`, `alarm`, `origin`, `waiting_for`, `claimed_by`, `plan`, `reporter`, `proposed`, `related`, `since`; `order`, `limit`, `cursor` | `issues`, `cursor` |
| `search` | report | `query`; the filters `state`, `kind`, `component`; `limit`, `cursor` | `issues`, `cursor` |
| `read` | report | `issue` | `issue`, `reports`, `links`, `history`, `comments`, `relations`, `landings`, `checks`, `proposals` |
| `attachment` | contribute | `id`, one image's id as a report or a comment lists it | `type`, the image's media type, and `bytes`, its bytes in base64url |
| `update` | contribute | `issue`, `fields`, `key` | `issue`, `mentioned` |
| `comment` | contribute | `issue`, `text`, `attachments`, `key` | `comment`, `issue`, `mentioned` |
| `relate` | contribute | `issue`, `type`, `target`, `withdraw`, `key` | `issue`, `relation` |
| `next` | contribute | `role`, `branch`, `component`, `paths`, `plan`, `key` | `issue`, `check`, `secret`, `packet` |
| `give_back` | contribute | `issue`, `check`, `secret`, `reason`, `key` | `issue` |
| `wait` | contribute | `issue`, `waiting_for`, `resolution`, `key` | `issue` |
| `cache` | contribute | `since` for a refresh | `link`, `expires_at`, `space`, `since`, `count`, `numbers`, `gone` |
| `land` | owner | `issue`, `commit`, `trailer`, `test`, `proof`, `remainder`, `component`, `key` | `landing`, `issue` |
| `deploy` | owner | `line`, `version`, `order`, `commit`, `environment`, `live`, `withdrawn`, `key` | `build` |
| `ship` | owner | `build`, `landings`, `key` | `build`, `shipped` |
| `check` | owner | `check`, `covers`, `result`, `builds`, `cause`, `component`, `evidence`, `withdraw`, `key` | `check`, `issue`, `outcome` |
| `settle` | owner | `issue`, `outcome`, `target`, `resolution`, `fixed_in`, `key` | `issue`, `related_open` |
| `reopen` | owner | `issue`, `key` | `issue` |
| `unmerge` | owner | `issue`, `key` | `issue` |
| `configure` | owner | `components`, `lines`, `credential_forms`, `personal_forms`, `machinery`, `main_paths`, `constants` | `configuration` |
| `export` | owner | `limit`, `cursor` | `issues`, `comments`, `signals`, `asks`, `landings`, `builds`, `checks`, `cursor` |
| `count` | owner | the filters of `list`, `exclude_label`, `group_by` | `total`, `groups` |
| `rates` | owner | `since`, `until`, `grain`, `component`, `kind`, `label`, `exclude_label` | `since`, `until`, `grain`, `steps`, `time_to_close` |
| `read_space` | owner | nothing | `space`, `counts`, `configuration`, `sampled_merges`, `pass_runs` |

The form of each member is the contract's: [Issue Tracking](https://turnzero.ai/cloud/backend-packages/tztrack/) gives every member's type, bounds, and meaning, and every answer member's.

## What differs from the contract

A few members are set for you, and a few are bounded, each refused where the body passes the bound.

Set for you, whatever the body says:

- A write's actor is your account and your token's label, written as an agent.
- A filing's `evidence_basis` is `claimed` where the body names none.
- Below the owner's level, a filing's `origin` is `field`.

Refused where the body passes the bound:

- An `evidence_basis` other than `claimed` is refused `invalid_request`. So is a `seen_in` entry other than `{ line, version, environment }` with a repository commit as its version.
- A body that names `relayed_level` is refused `invalid_request`. Your level in the space decides which restricted issues your filings join.
- A `waiting_for` of `person:<account>` names the space's owner or one of its members, and any other account is refused `invalid_request`.
- Below the owner's level, an `update` that lowers `restricted` or names `alarm` is refused `grant_required`. A `relate` of the type `arising_from` whose `target` is an issue's number, such as `#7`, is refused `invalid_request` there too.
- `configure` takes the members the table lists. Among `constants` it takes `report_text_retention_days`, `lift_reports`, `lift_window_days`, `lift_account_max`, and `rank_trial_weight`, and refuses every other, `model_budget` among them.
- A `configure` that sets `machinery.passes` to `true` on a space with no model budget sets the budget to 200 in the same call. A budget already set stands.

Every other act of the contract, such as `mint`, `revoke`, or `delete_space`, is refused `grant_required`, whatever your level. Turn Zero Cloud makes those acts for you, and sharing a space and creating one are its own: the MCP server's `share` and `create_issue_space` tools carry them.

## Refusals

A refusal names itself in `error`. [Refusals](/reference/refusals) gives each one's status, cause, and remedy, from `credential_required` to `rate_limited`.

Two answers are no refusal of an act. A path that names no act answers 404 `not_found`, whatever the method: check the act's spelling against the table. A request to an act's path that is no `POST` answers 405 `method_not_allowed`, with an `allow` header naming `POST`.

## An example

This lists the open issues of your one space with curl, the token in the variable `TZTRACK_TOKEN`:

```text
curl -s -X POST https://tztrack.turnzero.ai/v1/list -H "authorization: Bearer $TZTRACK_TOKEN" -H "content-type: application/json" -d '{"state": "open", "limit": 20}'
```

The answer holds the space's id as `space`, `list` as `act`, and the page of issues with its `cursor` as `result`. To raise an issue in a named space:

```text
curl -s -X POST https://tztrack.turnzero.ai/v1/submit -H "authorization: Bearer $TZTRACK_TOKEN" -H "content-type: application/json" -d '{"space": "<id>", "origin": "workspace", "kind": "bug", "title": "The nightly build fails on the login test", "text": "Seen on three runs since Monday."}'
```

Any HTTP client works the same way: a `POST`, the `authorization` header, and one JSON object.

## Related

- [The tools](/reference/the-tools) lists the MCP server's tools, which make some of the same acts, under the same rules.
- [Refusals](/reference/refusals) lists every refusal with its remedy.
- [Sharing a space](/concepts/sharing-a-space) explains the levels a member works at.
- [The setting file](/reference/the-setting-file) gives the file that holds a project's space id.
