Errors
Single-field JSON error body, standard HTTP status codes, no retry-after on 429 by default.
ProxifAI returns errors as a single-field JSON body — no nested error codes, no separate details. Status codes follow REST conventions; the body just carries the human-readable message.
Error response shape
The canonical helper is utils.Error, which writes:
{
"error": "team not found"
}
That’s it — one field. There’s no code, no details, no requestId. Some subsystems (LLM gateway, port proxy) add nested envelopes for compatibility:
{
"error": {
"message": "no LLM providers configured",
"type": "gateway_error"
}
}
…but this is the exception. Always check whether the body parses as a flat string field or an envelope before consuming it programmatically. The two surfaces:
| Shape | Where it’s used |
|---|---|
{"error":"..."} | Core REST API (/api/v1/{issues,projects,workflows,...}), forge, plan, platform |
{"error":{"message":"...","type":"..."}} | LLM gateway (/api/v1/llm/*) — OpenAI/Anthropic-shaped for SDK compatibility |
Robust clients pull body.error?.message ?? body.error and treat both as strings.
HTTP status codes
| Code | Meaning | When it happens |
|---|---|---|
200 | OK | Read succeeded |
201 | Created | Resource created (most POST endpoints) |
202 | Accepted | Async op enqueued (instances, dispatch — final state via subsequent GET) |
204 | No Content | Delete or other success without a body |
400 | Bad Request | Validation failure, malformed JSON, missing required field |
401 | Unauthorized | Missing/invalid Authorization header — see Authentication |
402 | Payment Required | Credit check failed (Enterprise only — needs account-api) |
403 | Forbidden | Token valid but lacks the scope or RBAC permission |
404 | Not Found | Resource doesn’t exist or you can’t see it (404 returned instead of 403 for tenant-isolated resources) |
409 | Conflict | Unique-key violation (duplicate slug, repo name, etc.) |
412 | Precondition Failed | If-Match/If-Unmodified-Since mismatch (rare, mostly forge writes) |
422 | Unprocessable Entity | Semantic validation (state machine violation, e.g. merging a closed PR) |
429 | Too Many Requests | LLM gateway rate limit only — see Rate Limits |
500 | Internal Server Error | Unexpected server failure — file an issue with the response body |
502 | Bad Gateway | Upstream provider failure (LLM gateway when all providers fail their circuits) |
503 | Service Unavailable | Boot phase or core dependency missing (Postgres unreachable, etc.) |
Async operations
A handful of endpoints return 202 Accepted because the operation runs through a NATS-backed worker:
- Issue dispatch —
POST /api/v1/issues/{id}/dispatchreturns202and theagentExecutionId; subscribe to/api/v1/agents/executions/{id}/events(SSE) for progress - Instance creation —
POST /api/v1/instances(Enterprise / vm-manager) is async; poll the instance row untilstatussettles - Workflow run —
POST /api/v1/workflows/{id}/runreturns the execution ID; pollGET /api/v1/workflows/{id}/executions/{execId}or streamGET /api/v1/workflows/{id}/executions/{execId}/stream
Pattern:
# 1. Kick off
EXEC_ID=$(curl -s -X POST http://localhost:3000/api/v1/issues/iss_42/dispatch \
-H "Authorization: Bearer $PFAI_TOKEN" -d '{"agentId":"claude-code"}' \
| jq -r '.executionId')
# 2. Block until terminal (issue dispatch creates an agent execution)
pfai wait agent-execution/$EXEC_ID
# or stream:
curl -N http://localhost:3000/api/v1/agents/executions/$EXEC_ID/events \
-H "Authorization: Bearer $PFAI_TOKEN"
pfai wait <kind>/<id> (see CLI → Utility) is the canonical way to block until a terminal state — handles reconnects and surfaces a non-zero exit on failure.
Idempotency
Idempotency keys are only honored on the instances API (POST /api/v1/instances):
curl -X POST http://localhost:3000/api/v1/instances \
-H "Authorization: Bearer $PFAI_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{...}'
The handler (internal/instances/api/handlers/instance.go) deduplicates retries inside a 24-hour window. Other POST endpoints don’t yet support Idempotency-Key — the header is parsed by the CORS middleware (allowed) but ignored.
For non-idempotent endpoints, dedupe at the application layer (use a deterministic identifier in the body, then ignore 409 conflicts).
Handling errors in clients
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
// Both flat and nested error envelopes:
const message = body?.error?.message ?? body?.error ?? res.statusText;
if (res.status === 401) throw new AuthError(message);
if (res.status === 403) throw new PermissionError(message);
if (res.status === 404) throw new NotFoundError(message);
if (res.status === 429) {
// Gateway-only — back off and retry
await new Promise(r => setTimeout(r, 1000));
return retry();
}
throw new ApiError(res.status, message);
}