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

# Agent

# Agent API (`/v1`)

**Used by:** [n8n](../integrations/n8n), [Zapier](../integrations/zapier), [MCP](../integrations/mcp), and any HTTP client. Get `X-Agent-Id` and `X-Api-Key` from [Credentials](../guide/credentials).

Per-request **`timeout` overrides** in the JSON body are **REST-only**. The [MCP integration](../integrations/mcp) does not forward `timeout`; expiry always uses the agent’s dashboard defaults unless you call this API over HTTP.

Most routes require [agent authentication](./overview#agent-authentication) (`X-Agent-Id` and `X-Api-Key`). The exception is **[`GET /v1/agents`](#get-v1agents)**, which lists agents for a workspace using **`X-Api-Key` only** (for example Zapier’s agent picker).

A machine-readable description of these endpoints is available as [OpenAPI YAML](./agent-api-openapi.yaml). For plan and quota errors on create, see [Automation limits & billing](../guide/automation-limits).

***

## `GET /v1/agents`

Lists **active** agents for the team that owns the workspace API key. Used by integrations that first authenticate with the key, then let the user pick an agent.

### Request

| Aspect  | Behavior                                                  |
| ------- | --------------------------------------------------------- |
| Method  | **GET**                                                   |
| Path    | `/v1/agents`                                              |
| Headers | **`X-Api-Key`** (required). `X-Agent-Id` is **not** used. |

### Success response (`200`)

JSON array of objects:

| Field  | Type   | Notes                                                                                |
| ------ | ------ | ------------------------------------------------------------------------------------ |
| `id`   | string | Agent UUID (use as `X-Agent-Id` on other `/v1/*` calls).                             |
| `name` | string | Dashboard agent name; if empty, the API returns the agent `id` as the display value. |

Agents are sorted by **`name`** ascending (null/empty names sort with Postgres rules; clients may still show `id` when `name` was missing).

### Error responses

| HTTP | `error`              | When                           |
| ---- | -------------------- | ------------------------------ |
| 401  | `missing_api_key`    | `X-Api-Key` missing or blank   |
| 401  | `invalid_api_key`    | Unknown, revoked, or wrong key |
| 500  | `agents_list_failed` | Unexpected database error      |

***

## Approval status values

The `status` field describes the **approval lifecycle** (human choice or expiry). Values you will see on **`GET /v1/approvals/:requestId`**, **`GET /v1/approvals/:requestId/result`**, and in **callback** bodies:

| Value                | Meaning                                                                                             |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `pending`            | Open; waiting for a human or for expiry. Poll responses may include only `request_id` and `status`. |
| `selected_option`    | Resolved: the human chose one of the configured options.                                            |
| `custom_instruction` | Resolved: the human provided free-text instruction instead of an option.                            |
| `expired`            | Closed by **timeout** (no human resolution in time).                                                |

The create response’s `status` is **`pending`** for a newly created row.

***

## Result delivery fields

On **`GET /v1/approvals/:requestId`** only, the API also returns how the **final outcome** was (or was not) handed off when a **callback URL** was configured on create. This is separate from approval `status` above.

| Field                    | Values           | Meaning                                                                                                      |
| ------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------ |
| `result_delivery_status` | `pending`        | No callback, or callback not completed successfully yet, or consumer is expected to **poll** for the result. |
|                          | `delivered`      | Callback URL returned a **successful HTTP response** (2xx) after resolution.                                 |
|                          | `failed`         | Callback retries were **exhausted**; see `result_delivery_error`.                                            |
| `result_delivery_error`  | string or `null` | Short error message when delivery failed (for example HTTP status or network error).                         |

If you did **not** set `callback.url` on create, the service still tracks completion via polling; `result_delivery_status` typically stays **`pending`** from a “callback delivery” perspective even after the approval is resolved.

***

## Callback webhooks

If you include `callback` on **`POST /v1/approvals`**, the service sends the outcome to your URL when the request **leaves** `pending` (human resolution **or** timeout expiry).

### Request the service makes to your URL

| Aspect      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| HTTP method | **POST**                                                                                                                                                                                                                                                                                                                                                                                                     |
| URL         | The `callback.url` you supplied at create time                                                                                                                                                                                                                                                                                                                                                               |
| Headers     | `Content-Type: application/json`, then any custom keys from `callback.headers` (if set), then optional **agent-level** callback authentication from the dashboard (same header name + value you configure for tools such as n8n Wait **Header Auth**). If the same header name appears in both places, the **agent** value wins so clients cannot strip auth by omitting it from the create payload.         |
| Body        | **Same JSON object** as **`GET /v1/approvals/:requestId/result`** returns for a **resolved** request (not the minimal pending shape). Same fields as described under [Resolved (`200`)](#resolved-200) below.                                                                                                                                                                                                |
| Success     | Any **2xx** response is treated as success; no response body is required.                                                                                                                                                                                                                                                                                                                                    |
| Retries     | On non-2xx or network failure, the service **retries** with **exponential backoff** (defaults in the open-source server: base delay **1s**, max delay **30s**, up to **3** attempts—your operator can tune env vars).                                                                                                                                                                                        |
| Security    | Prefer **HTTPS** and optional **agent-level callback auth** in the dashboard (merged at delivery; the secret is **not** part of `POST /v1/approvals`). Use `callback.headers` only for **non-secret** extras (for example tracing IDs). There is no HMAC body signature in the reference server; see [n8n integration](../integrations/n8n.md#securing-the-wait-resume-url) for Wait-compatible header auth. |

Expiry-driven resolutions trigger the same callback path once the timeout worker marks the request expired.

***

## Rate limits

`POST /v1/approvals` is additionally limited by a **per-agent sliding window** in the API process to absorb bursts. **Default** in the reference server configuration: **60** successful create attempts per agent per **60 seconds** (your deployment may override `AGENT_CREATE_RATE_LIMIT_MAX` and `AGENT_CREATE_RATE_LIMIT_WINDOW_MS`).

When exceeded, the API returns **429** with `{ "error": "rate_limited" }`. **Backoff and retry** with jitter; do not spin in a tight loop.

***

## `POST /v1/approvals`

Creates an approval request. The body must match the API validation rules below.

### Request body

| Field                            | Type             | Required              | Notes                                                                                                                                                                                                     |
| -------------------------------- | ---------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `request_id`                     | string           | no                    | 1–120 chars when set; omit or leave blank and the server assigns a UUID (see response `request_id`). Supply your own stable id for idempotent retries.                                                    |
| `correlation_id`                 | string           | no                    | max 120                                                                                                                                                                                                   |
| `question`                       | string           | yes                   | max 500                                                                                                                                                                                                   |
| `context_markdown`               | string           | yes                   | max 2500                                                                                                                                                                                                  |
| `notification_summary`           | string           | no                    | max 120                                                                                                                                                                                                   |
| `options`                        | array            | yes                   | 1–4 items                                                                                                                                                                                                 |
| `options[].label`                | string           | yes                   | max 120                                                                                                                                                                                                   |
| `options[].value`                | string           | yes                   | max 500                                                                                                                                                                                                   |
| `timeout`                        | object           | no                    | Omit to use this agent’s [default timeout](../guide/agent-settings#default-timeout) from the dashboard. If present, each field you send overrides only that part of the defaults (partial override).      |
| `timeout.expires_in_seconds`     | number           | no\*                  | integer, 30–86400; \*required in the merged result (from body or agent default)                                                                                                                           |
| `timeout.mode`                   | string           | no\*                  | `expire_no_action` or `expire_with_predefined_instruction`; \*same as above                                                                                                                               |
| `timeout.predefined_instruction` | string \| null   | no\*                  | When non-null, max **1000** chars. After merge, required to be non-empty when mode is `expire_with_predefined_instruction`                                                                                |
| `callback`                       | object           | no                    |                                                                                                                                                                                                           |
| `callback.url`                   | string (URL)     | yes if `callback` set | Valid URL                                                                                                                                                                                                 |
| `callback.headers`               | record of string | no                    | Optional **extra** headers (non-secret metadata). Do **not** rely on this for auth secrets when agent callback auth is enabled in the dashboard—the server merges the configured auth header at delivery. |

**Omitting `timeout`:** The server merges a missing `timeout` with the agent’s saved defaults (`default_timeout_seconds`, `default_timeout_mode`, `default_timeout_instruction`). If you include `timeout`, only the properties you set replace the corresponding defaults; omitted properties keep the agent values. The merged timeout is validated (including instruction required for `expire_with_predefined_instruction`) and used for `expires_at` and idempotency hashing.

**Omitting `request_id`:** The create response always includes `request_id` (either yours or the server-generated UUID). Use that value for polling. Repeating `POST /v1/approvals` without a client `request_id` assigns a **new** id each time, so blind retries can create duplicate approvals. For safe retries, send the same `request_id` you chose on the first attempt.

### Success response (`200`)

```json theme={null}
{
  "approval_id": "<uuid>",
  "request_id": "<external id: yours or server-generated>",
  "status": "<string>",
  "expires_at": "<iso8601>",
  "idempotent": true
}
```

`idempotent` is `true` when this call matched an existing equivalent request for the same idempotency identity (only when you supplied a `request_id`; server-assigned ids are never merged across calls).

### Error responses

| HTTP | `error`                                                  | When                                                                                                                                                            |
| ---- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `invalid_payload`                                        | Validation failed; `issues` may be present                                                                                                                      |
| 401  | `missing_agent_auth`, `invalid_agent`, `invalid_api_key` | Auth failure                                                                                                                                                    |
| 403  | `USAGE_APPROVAL_LIMIT`                                   | Team is over the subscription/plan approval quota (when billing/usage limits are enabled). Body includes `code: "USAGE_APPROVAL_LIMIT"` and `over_limit: true`. |
| 409  | `idempotency_conflict`                                   | Same `request_id` for this agent but incompatible canonical payload (see body below)                                                                            |
| 429  | `rate_limited`                                           | Too many creates per agent in the sliding window                                                                                                                |
| 500  | `approval_creation_failed`                               | Unexpected failure                                                                                                                                              |

**409 `idempotency_conflict` body** (in addition to `error`):

```json theme={null}
{
  "error": "idempotency_conflict",
  "message": "<human-readable explanation; idempotency compares question, context, options, merged timeout, callback URL and headers>",
  "request_id": "<the external id you sent>",
  "existing_approval_id": "<uuid of the approval row already stored>"
}
```

Use a **new** `request_id` when you intend a separate approval; repeat the **same** body to retry safely.

***

## `GET /v1/approvals/:requestId`

Returns metadata for an approval identified by your **external** `request_id` (the path parameter is that same string). Use this for dashboards or to inspect **callback delivery** state; use **`/result`** for the full outcome payload.

### Success response (`200`)

```json theme={null}
{
  "request_id": "<string>",
  "status": "<string>",
  "expires_at": "<iso8601>",
  "result_delivery_status": "<string>",
  "result_delivery_error": "<string|null>"
}
```

`status` follows [Approval status values](#approval-status-values). `result_delivery_status` / `result_delivery_error` are described under [Result delivery fields](#result-delivery-fields).

### Error responses

| HTTP | `error`      | When                                     |
| ---- | ------------ | ---------------------------------------- |
| 401  | (agent auth) | Invalid credentials                      |
| 404  | `not_found`  | No approval for this agent + external id |

***

## `GET /v1/approvals/:requestId/result`

Polls the outcome. The path parameter `:requestId` is your **external** id (`request_id` from creation).

### Pending (`200`)

```json theme={null}
{
  "request_id": "<string>",
  "status": "pending"
}
```

### Resolved (`200`)

Includes fields such as:

* `request_id`, `agent_id`, `correlation_id`
* `status` — one of `selected_option`, `custom_instruction`, or `expired` (see [Approval status values](#approval-status-values))
* `selected_option_index`, `selected_option_label` (when applicable)
* `custom_instruction`
* `resolved_by` (`user_id`, `name`), `resolved_at`

This is the same JSON shape **POSTed to your `callback.url`**, if configured.

### Error responses

| HTTP | `error`      | When                                          |
| ---- | ------------ | --------------------------------------------- |
| 401  | (agent auth) | Invalid credentials                           |
| 404  | `not_found`  | Unknown approval for this agent + external id |
