This page lists the rules of the Syndai coding API. It does not depend on any one language. The other API pages explain each part in more depth.

The words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL mean what [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) says they mean.

All paths here sit under `https://api.syndai.ai/api/v1/external/coding`.

## 1. Keys

1.1. A client MUST send its key on every request as `Authorization: Bearer <key>`.

1.2. A client MUST treat a key as an opaque string. It MUST NOT read the key's text to guess what the key may do. `GET /me` tells it.

1.3. The server MUST check each request against the scope its route needs. A key without that scope MUST get `403` with code `INVALID_SCOPE`. A scope the workspace's plan does not include MUST get `403` with code `PLAN_UPGRADE_REQUIRED`.

1.4. The server MUST turn away a revoked key on its next request, with `401` and code `INVALID_TOKEN`.

1.5. The server MUST limit calls per workspace, and each route MAY have its own limit too. More keys MUST NOT raise a workspace's limit. Over a limit, the server MUST answer `429` and SHOULD send `Retry-After`.

See [Authentication](/docs/api/authentication).

## 2. Run states

2.1. Each run MUST report a `current_phase` that the server sets. `completed`, `failed` and `cancelled` are the end phases. Other phase names MAY be added at any time.

2.2. A client MUST accept phase names it does not know. It MUST NOT treat the phase list as closed.

2.3. The server SHOULD send `coding.run.phase_changed` when a run moves to a new phase. A client SHOULD NOT guess a run's state from events that did not come. When it is unsure, it SHOULD read `GET /runs/{id}`.

2.4. A run in an end phase MUST NOT move to any other phase.

2.5. A client MAY cancel a run that has not ended. Work already pushed to the repo is not rolled back.

2.6. A `completed` run MAY have a `result_kind` of `pull_request` or `no_changes`, or none. `result_kind` is an open string. A client MUST NOT treat it as a phase.

2.7. `GET /runs/{id}` MUST return `final_response.status`. It is an open string. The values today are `pending`, `ready`, `failed` and `unavailable`.

- `ready` MUST mean `final_response.content` holds the text. `ready` wins over any other state.
- A run that has not ended reports `pending`. A client SHOULD keep reading until the status is not `pending`.
- `failed` MUST mean the server kept a record that it could not publish the text. Its `error_code` is `final_response_publication_failed`.
- An ended run with no text and no such record reports `unavailable`.

2.8. The final message text MUST only be sent in `final_response.content` on `GET /runs/{id}`. It MUST NOT be copied into list rows, run summaries, artifacts, stream events or webhooks.

## 3. Repeat-safe requests

3.1. Making a run MUST be safe to repeat for each `external_request_id` in a workspace.

- Sending the same `external_request_id` and the same request again MUST NOT make a second run or charge twice. The server MUST answer `409` with code `IDEMPOTENCY_REPLAY`. The body MUST have `existing_run_id`, and the `Location` header MUST point at the first run.
- A client MUST treat that `409` as success, not as an error to retry. It SHOULD read the run that `Location` names.
- The server compares requests after it puts them in a standard form. Sending the same `external_request_id` with a different request MUST get `409` with code `CONFLICT`. No run is made.

3.2. A client MUST use a new `external_request_id` for each new piece of work. Reusing one for new work gets the old run back. That is a client error, not a server error.

3.3. An `external_request_id` belongs to one workspace. Two workspaces may use the same value without clashing.

3.4. An answer to a question (`POST /runs/{id}/responses`) MUST carry an `idempotency_key`. Sending the same `idempotency_key` for the same `hitl_request_id` again MUST NOT change the run twice.

## 4. Events and the stream

4.1. Each event has a `global_seq`. It comes from one log shared by all runs. So for one run it goes up over time but skips numbers. A client MAY use it to order events and to drop repeats. A client MUST NOT treat a jump in `global_seq` as a lost event.

4.2. A client MUST NOT expect events from different runs to come in any set order.

4.3. `GET /runs/{id}/events` is a Server-Sent Events stream. It first sends the run's stored events, then new ones as they happen. Each event's SSE `id` is its `global_seq`.

4.4. To pick up after a break, a client SHOULD send the last `id` it got as `Last-Event-ID`. It MAY send `?since_global_seq=` instead. The stream then sends only newer events.

4.5. The stream MAY send `stream_heartbeat` events when there is nothing new. A client MUST NOT act on them.

4.6. `coding.run.completed` means the repo work ended in success. It MUST carry no final message text. `coding.run.final_response_ready` means the text can be read. Its fields MUST be only `message_id` and `result_kind`. `coding.run.final_response_failed` MUST carry only `error_code` and `result_kind`.

## 5. Webhooks

5.1. Delivery is at least once. A client MUST handle the same event twice without harm. The body's `event_id` stays the same on every retry and redelivery. A client SHOULD use it to drop repeats.

5.2. Each delivery MUST carry a `Syndai-Signature` header of the form `t=<unix seconds>,v1=<hex>`. `v1` MUST be the HMAC-SHA256 of `{t}.{raw body}`, keyed with the whole signing secret. A client MUST check it before it trusts the body.

5.3. A client SHOULD turn away a delivery whose `t` is more than 300 seconds from its own clock.

5.4. A client SHOULD answer with a `2xx` within 10 seconds. Any other answer, or no answer in that time, counts as a failed try.

5.5. The server MUST retry a failed try, up to 8 tries in all. It waits 30 seconds before the second try and doubles the wait each time after. After the eighth failed try, the delivery is `failed`.

5.6. A failed webhook MUST NOT stop a run or make it fail.

5.7. An endpoint with no `event_types`, or an empty list, MUST get every event type, including ones added later. An endpoint with a list MUST get only those types. The server MUST NOT add new types to that list.

5.8. A client MUST ignore event types it does not know.

See [Webhooks](/docs/api/webhooks).

## 6. Errors

6.1. Every error MUST use the `application/problem+json` content type ([RFC 7807](https://www.rfc-editor.org/rfc/rfc7807)).

6.2. Every error body MUST have a `code`. A client MUST use `code` to tell errors apart. The `detail` text is for people and MAY change at any time.

6.3. A `400` means the client must fix the request. A client MUST NOT send the same `400` request again unchanged.

6.4. After a `429`, a client MUST wait for the `Retry-After` time when the header is there. When it is not, the client MUST back off, with a wait that grows each time.

6.5. A client SHOULD retry only `408`, `429`, `500`, `502`, `503` and `504`, with backoff. It SHOULD wait for `Retry-After` when it is there. `POST /runs` is safe to retry with the same `external_request_id`, as section 3 says.

6.6. A client MUST NOT retry `CUSTOMER_DISABLED`, `INVALID_SCOPE` or `REPO_NOT_BOUND`. A person has to fix these first.

See [Errors](/docs/api/errors).
