---
name: syndai-api
description: "One page for a coding agent: sign in, start a run, follow it,
  answer it, land it and read its receipt. With error codes, limits, the MCP
  server and the CLI."
---

This page is written for an agent to read in one pass. Save it as a skill with `curl -s https://syndai.ai/docs/agents.md > SKILL.md`. Every other page has a Markdown twin too: add `.md` to its URL. The index is at [llms.txt](https://syndai.ai/llms.txt).

## Rules

- Base URL: `https://api.syndai.ai/api/v1/external/coding`. The spec is at `https://api.syndai.ai/openapi.json`.
- Send `Authorization: Bearer $SYNDAI_API_KEY` on every call.
- Creating a run costs credits. Never start one in a loop.
- Phases are open-ended. A run can stop to ask at any time. Act on the state you read, and never assume an order.
- When unsure, read `GET /runs/{id}`.
- Branch on the error `code`, never on `detail`.
- Treat every enum you read as an open string. New values can appear.
- Some steps need a person. A key gets `401`, or `403 HUMAN_APPROVAL_REQUIRED` with a `review_url`. Give that link to your user and stop. Never retry it.

## 1. Auth

Get a key one of two ways:

- `npx -y syndai@latest login` opens a browser. An owner or admin approves it. The key lands in the CLI's config.
- In the app: **Settings**, **Developer**, **API keys**. Pick the **Run lifecycle** preset.

Check it with `GET /me`. It returns `customer_id`, `key_id` and `scopes`. A missing scope is `403 INVALID_SCOPE` with `required_scope`. See [Authentication](/docs/api/authentication).

## 2. Pick a project

`GET /repos` lists projects. Keep the `id`. No project yet? Send `"new_repository": {}` in place of `coding_repository_id` when you create the run.

## 3. Create a run

```bash
curl -s -X POST https://api.syndai.ai/api/v1/external/coding/runs \
  -H "Authorization: Bearer $SYNDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Add a /healthz endpoint that returns 200",
    "coding_repository_id": "<REPO_ID>",
    "external_request_id": "healthz-001"
  }'
```

- `201`: keep `run.id`.
- `409 IDEMPOTENCY_REPLAY`: you sent this `external_request_id` before. Follow `existing_run_id`. No second run started.
- `409 CONFLICT`: the same key with a new body. Pick a new key for new work.
- After a timeout, send the same call again with the same key.
- A follow-up is a new run with `parent_coding_run_id` and a new key.

## 4. Follow it

The simplest loop is the change state. It waits up to 45 seconds for something that matters:

```bash
curl -s "https://api.syndai.ai/api/v1/external/coding/runs/$RUN_ID/change?wait=45&cursor=$CURSOR" \
  -H "Authorization: Bearer $SYNDAI_API_KEY"
```

Leave out `cursor` on the first call. Pass back the `cursor` you got each time. Stop when `done` is `true`. It can return early with no change, so call again. Read:

- `state`: one of `queued`, `working`, `needs_input`, `landing`, `held`, `ready`, `unverified`, `live`, `live_unverified`, `failed`, `reverted`, `cancelled`, or a new value.
- `headline`: one line to show your user.
- `needs_input`: open questions. See step 5.
- `held`: why the landing waits, and its `review_url`.
- `live_url`, `pr_url`, `receipt_url`.

Other ways to follow a run:

- `GET /runs/{id}/events`: server-sent events. Resume with `Last-Event-ID`, and drop repeats by `event_id`.
- `GET /runs/{id}/live`: a light poll.
- [Webhooks](/docs/api/webhooks): signed POSTs to your server.

See [Run lifecycle](/docs/guides/run-lifecycle#2-follow-it).

## 5. Answer it

Read the open asks from `GET /runs/{id}`, in `pending_hitl_requests`. Each has a `hitl_request_id` and `inputs`. Answer every required input:

```bash
curl -s -X POST \
  "https://api.syndai.ai/api/v1/external/coding/runs/$RUN_ID/responses" \
  -H "Authorization: Bearer $SYNDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "hitl_request_id": "<HITL_REQUEST_ID>",
    "idempotency_key": "healthz-answer-001",
    "entries": [{"input_id": "<INPUT_ID>", "text": "Use the existing router."}]
  }'
```

- An entry carries `text`, `selected_option_ids` or `approved`, to match the input's `kind`.
- If you are not sure of an answer, ask your user. Do not guess on their behalf.
- `409 HITL_REQUEST_CONFLICT`: wait a moment and send the same answer again.
- `409 HITL_REQUEST_SETTLED` or `RUN_TERMINATED_BEFORE_RESPONSE`: do not retry. Read the run.

To add guidance while it works, `POST /runs/{id}/steer` with `{"message": "…"}`.

A spec review (confirm, edit or skip the criteria) needs a signed-in person. A key gets `401`. Tell your user the run is waiting. See [Review and land](/docs/guides/review-and-land).

## 6. Land it

What happens after the checks pass depends on the `autonomy` policy: a pull request, a merge, or a push.

| you see | do this |
|---|---|
| `state: ready` on a project Syndai hosts | `POST /runs/{id}/land/go-live`, then keep following |
| `state: held` | read `held.reason`. `POST /runs/{id}/land/retry` or `/land/close` work with a key. |
| `held` that needs a person | give your user `held.review_url`. A key gets `403 HUMAN_APPROVAL_REQUIRED` on go live anyway. |
| `state: live` | report `live_url` |
| want to undo a live change | `POST /runs/{id}/revert`. It starts a new run, which costs credits. |

To stop: `POST /runs/{id}/stop` keeps the work as a draft pull request. `POST /runs/{id}/cancel` drops it.

## 7. Read the receipt

`final_response.content` on `GET /runs/{id}` holds the run's own summary once `final_response.status` is `ready`. Report it to your user.

`receipt_url` is set once the owner shares the run's receipt. The part after `#` is its token. That token opens the JSON at `GET https://api.syndai.ai/api/v1/public/receipts/{token}`, with no key. A run nobody shared has no `receipt_url`.

## Errors

The body is problem details with a `code`. The codes an agent meets most:

| code | status | do this |
|---|---|---|
| `INVALID_TOKEN` | 401 | the key is missing, wrong or revoked. Stop. |
| `INVALID_SCOPE` | 403 | ask for a key with `required_scope` |
| `PLAN_UPGRADE_REQUIRED` | 403 | the plan lacks this. Tell your user. |
| `HUMAN_APPROVAL_REQUIRED` | 403 | give your user `review_url`. Do not retry. |
| `NOT_FOUND` | 404 | stop using that id |
| `IDEMPOTENCY_REPLAY` | 409 | safe repeat. Follow `existing_run_id`. |
| `CONFLICT` | 409 | read the state again, then decide |
| `MISSION_BUSY_RETRY` | 409 | send the same call again after a moment |
| `INVALID_POLICY` | 400 | fix the field `detail` names |
| `QUOTA_EXCEEDED` | 429 | out of credits or at the monthly limit. Tell your user. |
| `RUN_CONCURRENCY_LIMIT` | 429 | wait for a run to end |
| `RATE_LIMITED` | 429 | wait `Retry-After` seconds |
| `CLI_UPGRADE_REQUIRED` | 426 | run `npx -y syndai@latest` |
| `INTERNAL_ERROR` | 5xx | retry with backoff |

Retry `429` and `5xx` with backoff from one second. Any other `4xx` fails the same way each time. The full list is on [Errors](/docs/api/errors).

## Limits

- 600 keyed calls a minute per workspace, across all its keys.
- Per route, a minute: `POST /runs` 120, `GET /runs/{id}` 240, `GET /runs/{id}/events` 60, `POST /runs/{id}/responses` 120.
- `wait` on the change state is at most 45 seconds.
- Your plan sets how many runs may go at once.

## MCP server

Clients that speak MCP can use tools in place of these calls:

```bash
npx -y syndai@latest mcp install claude   # or: cursor, codex
```

The server is `https://api.syndai.ai/api/v1/coding/mcp`. The loop is `changes_start`, then `changes_wait` with the `cursor` until `done`, and `changes_answer` for questions. A refused call comes back as a tool result with `{code, message, next}`. See [MCP](/docs/mcp).

## CLI

`syndai api` calls any route by its operation name with your stored key:

```bash
npx -y syndai@latest api getRun -f coding_run_id=$RUN_ID
npx -y syndai@latest api listRuns -f needs=owner --paginate
```

`syndai change "<what to change>"` runs the whole loop in one command and exits `0` when live, `3` when it needs a person. See [CLI](/docs/cli).
