Authentication, request headers, and errors
Authentication
Section titled “Authentication”All Gateway endpoints accept a bearer Gateway Key:
Authorization: Bearer gw_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxA 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
Section titled “Explicit scopes”| Scope | Operation |
|---|---|
inference | Call model-compatible endpoints |
work_unit:create | Create and close Work Units |
run:create | Create and close Runs |
run:attach | Attach an inference request to an existing Run |
governance:read | Read this Project’s Work Units, Runs, and Outcomes |
outcome:write | Report 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.
Request IDs
Section titled “Request IDs”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.
| Facade | Location |
|---|---|
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 SDKresponse = client.chat.completions.create(...)print(response._request_id)
try: ...except openai.APIStatusError as error: print(error.request_id)// OpenAI Node SDKconst 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.
Run attribution
Section titled “Run attribution”X-Halro-Run-ID: run_xxxxxxxxxxxxxxxxxxxxxxxxxxWhen 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).
| HTTP | OpenAI error.code | Meaning |
|---|---|---|
| 400 | invalid_run_id | malformed Run ID |
| 403 | run_governance_disabled | Project has not enabled Run Governance |
| 403 | gateway_key_scope_denied | Key lacks run:attach |
| 403 | run_budget_exceeded | Run has insufficient available budget; request did not reach a Provider |
| 404 | run_not_found | Run is absent or belongs to another Project |
| 409 | run_not_active | Run is closed or expired |
| 503 | run_governance_unavailable | authority 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.
No GET /v1/models
Section titled “No GET /v1/models”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
Section titled “Seven sources of 429”| Trigger | Code | When it happens |
|---|---|---|
| Per-source-address limit | rate_limit_exceeded | Before authentication; bounds work requested by anonymous callers |
| Project requests per minute | rate_limit_exceeded | After authentication; configured on the Project |
| Project tokens per minute | token_rate_limit_exceeded | After authentication |
| Project concurrency | concurrency_limit_exceeded | After authentication |
| Deployment concurrency | deployment_concurrency_limit_exceeded | Configured on the Deployment |
| Every eligible Provider target at concurrency limit | provider_concurrency_limit_exceeded | After eligible-target selection |
| Upstream Provider throttling | provider_rate_limit | Returned 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.
Retry-After is not always present
Section titled “Retry-After is not always present”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-Afteris always present. - Do not call
GET /v1/models; Halro exposes Project-specific Route aliases instead.