# Errors

Every non-2xx response returns the same envelope:

```json
{
  "error": {
    "code": "machine_readable_snake_case",
    "message": "Human-readable, specific to this failure.",
    "hint": "What to change to make the call succeed.",
    "docsUrl": "https://sandbox.bilnex.io/docs/<relevant-page>.md"
  }
}
```

Branch on `error.code` — never on `message` text, which may change. `hint` and `docsUrl` are written for agents: when a call fails, read them before retrying.

Example — 404 on a foreign invoice id:

```bash
curl -s https://sandbox.bilnex.io/partner/v1/invoices/00000000-0000-0000-0000-000000000000 \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json
{
  "error": {
    "code": "not_found",
    "message": "Invoice 00000000-0000-0000-0000-000000000000 does not exist for the acting company.",
    "hint": "Check the id, and check whether the invoice belongs to another company owned by this key (set X-Company-Id).",
    "docsUrl": "https://sandbox.bilnex.io/docs/errors.md"
  }
}
```

## Error codes

| HTTP | `error.code` | Meaning | Retryable? | Fix |
|---|---|---|---|---|
| 400 | `validation_error` | Request body/params malformed or missing required fields | No | `message` names the field; fix the payload |
| 401 | `missing_api_key` | No auth header | No | Send `Authorization: Bearer <YOUR_API_KEY>` |
| 401 | `invalid_api_key` | Key unknown or revoked | No | Check for truncation; re-provision or request rotation |
| 401 | `wrong_environment` | Key prefix does not match host (`blx_sbx_` on production or `blx_live_` on sandbox) | No | Use the host named in `hint` |
| 401 | `sandbox_expired` | Sandbox key past its `expiresAt` | No | `POST /partner/v1/sandbox` for a new sandbox (or `claim` before expiry next time) |
| 403 | `company_not_owned` | `X-Company-Id` names a company the key does not own | No | Pick a company from `GET /partner/v1/me` |
| 403 | `sandbox_limit_exceeded` | A sandbox cap hit (`maxInvoices` 500, `maxEmails` 200) | No | `POST /partner/v1/sandbox/reset`, or provision a new sandbox |
| 404 | `not_found` | Resource does not exist for the acting company | No | Check the id and the acting company |
| 404 | `sandbox_only` | A `/partner/v1/sandbox/*` path called in production | No | Gate sandbox calls behind an environment flag |
| 409 | `invalid_state` | Operation not valid in the resource's current status (e.g. send an already-`SENT` invoice, trigger `invoice_seen` on a non-EMAIL invoice) | No | Read the current status first; do not repeat sends |
| 409 | `idempotency_conflict` | `Idempotency-Key` reused with a different request body | No | Use a fresh key per distinct operation |
| 409 | `already_claimed` | Second `POST /partner/v1/sandbox/claim` | No | The extension is once per sandbox |
| 413 | `payload_too_large` | Upload exceeds 10 MB | No | Compress or split the file |
| 415 | `unsupported_media_type` | Digitization upload is not a PDF/image | No | Send `application/pdf` or an image type |
| 422 | `customer_not_einvoice_capable` | `E_INVOICE` send to a customer whose registry entry cannot receive | No | Fall back to `EMAIL` (this is the required branch) |
| 422 | `customer_not_peppol_capable` | `PEPPOL` send to a customer without a Peppol registration | No | Use `E_INVOICE` or `EMAIL` per the capability check |
| 422 | `einvoice_rejected` | Receiving operator rejected the document (fixture `95999903` simulates this) | No | Invoice remains `SAVED`; correct it or use another channel — never blind-retry the same send |
| 422 | `extraction_failed` | Digitization could not read the document as an invoice | No | Check the file is a real invoice; retry with a better scan |
| 429 | `rate_limited` | Requests-per-minute or per-day limit exceeded | **Yes** | Wait `Retry-After` seconds, then retry the same request |
| 500 | `internal_error` | Unexpected server failure | **Yes** | Retry with exponential backoff (start 1s, ×2, max 3 attempts); include `Idempotency-Key` on POSTs so retries are safe |
| 503 | `service_unavailable` | Temporary outage/maintenance | **Yes** | Same backoff as 500; honor `Retry-After` if present |

## Retry policy (the only one you need)

- Retry ONLY 429/500/503. Every 4xx except 429 is deterministic — retrying without changing the request will fail identically.
- On 429, sleep exactly `Retry-After` seconds (always present on 429). On 500/503, exponential backoff: 1s, 2s, 4s, then surface the failure.
- Make POST retries safe by always sending `Idempotency-Key` on `POST /partner/v1/invoices` and `POST /partner/v1/invoices/{id}/send`. `mark-exported` is idempotent by design and needs no key.
- Never re-send an e-invoice as an error-handling strategy — a 422 rejection means fix or re-route, not repeat.

## The 429 response

```
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1786358472
```

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit of 60 requests per minute exceeded.",
    "hint": "Wait 12 seconds (Retry-After), then retry. Slow the polling loop; never poll faster than every 2 seconds.",
    "docsUrl": "https://sandbox.bilnex.io/docs/rate-limits.md"
  }
}
```

See [rate-limits](https://sandbox.bilnex.io/docs/rate-limits.md) for the headers on every response.

API version 2026-08-01. /partner/v1 changes are additive-only; see [versioning-policy](https://sandbox.bilnex.io/docs/versioning-policy.md).

---
**Building with a coding agent?** Start from [agents.md](https://sandbox.bilnex.io/agents.md) or install the skill: `npx skills add https://sandbox.bilnex.io`. Machine-readable index: [llms.txt](https://sandbox.bilnex.io/llms.txt). Every page on this site is also plain markdown — append `.md`.
