Error responses
Every non-2xx response on /api/v2/* uses the same canonical envelope. Machine-readable reason_codes let you switch on error types without parsing free-text messages.
Error envelope
{
"error": "Unauthorized: Missing API key",
"reason_code": "auth_missing_key",
"request_id": "f9e3a1b2-4c5d-6e7f-8a9b-0c1d2e3f4a5b"
}
| Field | Meaning |
|---|---|
error | Human-readable message. Never parse this; use reason_code. |
reason_code | Stable machine-readable code. Safe to switch on. |
request_id | UUID4. Echoed in the X-Request-Id response header. Include this when emailing support. |
HTTP status codes
| Code | Meaning |
|---|---|
200 | Success. |
401 | Missing, malformed, or revoked API key. |
403 | Key valid but owner is not on Professional, or account inactive. |
404 | Resource not found. |
422 | Invalid query parameter or conflicting params. |
429 | Rate limit exceeded (60/min per key). See Retry-After header. |
500 | Unhandled server error. Include request_id when reporting. |
502 | Upstream EU portal temporarily unavailable. |
503 | Derived computation unavailable (e.g. recital-article map failed to compute). |
Reason codes
reason_code | Status | Trigger |
|---|---|---|
auth_missing_key | 401 | No Authorization or X-API-Key header sent. |
auth_invalid_format | 401 | Key doesn't start with brubru_live_. |
auth_key_not_found | 401 | Key not in our DB or revoked. |
tier_insufficient | 403 | Owner isn't on Professional. |
org_inactive | 403 | Owner's account is disabled. |
rate_limited | 429 | Over 60 requests per minute for this key. |
not_found | 404 | Generic not-found (CELEX, procedure ref, MEP id, etc.). |
text_unavailable | 404 | No text cached for the requested law and Cellar fallback also failed. |
invalid_query | 422 | A query param failed validation. |
conflicting_params | 422 | Two params that alias the same bound were sent with different values. |
invalid_date_range | 422 | from is after to. |
upstream_error | 502 | An EU source (HYS, Commission calendar, EP Open Data) returned an error. |
computation_unavailable | 503 | Derived computation (recital-article map) failed for this CELEX. |
internal_error | 500 | Unhandled exception on our side. |
X-Request-Id header
Every response, successful or not, carries an X-Request-Id header. This equals the request_id in error bodies. When you report an issue, include it: we can find the exact request in our logs.
You can also supply your own X-Request-Id on the request; we honour it as long as it's ≤ 64 characters. Helpful for distributed tracing.
Recovery patterns
Rate limit (429)
Retry-After: 7
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Back off for Retry-After seconds. In Python:
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 60)))
continue # retry
Upstream error (502)
Something outside Brubru is broken (Have Your Say portal is down, Commission calendar is slow). Retry with exponential backoff up to 3 times. If it persists > 30 minutes, email us.
Conflicting params (422 conflicting_params)
You sent published_to AND published_end with different values. Pick one (prefer published_to) and retry.
Computation unavailable (503)
Specific to /legal-text/{celex}/recital-article-map. The TF-IDF parser can fail on edge-case XML. Fall back to /legal-text/{celex}/defined-terms which exercises a different parser. Email us the CELEX so we can debug.
Example error responses
Missing key:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="Brubru API"
X-Request-Id: 7f9e3c2b-1a4d-5b6c-7d8e-9f0a1b2c3d4e
Content-Type: application/json
{
"error": "Unauthorized: Missing API key",
"reason_code": "auth_missing_key",
"request_id": "7f9e3c2b-1a4d-5b6c-7d8e-9f0a1b2c3d4e"
}
Conflicting params:
HTTP/1.1 422 Unprocessable Entity
{
"error": "Conflicting upper-bound parameters: published_to=2026-01-01 and published_end=2026-03-31. They are aliases of the same bound; pass only one, or pass matching values.",
"reason_code": "conflicting_params",
"request_id": "..."
}
Rate limit:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{
"error": "Rate limit exceeded: 60 requests per minute",
"reason_code": "rate_limited",
"request_id": "..."
}
Next
See Rate limits for a full explanation of quotas and how to back off.