REST API

Pilot5 over HTTPS.

Launch a deliberation with an API key, receive the result on a signed webhook — from any backend, workflow tool, or non-MCP client. If you’re wiring Pilot5 into Claude, Cursor, Perplexity, or another MCP-compatible client, the MCP connector is the simpler path.

Submit a question, get a deliberated answer from a 5-persona AI panel via webhook callback. Same backend that powers mcp.pilot5.ai/mcp — exposed as plain HTTPS for direct integration.

Base URL: https://api.pilot5.ai (production) · https://staging-api.pilot5.ai (staging)


1. Quickstart

1. Create an API key in the dashboard at app.pilot5.ai/settings?tab=api — sign in first; that URL returns a bare 404 when you're logged out, which is the auth guard rather than a wrong address. Live keys start with pk_live_, test keys with pk_test_. The plaintext key and the webhook signing secret are shown once on creation — store both securely.

2. Submit a deliberation:

curl -X POST https://api.pilot5.ai/v1/api/deliberations \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "question": "Should we price our SaaS per-seat or usage-based for mid-market?",
    "mode": "deliberation1",
    "use_case": "pricing",
    "webhook_url": "https://example.com/hooks/pilot5",
    "metadata": {"order_id": "ord_123"}
  }'

Response (202 Accepted):

{
  "deliberation_id": "9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34",
  "status": "queued",
  "result_url": "https://api.pilot5.ai/v1/api/deliberations/9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34/result",
  "estimated_credits": 4.2
}

3. Receive the webhook at https://example.com/hooks/pilot5. smartroute1 typically returns in a couple of minutes; deliberation1 takes several and can run to ~20 minutes on a hard, retrieval-heavy question — size your timeouts for the ceiling, not the median. The run continues server-side regardless of your client timing out, and the result stays retrievable by id. Verify the signature, then 4. fetch the full result from result_url.

IDs are plain UUIDs — no del_ or evt_ prefix. The path parameter is typed as a UUID, so a prefixed id is rejected as a schema error, not a 404. Size your columns and write your validators accordingly.


2. Authentication

All requests authenticate with an API key — not a dashboard login session or Clerk JWT:

Authorization: Bearer pk_live_xxx
Key prefixCreated onStored inUse against
pk_live_...app.pilot5.aiProduction databaseapi.pilot5.ai
pk_test_...staging dashboardStaging databasestaging-api.pilot5.ai

The prefix is a label for the key's environment of origin. Real isolation comes from which database the key was created in: production keys (pk_live_*) live in the production database and only authenticate against api.pilot5.ai; staging keys (pk_test_*) live in the staging database and only authenticate against staging-api.pilot5.ai. A pk_test_* key sent to api.pilot5.ai returns 401 INVALID_API_KEY because the production database has no record of it.

Both prefixes debit real credits in their respective environments. Use staging credits for development and integration testing — they're separate from your production balance. Never commit keys to source; rotate immediately via the dashboard if exposed.

Keys carry scopes (deliberations:write, deliberations:read). A write key implicitly grants read.


3. POST /v1/api/deliberations

Submit a question for deliberation.

Request body

FieldTypeRequiredNotes
questionstringyes10–2000 chars
modestringnodeliberation1 (5 personas, ~4 cr — default) or smartroute1 (single LLM, ~0.5 cr)
use_casestringnogeneral (default) · pricing · code · strategy · marketing · logistics
webhook_urlstringnoHTTPS URL that receives the signed terminal-state callback. Private/loopback IPs are rejected. Omit it to poll instead (see §4)
metadataobjectnoOpaque JSON, ≤4 KB. Echoed verbatim in the webhook payload
difficultystringnoEASY · MEDIUM (default) · HARD. Sizes the credit reservation only
output_archetypestringnoPre-select the report shape instead of letting Pilot5 classify the question: commit · map · execute · audit · generate · align · periodic · ranking · framework · paper. Useful for recurring reports that must keep the same structure

deliberation2 (Dream Team with HITL checkpoints) is not available via the API — it requires interactive pauses incompatible with async webhook delivery. Use the MCP server or web UI for HITL flows.

Headers

  • Idempotency-Key (recommended): any string ≤255 chars. Replaying the same key + body within 24h returns the original deliberation_id instead of creating a duplicate.

Two different 409s come out of idempotency, and they need opposite handling — branch on the code, because a blind retry-on-409 loop spins forever on the first:

CodeMeaningWhat to do
IDEMPOTENCY_CONFLICTSame key, different body — a bug in your callerDo not retry; use a fresh key
IDEMPOTENCY_IN_PROGRESSSame key and body, first request still processingRetry after the Retry-After delay (5s)

Responses

202 Accepted:

{
  "deliberation_id": "9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34",
  "status": "queued",
  "result_url": "https://api.pilot5.ai/v1/api/deliberations/9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34/result",
  "estimated_credits": 4.2
}

estimated_credits is the amount reserved at submit time. The final charge is computed from actual token usage and may be lower; any excess is refunded automatically (see §8).

StatusCodeMeaning
401INVALID_API_KEYMissing, malformed, revoked, or wrong-environment key
402INSUFFICIENT_CREDITSWorkspace balance < estimated_credits
403CONTENT_BLOCKEDQuestion failed safety guardrails (no charge)
403INSUFFICIENT_SCOPEKey lacks deliberations:write
409IDEMPOTENCY_CONFLICTSame Idempotency-Key reused with a different body — do not retry
409IDEMPOTENCY_IN_PROGRESSSame key and body, first request still running — retry after Retry-After
422(none)Schema violation; detail is an array of field errors
429(none)Rate limit — see Retry-After and §6

Codes live at detail.error.code (see §7 for the full envelope), not at the top level.


4. GET /v1/api/deliberations/{id} · poll-only (no webhook needed)

You don't have to run a webhook receiver. If you omit webhook_url on the POST, just poll this endpoint until the status is terminal — the result is available here whether or not a webhook is configured. This is the simplest integration when you can't host a public HTTPS callback (serverless, no-code, behind a firewall):

1. POST /v1/api/deliberations  (no webhook_url)  → { deliberation_id, result_url }
2. GET  result_url every ~10s   → 202 while running, 200 with `synthesis` when done

The shape is status-only while running, full payload on terminal states — pollers don't need a second round-trip to /result.

curl https://api.pilot5.ai/v1/api/deliberations/9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34 \
  -H "Authorization: Bearer pk_live_xxx"

While running (status: queued or running):

{
  "id": "9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34",
  "status": "running",
  "mode": "deliberation1",
  "use_case": "pricing",
  "created_at": "2026-04-29T10:00:00Z",
  "updated_at": "2026-04-29T10:01:32Z"
}

On completed:

{
  "id": "9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34",
  "status": "completed",
  "synthesis": "...full arbiter synthesis markdown...",
  "summary": "...one-paragraph TL;DR...",
  "confidence_index": 7.2,
  "mode": "deliberation1",
  "use_case": "pricing",
  "credits_charged": 3.87,
  "error": null,
  "created_at": "2026-04-29T10:00:00Z",
  "updated_at": "2026-04-29T10:04:12Z",
  "completed_at": "2026-04-29T10:04:12Z"
}

On failed:

{
  "id": "9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34",
  "status": "failed",
  "synthesis": null,
  "summary": null,
  "confidence_index": null,
  "mode": "deliberation1",
  "use_case": "pricing",
  "credits_charged": 0,
  "error": {
    "code": "processing_timeout",
    "message": "The deliberation exceeded its processing time and was halted before completing. This is usually transient — retry the request."
  },
  "created_at": "2026-04-29T10:00:00Z",
  "updated_at": "2026-04-29T10:01:00Z",
  "completed_at": "2026-04-29T10:01:00Z"
}

status is one of queued · running · completed · failed. On failed, the deliberation creation cost is fully refunded — credits_charged reflects the actual debit (0 for full refunds).

The error.code on a failed deliberation is one of:

error.codeMeaningWhat to do
processing_timeoutThe run exceeded its processing budget and was halted. Usually transient.Retry the request
processing_failedThe run failed during processing for another reason.Retry, or contact support with the deliberation id

GET /v1/api/deliberations/{id}/result returns the same payload shape as the completed case above, so partners can use either endpoint depending on whether they want to keep polling or fetch once after a webhook arrives.

The two endpoints diverge only while the run is in flight, which matters if you branch on the HTTP status rather than the status field:

While runningOn completed / failed
GET /{id}200 with the short status block200 with the full payload
GET /{id}/result202 with {status, deliberation_id}200 with the full payload

Pick one and stay on it.

confidence_index may be null on a completed run, as may summary on older runs. It is read from the synthesis record, which is not guaranteed on every pipeline path. Render "not scored" rather than 0 — a null is not a zero-confidence verdict.

Poll at most every 10s. Webhook delivery is faster and cheaper.


5. Webhook delivery

When a deliberation reaches a terminal state, Pilot5 sends:

POST {webhook_url}
X-Pilot5-Event: deliberation.completed
X-Pilot5-Timestamp: 1745923452
X-Pilot5-Signature: t=1745923452,v1=5257a869e7ec...
X-Pilot5-Event-Id: 4c81e0d5-2b6f-4c9a-9f13-8ad2e7b45011
X-Pilot5-Delivery-Id: 71a0c934-55b8-4e2d-9c07-3f1d6b8e2a44
X-Pilot5-Attempt: 1
User-Agent: Pilot5-Webhook/1.0
Content-Type: application/json
{
  "id": "4c81e0d5-2b6f-4c9a-9f13-8ad2e7b45011",
  "event": "deliberation.completed",
  "created_at": "2026-04-29T10:04:12Z",
  "data": {
    "deliberation_id": "9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34",
    "status": "completed",
    "mode": "deliberation1",
    "use_case": "pricing",
    "result_url": "https://api.pilot5.ai/v1/api/deliberations/9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34/result",
    "completed_at": "2026-04-29T10:04:12Z",
    "credits_charged": 3.87,
    "metadata": {"order_id": "ord_123"}
  }
}

Other events: deliberation.failed — same shape with status: "failed". The webhook itself carries no report and no error detail; GET result_url returns the error block (processing_timeout / processing_failed) and the run is fully refunded.

The webhook never contains the synthesis itself. Fetch result_url with your API key — this keeps payloads small and ensures only authenticated callers see the deliberation content.

Delivery semantics

  • We expect a 2xx response within 10 seconds.
  • Retries on 5xx, network error, or 408 / 429: +1 min, +5 min, +30 min, +2 h, +12 h — 5 attempts in total, spanning roughly 15 hours. After the last one the delivery is marked dead.
  • 4xx other than 408 / 429 is not retried.

A webhook_url on the request is dispatched statelessly. It is not attached to a managed endpoint, which has three consequences worth designing around:

  • the delivery is not listed in the dashboard and cannot be replayed from it;
  • there is no consecutive-failure counter and nothing to auto-disable or re-enable;
  • if all 5 attempts fail, polling is your only recovery.

Treat the webhook as an optimisation and GET /v1/api/deliberations/{id} as the source of truth — the result remains available there indefinitely, whatever happened to the callback. (Managed endpoints configured in the dashboard do get delivery history, manual replay, and auto-disable after 20 consecutive failures. That is a different feature from the per-call webhook_url documented here.)

Signature verification

X-Pilot5-Signature is t={timestamp},v1={hex_hmac}. The signed payload is:

{timestamp}.{raw_request_body}

HMAC-SHA256 with your key's webhook secret (returned once, alongside the key, on creation). Always use a timing-safe compare and reject if |now - timestamp| > 300s (replay protection).

Python (quick reference):

import hmac, hashlib, time

def verify(secret: str, body: bytes, sig_header: str, ts_header: str) -> bool:
    parts = dict(p.split("=", 1) for p in sig_header.split(","))
    if abs(time.time() - int(ts_header)) > 300:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{ts_header}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

body must be the raw bytes of the request — re-serializing parsed JSON will change whitespace and break the signature. Node and Go reference implementations are available on request — email support@pilot5.ai.


6. Rate limits

Two independent limiters guard this surface. Either can reject you.

LimiterLimitKeyed on
POST /v1/api/deliberations60 / hourAPI key
GET /v1/api/deliberations/{id}600 / minAPI key
Every request, before auth60 / minsource IP

The per-IP limiter is the one that surprises people: it counts every request from an egress IP regardless of which key sent it, so shared NAT, CI runners and serverless pools can trip it while your per-key budget is untouched. It answers with detail as a plain string ("Too many requests. Please slow down.") plus Retry-After.

Do not read your key budget off X-RateLimit-* on a successful response. Those headers are stamped by the per-IP middleware, so a 202 reports X-RateLimit-Limit: 60 meaning 60 per minute per IP — not the 60-per-hour key budget — and omits X-RateLimit-Reset entirely. The per-key figures appear only on a per-key 429:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1745925600
Retry-After: 1420

Track your own submission count rather than inferring it from headers. Email enterprise@pilot5.ai to lift caps.


7. Errors

There are three error shapes, not one. Which one you get depends on which layer rejected the request, so write your handler defensively rather than assuming a single envelope.

Most errors — auth, scope, credits, guardrails, idempotency, not-found, internal. The error object is nested under detail, and codes are SCREAMING_SNAKE_CASE:

{
  "detail": {
    "error": {
      "code": "INSUFFICIENT_CREDITS",
      "message": "Insufficient credits. Required: 4.20, available: 1.20",
      "request_id": "",
      "required": 4.2,
      "available": 1.2,
      "requested_mode": "deliberation1",
      "alternatives": [{"mode": "smartroute1", "estimated_credits": 0.5}]
    }
  }
}

request_id may be an empty string. INSUFFICIENT_CREDITS carries required, available, requested_mode and alternatives in addition to code/message — use alternatives to offer the caller a cheaper mode instead of a hard failure.

Rate limits (429) — no error object, no code:

{ "detail": { "message": "Rate limit exceeded. Maximum 60 requests per 3600 seconds.",
              "retry_after": 1420 } }

The pre-auth per-IP limiter (see §6) returns detail as a plain string instead. Both carry Retry-After.

Schema validation (422) — FastAPI's default, an array:

{ "detail": [ { "loc": ["body", "question"], "msg": "...", "type": "..." } ] }
CodeHTTPWhen
INVALID_API_KEY401Bad/missing/revoked key, or env mismatch
INSUFFICIENT_SCOPE403Key lacks deliberations:write / deliberations:read
INSUFFICIENT_CREDITS402Balance below estimate
CONTENT_BLOCKED403Guardrails rejected the question (no charge)
NOT_FOUND404Unknown deliberation_id or wrong workspace
IDEMPOTENCY_CONFLICT409Key reused with a different body — do not retry
IDEMPOTENCY_IN_PROGRESS409Same key and body, first request still running — retry after Retry-After
(none)422Schema/length/enum violation — read the detail array
(none)429Rate limit — honour Retry-After
QUEUE_UNAVAILABLE · DB_UNAVAILABLE · INTERNAL_ERROR5xxOur fault — include request_id in support tickets

A safe parse: read detail.error.code when detail is an object, and fall back to the HTTP status when it is a list or a string. Always log request_id — it's the fastest path to a root cause from our side.


8. Pricing

Credits are reserved at submit (using estimated_credits) and finalized to actual cost on completion. Excess is refunded to your workspace balance automatically. Full refund on:

  • Guardrail block (no LLM call made)
  • Creation failure (queue / DB error before pipeline starts)
  • Pipeline failure (status: "failed")

Budget against the per-mode ceiling, not estimated_credits. The reservation is a P75 estimate: three quarters of runs finalize below it, but a long or retrieval-heavy question finalizes above it, up to a hard cap that cannot be exceeded.

ModeReserved at submit (easy / medium / hard)Hard ceiling
smartroute10.4 / 0.5 / 0.71.2
deliberation13.0 / 4.2 / 5.57.5

Large attached documents add a context surcharge within that ceiling. Per-mode ranges and the live credit table: pilot5.ai/pricing.


9. OpenAPI spec

A machine-readable OpenAPI 3.1 spec for this surface is published at pilot5.ai/docs/api/openapi.json. Point a Vertex AI Extension, a Gemini function-calling config, Postman, or any standard OpenAPI client at it. Both servers are listed (production + staging); authenticate with Authorization: Bearer pk_live_… (or pk_test_… on staging).


10. SDKs

No official SDKs — the API is small enough that hand-written HTTP is the right tool. If you're integrating from an AI-tool context (Claude Desktop/Cowork, agents, IDE extensions), use the MCP server at https://mcp.pilot5.ai/mcp instead — same backend, same credits, native tool-call ergonomics.


Happy-path trace

# 1. Submit
$ curl -X POST https://api.pilot5.ai/v1/api/deliberations \
    -H "Authorization: Bearer pk_live_xxx" \
    -H "Idempotency-Key: 7c1d..." \
    -H "Content-Type: application/json" \
    -d '{"question":"...","mode":"deliberation1","use_case":"pricing",
         "webhook_url":"https://example.com/hooks/pilot5"}'

HTTP/1.1 202 Accepted
{"deliberation_id":"9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34","status":"queued","estimated_credits":4.2,...}

# 2. ~4 min later, your endpoint receives:
POST https://example.com/hooks/pilot5
X-Pilot5-Event: deliberation.completed
X-Pilot5-Signature: t=1745923452,v1=5257a869...
{"event":"deliberation.completed","data":{"deliberation_id":"9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34",
 "result_url":"https://api.pilot5.ai/v1/api/deliberations/9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34/result",...}}

# 3. Verify signature, then fetch:
$ curl https://api.pilot5.ai/v1/api/deliberations/9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34/result \
    -H "Authorization: Bearer pk_live_xxx"

HTTP/1.1 200 OK
{"id":"9f2c1b7a-4d3e-4a10-b8c2-6e5f0a1d7c34","status":"completed","synthesis":"...","summary":"...",
 "credits_charged":3.87,"completed_at":"2026-04-29T10:04:12Z"}

Questions: support@pilot5.ai

Questions, raised rate limits, or enterprise DPAs? Contact enterprise@pilot5.ai. See also the Privacy Policy, Terms of Service, and Data Processing Agreement.