HumanLayer API v1
Plug qualified human decisions into your AI agents through a REST API. Your agent automates by default; when a request crosses the Arbitrium threshold, it submits it to HumanLayer, which routes it to an authorized Sentinel and returns a justified, timestamped and signed decision.
Base URL: https://humanlayer.systems/api/v1
Authentication
Every request includes your API key in the x-api-key header.
x-api-key: hl_live_…
- Create and revoke keys in the client portal (up to 5 active keys).
- The full key is shown only once: HumanLayer stores only its SHA-256 fingerprint.
- Each key comes with a webhook signing secret (
whsec_…), also shown at creation.
X-HL-Lang: en to receive English messages. The code field is always stable.
Levels, SLAs and pricing
The request_type field sets the decision level, and therefore the SLA and the price. Your plan sets which levels you can use and your daily quota.
| request_type | Level | SLA | Price |
|---|---|---|---|
compliance (alias conformité) | 1 · Compliance | 30 min | €20 / decision |
judgment (alias jugement) | 2 · Contextual judgment | 2 h | €100 / decision |
| signature | 3 · Decision authority | 4 h, or custom from 30 min (Enterprise) | €250 / decision |
| Plan | Levels | Quota |
|---|---|---|
| Compliance | 1 | 100 requests / day |
| Expert judgment | 1, 2 | 50 requests / day |
| Authorized authority | 1, 2, 3 | 20 requests / day |
| Enterprise | 1, 2, 3 | By contract (premium SLAs, dedicated pools) |
The clock starts at submission. You may ask for a longer delay with sla_minutes, never shorter than the level’s commitment (7 days at most).
Request lifecycle
- The agent submits the request:
POST /api/v1/decisions. - HumanLayer routes it to an active Sentinel in the right domain (and jurisdiction) with two-factor authentication configured, least busy first.
- The Sentinel decides (
approvedorrejected, justification required) or escalates for a second opinion: the request then moves to another Sentinel. - The decision is signed (Ed25519) and delivered by webhook; it remains available through
GET /api/v1/decisions?id=…. - If the SLA is exceeded, you receive
sla.breachedand the request is reassigned automatically (up to three escalations). The check runs every 5 minutes. - Once the request is decided, you can ask for a second opinion: another Sentinel takes the case without knowing the first verdict.
Submit a request
Creates a decision request and assigns it to a qualified Sentinel as soon as one is available.
Request body (JSON, 64 KB max)
| Parameter | Type | Required | Description |
|---|---|---|---|
| agent_id | string | required | Your agent identifier (120 characters max) |
| agent_name | string | optional | Human-readable agent name |
| request_type | string | required | compliance | judgment | signature (see levels) |
| domain | string | required | legal, finance, compliance, medical, accounting, hr, insurance, quality, security, other |
| jurisdiction | string | optional | Required jurisdiction (e.g. “Quebec”, “France”) |
| summary | string | required | The question put to the Sentinel (5,000 characters max) |
| context_json | object | optional | Structured case prepared by the AI (50 KB max) |
| priority | string | optional | low | normal (default) | high | urgent |
| sla_minutes | integer | optional | A longer delay than the level’s commitment (10,080 max) |
| callback_url | string | optional | Webhook URL: HTTPS required, public host (private and internal addresses are rejected) |
Response (200)
{
"ok": true,
"request_id": "8b1f2c4e-…",
"status": "assigned",
"level": 1,
"sla_minutes": 30,
"expires_at": "2026-03-12T15:02:07.000Z",
"sentinel_ref": "S-4F2A9C"
}
pending; routing is retried every 5 minutes. The Sentinel’s identity is never exposed: sentinel_ref is a stable pseudonym.
Track a request
Returns the request state, its SLA and, once decided, the signed decision. Prefer webhooks to polling; if you poll, wait at least 15 seconds between calls.
Response (200): decided request
{
"request": {
"id": "8b1f2c4e-…",
"status": "decided",
"level": 1,
"priority": "normal",
"sentinel_ref": "S-4F2A9C",
"expires_at": "2026-03-12T15:02:07.000Z",
"escalation_count": 0,
"verdict": "approved",
"reasoning": "Compliant with the client’s internal policy.",
"decision": {
"id": "d41c…",
"verdict": "approved",
"signed_at": "2026-03-12T14:48:31.000Z",
"signature": "base64…",
"signed_payload": "{\"decision_id\":…}",
"key_id": "3f9a…"
}
}
}
Agent history
The last 100 requests from this agent created with the same key, newest first, in the format above.
Request a second opinion
Sends an already decided request to another Sentinel, who never handled the case and does not see the first verdict. The case (level, domain, jurisdiction, question, context) is reused as is. A second opinion is billed as a new decision at the same level and counts toward your daily quota.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| second_opinion_of | string | required | request_id of the decided request, created with the same key |
| priority | string | optional | Defaults to the original request’s |
| callback_url | string | optional | Defaults to the original request’s |
| sla_minutes | integer | optional | Same as for a regular request |
Response (200)
{
"ok": true,
"request_id": "c7d05e91-…",
"status": "assigned",
"level": 2,
"sla_minutes": 120,
"sentinel_ref": "S-9B31D0",
"second_opinion_of": "8b1f2c4e-…"
}
409 not_decided) and only once (409 second_opinion_exists). The original request then exposes second_opinion_id, and the second opinion second_opinion_of. Each verdict is signed separately; if they differ, what happens next is your call.
Verification key
Ed25519 public key (PEM) and its key_id, to verify decisions without trusting the transport. No authentication required.
Signed webhooks
If you provide callback_url, HumanLayer sends a JSON POST for each event:
| Event | When |
|---|---|
| decision.completed | The Sentinel approved |
| decision.rejected | The Sentinel rejected |
| decision.escalated | Second opinion requested: the request moves to another Sentinel |
| sla.breached | The deadline was missed; automatic reassignment |
Headers: X-HumanLayer-Event, X-HumanLayer-Delivery and X-HumanLayer-Signature: t=<timestamp>,v1=<hmac>, where v1 is the hex HMAC-SHA256 of t + "." + raw body with your key’s whsec_… secret. Each event is delivered once (5 s timeout); if delivery fails, the state stays available through the API (webhook_status).
import hmac, hashlib, time def verify_webhook(headers, raw_body: bytes, secret: str) -> bool: parts = dict(p.split("=", 1) for p in headers["X-HumanLayer-Signature"].split(",")) if abs(time.time() - int(parts["t"])) > 300: return False # too old: possible replay signed = parts["t"].encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"])
Verify a decision
Every decision is signed with Ed25519 over the signed_payload field (canonical JSON: identifiers, verdict, SHA-256 fingerprint of the justification, Sentinel pseudonym, timestamp, key_id). Any later change invalidates the signature.
import crypto from 'node:crypto'; const key = await (await fetch('https://humanlayer.systems/api/v1/decision-key')).json(); const ok = crypto.verify( null, Buffer.from(decision.signed_payload), key.public_key_pem, Buffer.from(decision.signature, 'base64') ); // Also check that sha256(reasoning) === JSON.parse(signed_payload).reasoning_sha256
Statuses
| status | Meaning |
|---|---|
| pending | Waiting for an available Sentinel |
| assigned | Assigned to a Sentinel, on time |
| sla_breached | Deadline missed and no other Sentinel available yet |
| decided | Decided: verdict is approved or rejected |
An escalation (escalated) is not final: the request returns to assigned with another Sentinel, and escalation_count increases.
Errors and limits
Errors return { "error": "message", "code": "…" }.
| HTTP | code | Cause |
|---|---|---|
| 400 | missing_fields, invalid_request_type, invalid_domain, invalid_priority, invalid_callback_url, context_too_large, invalid_second_opinion | Invalid request |
| 401 | missing_api_key, invalid_api_key | Missing or unknown key |
| 403 | api_key_inactive, api_key_expired, plan_restriction | Suspended, revoked or expired key, or level outside your plan |
| 404 | not_found | Unknown request, or created with another key |
| 409 | not_decided, second_opinion_exists | Second opinion on a request that is not decided yet, or already requested |
| 413 | payload_too_large | Body larger than 64 KB |
| 429 | rate_limited, daily_limit_reached | More than 60 requests per minute per key, or plan quota reached. Honor the Retry-After header. |
| 500 | server_error | Internal error: retry with increasing delays |
Full example
import time, requests API = "https://humanlayer.systems/api/v1/decisions" HEADERS = {"x-api-key": "hl_live_…"} # 1. Arbitrium threshold crossed: submit the case created = requests.post(API, headers=HEADERS, json={ "agent_id": "agent-credit", "request_type": "compliance", "domain": "finance", "summary": "KYC: partial match on a sanctions list. Validate?", "context_json": {"customer_id": "c_4521", "match_score": 0.71}, "callback_url": "https://your-app.example/webhooks/humanlayer", }).json() # 2. Without a webhook: poll, spaced out, until decided while True: req = requests.get(API, headers=HEADERS, params={"id": created["request_id"]}).json()["request"] if req["status"] == "decided": break time.sleep(15) # 3. The agent resumes its workflow, backed by a signed decision if req["verdict"] == "approved": continue_workflow(req["decision"])
curl -X POST https://humanlayer.systems/api/v1/decisions \ -H "x-api-key: hl_live_…" \ -H "content-type: application/json" \ -d '{"agent_id":"agent-legal","request_type":"judgment","domain":"legal","summary":"Is clause §4.2.b acceptable?"}'