# API overview

How the API is organized, how to authenticate, what an error looks like, and the rules every endpoint follows.

The withHuman API is organized around **REST**. It has predictable,
resource-oriented URLs, accepts JSON request bodies, returns JSON responses,
and uses standard HTTP response codes, verbs, and authentication.

The API has two halves:

- **`/api/aap/v1`** is the Agent Approval Protocol: what an adapter calls to
  hold a tool call until a person decides.
- **`/api/v1`** is the product: the review queue, agents, pipelines,
  escalation paths, and organization settings.

Your credential decides which half you can call. A few helper routes under
`/auth`, such as browser-assisted agent enrollment, sit outside both.

Read the [OpenAPI schema](/docs/reference/openapi) or [download its YAML](/openapi.yaml)
to generate a client for the published API. Each instance also serves its own
OpenAPI document at `/api/openapi.yaml`.

## Base URL

Every path in this reference is relative to your instance, and there are two
kinds of instance.

- **Hosted:** `https://app.withhuman.ai`. This is the version withHuman runs
  for you. Sign up, and your organization lives here.
- **Self-hosted:** the public origin you configured when you deployed the
  provider yourself, for example `https://withhuman.example.com`.

This reference covers the API both editions share. Endpoints that exist only
in the hosted edition are marked as such. The examples use `$WITHHUMAN_URL`
for this value.

```bash
export WITHHUMAN_URL=https://app.withhuman.ai
```

## Authentication

The credential you use determines which part of the API you can access.

| Credential | Who holds it | Sent as | Opens |
| --- | --- | --- | --- |
| **Agent credential** | The adapter running beside one copy of an agent | `Authorization: Bearer whc_...` | The Agent Approval Protocol: create an approval request for a tool call, then wait for a person's decision on it |
| **Provisioner token** | Whatever starts agent replicas: a fleet controller or a hosted agent platform | `Authorization: Bearer whp_...` | Only `POST /api/aap/v1/instances`, which creates a new instance of the token's agent and returns that instance's agent credential |
| **Personal API key** | A script acting as a member | `Authorization: Bearer whk_...` | Supported product operations using the member's current permissions, optionally narrowed to selected actions |
| **Organization API key** | A shared integration | `Authorization: Bearer who_...` | Supported product operations using independent permission policies; cannot claim or decide requests or manage human identities |
| **Session cookie** | A signed-in member of the organization, through the web app | `Cookie: withhuman_session=...` | Everything under `/api/v1`: the review queue and decisions, agents and their credentials, pipelines, escalation paths, members, teams, and webhook endpoints |

### When to use which

**Agent credential: one running copy of an agent.** This is what an adapter
holds. It is bound to one agent instance, so the server knows exactly which
agent, on which machine, is asking; a request body cannot claim a different
identity. It can create approval requests and wait for decisions, and nothing
else. It can never approve or deny.

You get one by running `withhuman agent install <runtime>` on a developer machine, which walks
you through connecting the agent in the browser, or by minting an instance
with a provisioner token.

**Provisioner token: a fleet that starts its own copies.** Use this when an
agent runs as many replicas that come and go, such as autoscaled pods or a
hosted agent platform. A person issues one provisioner token for the agent in
the product and puts it in the deployment. Each replica that starts calls the
mint endpoint once, receives its own agent credential, and appears in the
queue as its own instance. Nobody pre-creates instances, and no credential is
shared between replicas. The token can do nothing except mint.

**API keys: scripts and shared integrations.** A personal key follows the
member's current access. An organization key has its own scoped permission
policies and continues to work independently of the person who created it.
See [API keys](/docs/web-app/api-keys) for choosing a type, verifying access
and replacing a key.

**Session cookie: a signed-in person in the web app.** Signing in creates a
session. Browser-only operations, including creating and revoking API keys,
require it. API keys cannot manage other keys.

### Things to know

- **Credentials are shown once**, when issued. Only a hash is stored.
- **Every call resolves the credential live**, so revoking an instance or
  disabling an agent takes effect immediately.
- **Credentials expire.** A minted instance credential has a lifetime the
  provider caps, and a session ends when the member signs out or it expires.

## Authorization

Every endpoint in this reference lists the permission it requires, next to
its authentication method, and links to the
[permission vocabulary](/docs/reference/permissions), which describes each
key, who may hold it, and the other endpoints that check it. A valid
credential without that permission is answered with a `403` of type
`authorization`. Endpoints marked self-service act only on the caller's own
account and need no permission. [Permissions](/docs/web-app/permissions)
explains how policies combine actions and resource scopes.

## Errors

Every error response uses the same envelope:

```json
{
  "error": {
    "type": "conflict",
    "code": "quota_exceeded",
    "message": "The organization is at its active agent limit.",
    "request_id": "6b1f0d2e-3c4a-4f5b-9e8d-7a6b5c4d3e2f",
    "details": { "dimension": "active_agents", "limit": 5, "used": 5 }
  }
}
```

| Field | What it is |
| --- | --- |
| `type` | A coarse, stable category, one of the values in the table below. |
| `code` | The specific reason, also stable. Safe to match on when `type` is not enough. |
| `message` | Plain text written for a person. Its wording can change. |
| `request_id` | The same value as the `X-Request-ID` response header, which support uses to find the request in the logs. |
| `details` | Optional, structured, and only on documented codes. `quota_exceeded` carries the dimension, the limit, and what is used. |

The `type` values:

| `type` | Status | Meaning |
| --- | --- | --- |
| `invalid_request` | 400, 428 | The body or parameters failed validation, or a required `If-Match` header is missing. |
| `authentication` | 401 | The credential is missing, invalid, expired, or revoked. |
| `authorization` | 403 | The credential is valid but may not do this. Includes a disabled agent, an inactive membership, and a reviewer the request was not routed to. |
| `not_found` | 404 | No such resource in this organization. |
| `conflict` | 409, 412 | The request contradicts current state: a reused idempotency key, a stale `If-Match`, or a plan quota. |
| `rate_limit` | 429 | A limit on pending requests or active agents was reached. |
| `dependency` | 501, 504 | A feature this instance has not configured, or a downstream system that did not answer in time. |
| `internal` | 500 | Something failed on the provider. The `request_id` identifies it. |

### Request ids

Every response, success or failure, carries an `X-Request-ID` header. It
labels that one HTTP call: the provider writes the same value into its own
logs, and an error body repeats it as `request_id`. With that value, support
can find the exact call that went wrong.

The value comes from one of two places:

- **You send one.** Add an `X-Request-ID` header to the request and the
  provider adopts it: it logs under your value and returns it unchanged in
  the response. This is useful when you want one id to follow a call through
  your logs and the provider's.
- **You send nothing.** The provider generates a UUID, logs under it, and
  returns it in the response header, where you can read it after the fact.

## Idempotency

The API supports idempotency for safely retrying a request without performing
the same operation twice. Two commands require an `Idempotency-Key` header
and reject a request without one as `400`:

- `POST /api/aap/v1/requests`
- `POST /api/v1/requests/{id}/decision`

```bash
curl -X POST "$WITHHUMAN_URL/api/aap/v1/requests" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Idempotency-Key: 4f1c9a2e-7b3d-4e8f-a1c5-2d6b8e0f9a31" \
  -H "Content-Type: application/json" \
  -d '{ "tool": "Bash", "arguments": { "command": "rm -rf build" }, "timeout": "30m" }'
```

The client generates the key. Any string works; a V4 UUID is a good choice.
AAP request keys are scoped to the authenticated instance and the create
operation. Another instance can use the same key independently and cannot
read or cancel the first instance's request. Product decision keys remain
scoped to the organization and operation. A create key and decision key are
unrelated.

The provider records the first request made with a key and compares every
later request with that key against it.

- **Same key, same parameters:** the provider returns the current state of
  the original outcome instead of acting again. For a create, that is the
  approval request, including its decision once there is one. For a
  decision, that is the recorded decision operation.
- **Same key, different parameters:** the provider answers `409` with type
  `conflict` and does nothing.

For a create, the compared parameters are the authenticated identity, `tool`,
`arguments`, `agent_reasoning`, `context`, and the exact submitted `timeout`
string. For
a decision, they are `status`, `note`, and `channel`.

Keys are kept indefinitely. `GET` and `DELETE` requests do not take a key;
they are idempotent by definition.

## Concurrency control

Pipelines and escalation paths are stored as numbered, immutable revisions,
and one revision at a time is active. Creating a revision never changes which
one is active; the activate call does, and it requires an `If-Match` header.

The header carries the revision number you believe is active right now, in
quotation marks. Here revision 5 is being activated, and the caller last saw
revision 4 as the active one:

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/escalation-paths/oncall/revisions/5/activate" \
  -H "Cookie: withhuman_session=$SESSION" \
  -H 'If-Match: "4"'
```

The provider compares the header with the revision that is actually active:

- **They match:** revision 5 becomes active.
- **They differ,** because someone activated another revision since the
  caller looked: the provider answers `412` with code
  `active_revision_changed` and changes nothing. The caller reloads and
  decides again with current information.
- **The header is missing:** the provider answers `428` with code
  `if_match_required`.

Use `"0"` when nothing is active yet. Rolling back is activating an older
revision, with the same header.

## Long polling

After creating an approval request, an adapter finds out the decision by
reading the request back with `GET /api/aap/v1/requests/{id}`. The request
is `pending` until it becomes `approved`, `denied`, `expired`, or
`cancelled`.

Rather than polling in a tight loop, pass a `wait` duration:

```bash
curl "$WITHHUMAN_URL/api/aap/v1/requests/$REQUEST_ID?wait=30s" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN"
```

The provider holds the connection open and responds as soon as the status
becomes final, or when the wait runs out, whichever comes first. Either way
the body is the current request. If `status` is still `pending`, call again
with a fresh `wait`.

- `wait` is capped at **30 seconds**; longer values are trimmed. A value the
  server cannot parse means no wait.
- A request that is already final returns immediately.
- A dropped connection says nothing about the decision. Call again.
