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_orevt_prefix. The path parameter is typed as a UUID, so a prefixed id is rejected as a schema error, not a404. 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 prefix | Created on | Stored in | Use against |
|---|---|---|---|
pk_live_... | app.pilot5.ai | Production database | api.pilot5.ai |
pk_test_... | staging dashboard | Staging database | staging-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
| Field | Type | Required | Notes |
|---|---|---|---|
question | string | yes | 10–2000 chars |
mode | string | no | deliberation1 (5 personas, ~4 cr — default) or smartroute1 (single LLM, ~0.5 cr) |
use_case | string | no | general (default) · pricing · code · strategy · marketing · logistics |
webhook_url | string | no | HTTPS URL that receives the signed terminal-state callback. Private/loopback IPs are rejected. Omit it to poll instead (see §4) |
metadata | object | no | Opaque JSON, ≤4 KB. Echoed verbatim in the webhook payload |
difficulty | string | no | EASY · MEDIUM (default) · HARD. Sizes the credit reservation only |
output_archetype | string | no | Pre-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 originaldeliberation_idinstead 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:
| Code | Meaning | What to do |
|---|---|---|
IDEMPOTENCY_CONFLICT | Same key, different body — a bug in your caller | Do not retry; use a fresh key |
IDEMPOTENCY_IN_PROGRESS | Same key and body, first request still processing | Retry 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).
| Status | Code | Meaning |
|---|---|---|
| 401 | INVALID_API_KEY | Missing, malformed, revoked, or wrong-environment key |
| 402 | INSUFFICIENT_CREDITS | Workspace balance < estimated_credits |
| 403 | CONTENT_BLOCKED | Question failed safety guardrails (no charge) |
| 403 | INSUFFICIENT_SCOPE | Key lacks deliberations:write |
| 409 | IDEMPOTENCY_CONFLICT | Same Idempotency-Key reused with a different body — do not retry |
| 409 | IDEMPOTENCY_IN_PROGRESS | Same 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.code | Meaning | What to do |
|---|---|---|
processing_timeout | The run exceeded its processing budget and was halted. Usually transient. | Retry the request |
processing_failed | The 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 running | On completed / failed |
|---|---|
GET /{id} → 200 with the short status block | 200 with the full payload |
GET /{id}/result → 202 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
2xxresponse within 10 seconds. - Retries on
5xx, network error, or408/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 markeddead. 4xxother than408/429is 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.
| Limiter | Limit | Keyed on |
|---|---|---|
POST /v1/api/deliberations | 60 / hour | API key |
GET /v1/api/deliberations/{id} | 600 / min | API key |
| Every request, before auth | 60 / min | source 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": "..." } ] }
| Code | HTTP | When |
|---|---|---|
INVALID_API_KEY | 401 | Bad/missing/revoked key, or env mismatch |
INSUFFICIENT_SCOPE | 403 | Key lacks deliberations:write / deliberations:read |
INSUFFICIENT_CREDITS | 402 | Balance below estimate |
CONTENT_BLOCKED | 403 | Guardrails rejected the question (no charge) |
NOT_FOUND | 404 | Unknown deliberation_id or wrong workspace |
IDEMPOTENCY_CONFLICT | 409 | Key reused with a different body — do not retry |
IDEMPOTENCY_IN_PROGRESS | 409 | Same key and body, first request still running — retry after Retry-After |
| (none) | 422 | Schema/length/enum violation — read the detail array |
| (none) | 429 | Rate limit — honour Retry-After |
QUEUE_UNAVAILABLE · DB_UNAVAILABLE · INTERNAL_ERROR | 5xx | Our 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.
| Mode | Reserved at submit (easy / medium / hard) | Hard ceiling |
|---|---|---|
smartroute1 | 0.4 / 0.5 / 0.7 | 1.2 |
deliberation1 | 3.0 / 4.2 / 5.5 | 7.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.