Documentation
DocsAPI reference

API overview

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

Updated Sep 19, 2026

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 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.

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

Authentication

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

CredentialWho holds itSent asOpens
Agent credentialThe adapter running beside one copy of an agentAuthorization: Bearer whc_...The Agent Approval Protocol: create an approval request for a tool call, then wait for a person's decision on it
Provisioner tokenWhatever starts agent replicas: a fleet controller or a hosted agent platformAuthorization: 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 keyA script acting as a memberAuthorization: Bearer whk_...Supported product operations using the member's current permissions, optionally narrowed to selected actions
Organization API keyA shared integrationAuthorization: Bearer who_...Supported product operations using independent permission policies; cannot claim or decide requests or manage human identities
Session cookieA signed-in member of the organization, through the web appCookie: 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:

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 }
  }
}
FieldWhat it is
typeA coarse, stable category, one of the values in the table below.
codeThe specific reason, also stable. Safe to match on when type is not enough.
messagePlain text written for a person. Its wording can change.
request_idThe same value as the X-Request-ID response header, which support uses to find the request in the logs.
detailsOptional, structured, and only on documented codes. quota_exceeded carries the dimension, the limit, and what is used.

The type values:

typeStatusMeaning
invalid_request400, 428The body or parameters failed validation, or a required If-Match header is missing.
authentication401The credential is missing, invalid, expired, or revoked.
authorization403The 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_found404No such resource in this organization.
conflict409, 412The request contradicts current state: a reused idempotency key, a stale If-Match, or a plan quota.
rate_limit429A limit on pending requests or active agents was reached.
dependency501, 504A feature this instance has not configured, or a downstream system that did not answer in time.
internal500Something 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.