← Back to humanlayer.systems
FR | EN

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.

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.
💡 Error message language Send 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_typeLevelSLAPrice
compliance (alias conformité)1 · Compliance30 min€20 / decision
judgment (alias jugement)2 · Contextual judgment2 h€100 / decision
signature3 · Decision authority4 h, or custom from 30 min (Enterprise)€250 / decision
PlanLevelsQuota
Compliance1100 requests / day
Expert judgment1, 250 requests / day
Authorized authority1, 2, 320 requests / day
Enterprise1, 2, 3By 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

🤖
AI agent
Submits the case
→
⚡
Routing
Qualified Sentinel
→
👤
Sentinel
Decides and justifies
→
✓
Signed decision
Webhook + API
  1. The agent submits the request: POST /api/v1/decisions.
  2. HumanLayer routes it to an active Sentinel in the right domain (and jurisdiction) with two-factor authentication configured, least busy first.
  3. The Sentinel decides (approved or rejected, justification required) or escalates for a second opinion: the request then moves to another Sentinel.
  4. The decision is signed (Ed25519) and delivered by webhook; it remains available through GET /api/v1/decisions?id=….
  5. If the SLA is exceeded, you receive sla.breached and the request is reassigned automatically (up to three escalations). The check runs every 5 minutes.
  6. 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

POST /api/v1/decisions

Creates a decision request and assigns it to a qualified Sentinel as soon as one is available.

Request body (JSON, 64 KB max)

ParameterTypeRequiredDescription
agent_idstringrequiredYour agent identifier (120 characters max)
agent_namestringoptionalHuman-readable agent name
request_typestringrequiredcompliance | judgment | signature (see levels)
domainstringrequiredlegal, finance, compliance, medical, accounting, hr, insurance, quality, security, other
jurisdictionstringoptionalRequired jurisdiction (e.g. “Quebec”, “France”)
summarystringrequiredThe question put to the Sentinel (5,000 characters max)
context_jsonobjectoptionalStructured case prepared by the AI (50 KB max)
prioritystringoptionallow | normal (default) | high | urgent
sla_minutesintegeroptionalA longer delay than the level’s commitment (10,080 max)
callback_urlstringoptionalWebhook URL: HTTPS required, public host (private and internal addresses are rejected)

Response (200)

json
{
  "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” status If no qualified Sentinel is available, the request stays pending; routing is retried every 5 minutes. The Sentinel’s identity is never exposed: sentinel_ref is a stable pseudonym.

Track a request

GET /api/v1/decisions?id={request_id}

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

json
{
  "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

GET /api/v1/decisions?agent_id={agent_id}

The last 100 requests from this agent created with the same key, newest first, in the format above.

Request a second opinion

POST /api/v1/decisions

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

ParameterTypeRequiredDescription
second_opinion_ofstringrequiredrequest_id of the decided request, created with the same key
prioritystringoptionalDefaults to the original request’s
callback_urlstringoptionalDefaults to the original request’s
sla_minutesintegeroptionalSame as for a regular request

Response (200)

json
{
  "ok": true,
  "request_id": "c7d05e91-…",
  "status": "assigned",
  "level": 2,
  "sla_minutes": 120,
  "sentinel_ref": "S-9B31D0",
  "second_opinion_of": "8b1f2c4e-…"
}
One second opinion per request Only available on a decided request (otherwise 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

GET /api/v1/decision-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:

EventWhen
decision.completedThe Sentinel approved
decision.rejectedThe Sentinel rejected
decision.escalatedSecond opinion requested: the request moves to another Sentinel
sla.breachedThe 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).

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

javascript (Node.js)
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

statusMeaning
pendingWaiting for an available Sentinel
assignedAssigned to a Sentinel, on time
sla_breachedDeadline missed and no other Sentinel available yet
decidedDecided: 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": "…" }.

HTTPcodeCause
400missing_fields, invalid_request_type, invalid_domain, invalid_priority, invalid_callback_url, context_too_large, invalid_second_opinionInvalid request
401missing_api_key, invalid_api_keyMissing or unknown key
403api_key_inactive, api_key_expired, plan_restrictionSuspended, revoked or expired key, or level outside your plan
404not_foundUnknown request, or created with another key
409not_decided, second_opinion_existsSecond opinion on a request that is not decided yet, or already requested
413payload_too_largeBody larger than 64 KB
429rate_limited, daily_limit_reachedMore than 60 requests per minute per key, or plan quota reached. Honor the Retry-After header.
500server_errorInternal error: retry with increasing delays

Full example

python
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
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?"}'