Skip to content
v0.8.4stable

Authentication, request headers, and errors

All Gateway endpoints accept a bearer Gateway Key:

Authorization: Bearer gw_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

A real Gateway Key is gw_ plus 43 characters. Missing or malformed credentials return 401 invalid_api_key. Anthropic-compatible endpoints also accept x-api-key; if both headers are present and differ, authentication fails with 401.

Explicit scopes
ScopeOperation
inferenceCall model-compatible endpoints
work_unit:createCreate and close Work Units
run:createCreate and close Runs
run:attachAttach an inference request to an existing Run
governance:readRead this Project’s Work Units, Runs, and Outcomes
outcome:writeReport or revise an Outcome against the frozen definition

Historical keys without explicit scopes only receive inference; an upgrade does not silently expand their authority.

Keep an orchestration Key separate from a business-acceptance Key. The former commonly needs inference, lifecycle Scopes, and run:attach; the latter needs only outcome:write. Creating a Run never grants Outcome write authority.

Halro assigns every request a req_ ID before authentication and rate limiting. OpenAI-compatible endpoints return it in X-Request-ID. Anthropic-compatible endpoints use request-id, and error bodies also include top-level request_id.

The identifier is req_ plus 26 lowercase base32 characters representing 16 random bytes, for example req_k9x8v5fcwde34gqbn142t2r9qw. It is written before authentication and rate limiting, so 401 and 429 responses carry it and a streaming response exposes it before the first content byte.

Request IDs
FacadeLocation
OpenAI-compatible (/v1/chat/completions, etc.)X-Request-ID response header; not in the body
Anthropic-compatible (/v1/messages)request-id response header; error body also has top-level request_id

The response object’s id such as chatcmpl-... is not the Request ID. OpenAI error envelopes do not include it.

# OpenAI Python SDK
response = client.chat.completions.create(...)
print(response._request_id)
try:
...
except openai.APIStatusError as error:
print(error.request_id)
// OpenAI Node SDK
const response = await client.chat.completions.create({ ... });
console.log(response._request_id);

Anthropic Python and Node SDKs also expose the header through _request_id. Use with_raw_response in Python or withResponse in Node when complete response headers are needed.

Log this value for both success and failure. It is the stable join key an administrator can use to locate gateway-side evidence.

X-Halro-Run-ID: run_xxxxxxxxxxxxxxxxxxxxxxxxxx

When present, the key needs inference and run:attach. Halro validates Project ownership, state, TTL, and Run lifecycle budget before Provider I/O. Common failures include invalid_run_id (400), gateway_key_scope_denied (403), run_budget_exceeded (403), run_not_found (404), run_not_active (409), and run_governance_unavailable (503).

Run attribution
HTTPOpenAI error.codeMeaning
400invalid_run_idmalformed Run ID
403run_governance_disabledProject has not enabled Run Governance
403gateway_key_scope_deniedKey lacks run:attach
403run_budget_exceededRun has insufficient available budget; request did not reach a Provider
404run_not_foundRun is absent or belongs to another Project
409run_not_activeRun is closed or expired
503run_governance_unavailableauthority cannot be verified; Halro fails closed

Anthropic calls receive the same HTTP result but no error.code; read error.type using the Gateway error mapping.

The Gateway does not implement /v1/models. An SDK that calls models.list() receives 404 with an explanation. Applications select a public alias configured on their Project; the Gateway does not disclose the upstream model.

Seven sources of 429
TriggerCodeWhen it happens
Per-source-address limitrate_limit_exceededBefore authentication; bounds work requested by anonymous callers
Project requests per minuterate_limit_exceededAfter authentication; configured on the Project
Project tokens per minutetoken_rate_limit_exceededAfter authentication
Project concurrencyconcurrency_limit_exceededAfter authentication
Deployment concurrencydeployment_concurrency_limit_exceededConfigured on the Deployment
Every eligible Provider target at concurrency limitprovider_concurrency_limit_exceededAfter eligible-target selection
Upstream Provider throttlingprovider_rate_limitReturned by the Provider and normalized by Halro

403 token_guard_blocked is not a 429. It can mean a per-request threshold or a temporary block, so blind immediate retries create a useless loop.

See Scenario 1 for Project/Key isolation and Scenario 3 for Token Guard and budget rejection.

Halro emits Retry-After only when it can calculate a positive wait. Per-source and Project limits can provide it; Deployment concurrency does not. A normalized upstream 429 includes the header only when the Provider supplied a parseable value. Clients must handle it being absent.

The complete public code × HTTP status × handling mapping is in Gateway error codes. price_unavailable, sensitive_data_detected, and unsupported_feature can each use multiple statuses depending on the stage, so status alone is not a retry decision.

Client policy.

  • Branch on the stable error code, not message text alone.
  • Treat authentication, authorization, policy, and unsupported-feature errors as non-retryable until configuration or the request changes.
  • Bound retries for 429 and transient 503 responses; record every request ID.
  • Do not assume Retry-After is always present.
  • Do not call GET /v1/models; Halro exposes Project-specific Route aliases instead.