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/v1is the Agent Approval Protocol: what an adapter calls to hold a tool call until a person decides./api/v1is 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 or download its 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.
export WITHHUMAN_URL=https://app.withhuman.aiAuthentication
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 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, 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
explains how policies combine actions and resource scopes.
Errors
Every error response uses the same envelope:
{
"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-IDheader 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/requestsPOST /api/v1/requests/{id}/decision
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
409with typeconflictand 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:
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
412with codeactive_revision_changedand changes nothing. The caller reloads and decides again with current information. - The header is missing: the provider answers
428with codeif_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:
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.
waitis 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.