> ## Documentation Index
> Fetch the complete documentation index at: https://docs.antigen.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP API reference

> Create, observe, recover, and control authorized Antigen Runs over HTTP.

The HTTP API is the direct integration surface for applications that manage their own credentials,
persistence, and review workflow. Its origin is `https://api.antigen.sh`.

Authenticate every request with an organization API key in `x-api-key`. Keys begin with `atg_sk_`.
Send the key only in that header: never in a URL, a browser history, a source file, or a log. Use
`content-type: application/json` for Run creation and guidance.

A local caller deadline bounds only that caller. It does not create, stop, resume, or cancel a Run.

## Conventions

Every response carries `contractVersion`, currently `"v1"`. Successful reads return the resource
directly; errors return an `error` object described under [Errors](#errors).

Each endpoint requires a scope on the credential. See [Authentication](/authentication) for how
credentials are issued and how authorization errors are resolved.

| Scope           | Endpoints                                                             |
| --------------- | --------------------------------------------------------------------- |
| `targets:read`  | `GET /v1/targets`                                                     |
| `runs:read`     | `GET /v1/runs`, `GET /v1/runs/{runId}`, `GET /v1/runs/{runId}/events` |
| `runs:write`    | `POST /v1/runs`, guidance, and all lifecycle actions                  |
| `findings:read` | `GET /v1/runs/{runId}/findings`                                       |

Two endpoints accept query parameters: `GET /v1/runs` takes `cursor`, `limit`, and `status`, and
`GET /v1/runs/{runId}/events` takes `after`. Every other endpoint rejects a query string with `400`
and `INVALID_REQUEST`, and so does an unrecognized parameter on those two. The message does not name
the offending parameter. A filter that is silently ignored is worse than a rejected request, because
a dropped `status` returns Runs the caller did not ask for while appearing to have succeeded.

## Discovery and creation

### List approved targets

`GET /v1/targets`

Lists targets approved for the caller's organization.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail-with-body --silent --show-error \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  https://api.antigen.sh/v1/targets | jq .
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "contractVersion": "v1",
  "targets": ["example.com", "api.example.com"]
}
```

Select a target from this response for [`POST /v1/runs`](#create-a-run). If creation returns
`TARGET_NOT_APPROVED`, re-read this endpoint rather than editing the rejected target.

### Create a Run

`POST /v1/runs`

Creates one authorized Run for a coherent assessment objective.

| Field        | Type                  | Description                                                                    |
| ------------ | --------------------- | ------------------------------------------------------------------------------ |
| `model`      | `string`              | Required. The assessment model.                                                |
| `targets`    | `string[]`            | Required, non-empty. Each value from `GET /v1/targets`.                        |
| `guardrails` | `string`              | Natural-language scope guidance sent with the Run.                             |
| `skills`     | `{ name, content }[]` | Inline Skills. Both fields are non-empty strings; unknown fields are rejected. |

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
REQUEST=$(jq -nc --arg target "$ANTIGEN_TARGET" \
  '{model:"claude-sonnet-4-6",targets:[$target],guardrails:"Assess only the approved target."}')
curl --fail-with-body --silent --show-error \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  -H 'content-type: application/json' \
  -d "$REQUEST" \
  https://api.antigen.sh/v1/runs | jq .
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "contractVersion": "v1",
  "id": "run_01J8Z3M6K7Q2X4",
  "model": "claude-sonnet-4-6",
  "targets": ["example.com"],
  "status": "creating",
  "createdAt": "2026-07-31T09:14:22.104Z",
  "updatedAt": "2026-07-31T09:14:22.104Z"
}
```

Persist `id` immediately with the assessment record. If the response is ambiguous, read that saved
ID with [`GET /v1/runs/{runId}`](#read-a-run) before considering another creation request. A
second `POST /v1/runs` is a new assessment record, not a retry.

## List and read Runs

### List Runs

`GET /v1/runs`

Reads an organization-visible page of Runs.

| Query parameter | Description                                                                                          |
| --------------- | ---------------------------------------------------------------------------------------------------- |
| `limit`         | Page size, 1 through 100. Defaults to 50.                                                            |
| `cursor`        | An opaque continuation value from a previous page.                                                   |
| `status`        | One of `creating`, `running`, `stopping`, `stopped`, `resuming`, `completed`, `failed`, `cancelled`. |

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail-with-body --silent --show-error \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  'https://api.antigen.sh/v1/runs?limit=50&status=running' | jq .
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "contractVersion": "v1",
  "runs": [
    {
      "id": "run_01J8Z3M6K7Q2X4",
      "contractVersion": "v1",
      "model": "claude-sonnet-4-6",
      "targets": ["example.com"],
      "status": "running",
      "createdAt": "2026-07-31T09:14:22.104Z",
      "updatedAt": "2026-07-31T09:18:03.887Z",
      "startedAt": "2026-07-31T09:14:44.010Z"
    }
  ],
  "nextCursor": "eyJhZnRlciI6InJ1bl8wMUo4WjNNNks3UTJYNCJ9"
}
```

`nextCursor` is present only when another page exists. Pass it back unchanged. Do not derive
authorization, identity, or ordering from a cursor or a Run ID.

### Read a Run

`GET /v1/runs/{runId}`

Reads one known Run by its saved opaque ID.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail-with-body --silent --show-error \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  "https://api.antigen.sh/v1/runs/$RUN_ID" | jq .
```

The response fields depend on how far the Run has progressed. `startedAt` appears once it is
running, `stoppedAt` when it is stopped, and `completedAt` on a terminal status. A `failed` Run also
carries `failure` with a `code` of `EXECUTION_UNAVAILABLE` or `RUN_EXECUTION_FAILED` and a message.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "contractVersion": "v1",
  "id": "run_01J8Z3M6K7Q2X4",
  "model": "claude-sonnet-4-6",
  "targets": ["example.com"],
  "status": "completed",
  "createdAt": "2026-07-31T09:14:22.104Z",
  "updatedAt": "2026-07-31T09:41:52.660Z",
  "startedAt": "2026-07-31T09:14:44.010Z",
  "completedAt": "2026-07-31T09:41:52.660Z"
}
```

An ID alone grants nothing. A Run belonging to another organization answers `RUN_NOT_FOUND`, and a
credential without `runs:read` answers `FORBIDDEN`. Read this endpoint after any ambiguous create or
lifecycle request, then resume observation with
[`GET /v1/runs/{runId}/events`](#stream-run-events) after your last handled checkpoint.

## Events and findings

### Stream Run events

`GET /v1/runs/{runId}/events`

Opens an ordered server-sent event stream for a Run. The response uses
`content-type: text/event-stream` and stays open until the Run reaches a terminal state or the
caller disconnects.

Resume with **either** a numeric `after` query parameter **or** an opaque `Last-Event-ID` header.
They are alternatives, and sending both is rejected with `400` and `INVALID_REQUEST`.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --no-buffer --fail-with-body --silent --show-error \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  "https://api.antigen.sh/v1/runs/$RUN_ID/events?after=$AFTER"
```

Each frame carries the opaque event ID, the event type, and a JSON envelope:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
id: evt_01J8Z3P2R9
event: finding
data: {"contractVersion":"v1","event":{"id":"evt_01J8Z3P2R9","runId":"run_01J8Z3M6K7Q2X4","sequence":42,"type":"finding","agent":"executor","summary":"Reflected input in search parameter","timestamp":"2026-07-31T09:31:10.552Z","findingId":"fnd_01J8Z3P2RA","severity":"high","title":"Reflected cross-site scripting"}}
```

| `event` type                   | Additional `data.event` fields   | Meaning                                                  |
| ------------------------------ | -------------------------------- | -------------------------------------------------------- |
| `thinking`, `tool`, `progress` | n/a                              | Ordered progress narration.                              |
| `finding`                      | `findingId`, `severity`, `title` | A finding was recorded.                                  |
| `lifecycle`                    | `from`, `to`                     | The Run changed status. A terminal `to` ends the stream. |
| `error`                        | `error`                          | Execution failed, with `{ code, message }`.              |

Persist `sequence` only after handling that event, and resume with `after=<sequence>`. When you
store the opaque `id` instead, send it as `Last-Event-ID` and omit `after`; the two cursors are not
interchangeable. An invalid cursor is rejected with `INVALID_REQUEST`.

### Read Run findings

`GET /v1/runs/{runId}/findings`

Reads durable findings and remediation guidance for one Run.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail-with-body --silent --show-error \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  "https://api.antigen.sh/v1/runs/$RUN_ID/findings" | jq .
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "contractVersion": "v1",
  "runId": "run_01J8Z3M6K7Q2X4",
  "findings": [
    {
      "id": "fnd_01J8Z3P2RA",
      "runId": "run_01J8Z3M6K7Q2X4",
      "severity": "high",
      "title": "Reflected cross-site scripting",
      "description": "The search parameter is reflected without contextual encoding.",
      "remediation": "Encode the parameter for its HTML output context before rendering.",
      "proof": { "request": "GET /search?q=…", "response": "HTTP/1.1 200 OK…" },
      "endpoint": "/search",
      "cwe": "CWE-79",
      "createdAt": "2026-07-31T09:31:10.552Z"
    }
  ]
}
```

`severity` is one of `critical`, `high`, `medium`, `low`, or `info`. `proof`, `endpoint`, and `cwe`
appear only when available. An empty `findings` array is a valid outcome for a completed Run, not an
integration failure.

## Guidance and lifecycle

### Send guidance

`POST /v1/runs/{runId}/messages`

Delivers scoped guidance to an existing Run. The JSON body requires a `content` string; keep the
guidance within the Run's approved target and Guardrails.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
REQUEST=$(jq -nc --arg content 'Prioritize the approved target and preserve evidence.' \
  '{content:$content}')
curl --fail-with-body --silent --show-error \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  -H 'content-type: application/json' \
  -d "$REQUEST" \
  "https://api.antigen.sh/v1/runs/$RUN_ID/messages" | jq .
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "contractVersion": "v1",
  "id": "msg_01J8Z3T4V1",
  "runId": "run_01J8Z3M6K7Q2X4",
  "deliveredAt": "2026-07-31T09:35:02.318Z"
}
```

For an ambiguous response, read the Run before sending another message. Work outside the approved
scope needs its own authorized assessment.

### Lifecycle actions

`stop`, `resume`, and `cancel` all return the Run in its requested new state.

| Action                         | Request            | Result                             |
| ------------------------------ | ------------------ | ---------------------------------- |
| `POST /v1/runs/{runId}/stop`   | `{}` body required | Non-terminal, resumable `stopped`. |
| `POST /v1/runs/{runId}/resume` | `{}` body required | Continues a `stopped` Run.         |
| `POST /v1/runs/{runId}/cancel` | No body            | Terminal `cancelled`.              |

`stop` and `resume` parse a JSON body and reject a malformed one with `INVALID_REQUEST`, so send
`{}` and `content-type: application/json`. `cancel` reads no body at all. `DELETE /v1/runs/{runId}`
is an exact alias for cancel, for clients that prefer the verb.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --fail-with-body --silent --show-error \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  -H 'content-type: application/json' \
  -d '{}' \
  "https://api.antigen.sh/v1/runs/$RUN_ID/stop" | jq .

curl --fail-with-body --silent --show-error -X POST \
  -H "x-api-key: $ANTIGEN_API_KEY" \
  "https://api.antigen.sh/v1/runs/$RUN_ID/cancel" | jq .
```

`INVALID_RUN_STATE` means the action does not apply to the Run's observed status, usually because
another transition already occurred. Read the Run and choose a valid action rather than repeating
the request. Resuming does not re-evaluate target approval, so confirm the scope still applies
first. A local connection failure does not prove the action was rejected.

## Errors

Error responses carry `contractVersion` and an `error` object with a stable `code`, a message, and
an optional `requestId`.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "contractVersion": "v1",
  "error": {
    "code": "TARGET_NOT_APPROVED",
    "message": "One or more targets are not approved for this organization",
    "requestId": "req_01J8Z3M6K7",
    "details": { "targets": ["staging.example.com"] }
  }
}
```

| Code                    | HTTP  | Meaning                                                                                           | What to do                                                    |
| ----------------------- | ----- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `INVALID_REQUEST`       | `400` | The body, a path value, or a query parameter the endpoint does not accept.                        | Correct the request. Retrying it fails again.                 |
| `TARGET_NOT_APPROVED`   | `400` | A submitted target is not approved. `details.targets` lists the rejected values.                  | Have the target approved. Do not edit the value.              |
| `UNAUTHENTICATED`       | `401` | No recognizable credential was presented.                                                         | Supply a credential.                                          |
| `FORBIDDEN`             | `403` | The credential is valid but lacks the required scope, or was used on a surface it does not cover. | Use a credential carrying the scope. Retrying it fails again. |
| `RUN_NOT_FOUND`         | `404` | No Run with that ID exists within what the caller can see.                                        | See below before treating this as a missing Run.              |
| `NOT_FOUND`             | `404` | The request path matches no endpoint on this API.                                                 | Correct the path. This is not a statement about any Run.      |
| `INVALID_RUN_STATE`     | `409` | The Run's current status does not accept that action.                                             | Read the Run back and choose a valid action.                  |
| `EXECUTION_UNAVAILABLE` | `503` | Execution capacity is temporarily unavailable.                                                    | Retry with backoff.                                           |
| `INTERNAL`              | `500` | An unexpected service failure.                                                                    | Do not retry automatically. Retain the `requestId`.           |

The status code carries information the `code` does not, and the reverse is also true, so read both.
The line between the two authorization codes is whether the credential is allowed to do this at all,
which is `FORBIDDEN`, or whether the request itself is written incorrectly, which is
`INVALID_REQUEST`.

`RUN_NOT_FOUND` deliberately covers more than a missing Run. It is also the response when the Run
exists but belongs to another organization. Those two cases are not distinguishable from outside, by
design: a caller holding a Run ID cannot learn whether that ID is real. Do not use this endpoint to
test whether an ID exists, and do not report a cross-organization Run to a user as deleted. A
mistyped path is a separate code, `NOT_FOUND`, so a 404 about routing is never confused with a 404
about a Run.

Request bodies are validated strictly. A field the endpoint does not define is rejected with
`INVALID_REQUEST` rather than ignored, so a misspelled key fails loudly instead of silently dropping
the value you meant to send.

This set may grow. Handle a `code` you do not recognize by its HTTP status rather than by an
exhaustive match, so that a new code does not turn into an unhandled failure in your client.

Retain `requestId` with the saved Run ID whenever one is present. It lets support identify the
failing request without exposing credentials.

### Retrying a failed request

Retry on `408`, `429`, and any `5xx`. Do not retry `400`, `401`, `403`, `404`, or `409`: the request
is wrong or no longer applicable, and repeating it produces the same response. `503` with
`EXECUTION_UNAVAILABLE` means execution capacity is momentarily exhausted: back off and retry.

The TypeScript SDK reconnects event streams with exponential backoff from a 250ms base, capped at 5s,
scaled by random jitter between 0.5 and 1.5, for at most 5 consecutive failures. Those values are a
reasonable starting point for a hand-written client.

`POST /v1/runs` accepts no idempotency key. Retrying a create that failed with `503` can produce two
Runs, both of which will execute and both of which are billable. When a create is ambiguous, list
Runs and look for one matching your targets before sending it again.

After an ambiguous create, lifecycle, or observation request, do not infer the remote result from a
local failure. Preserve the Run ID and your event checkpoint, read the Run, resume events after that
checkpoint, and read findings for durable remediation work.

Related concepts: [Get started](/getting-started), [TypeScript SDK](/sdk/typescript),
[Guides](/guides), and [Reliability and security](/reliability-security).
