# 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, and show the `hint` to the person who has to act.

Example — 404 on an invoice the acting company did not issue:

```bash
curl -s https://sandbox.bilnex.io/partner/v1/invoices/9738fbfd-6c15-4b1c-8777-54fe8ed2bcab \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json
{
  "error": {
    "code": "invoice_not_found",
    "message": "No invoice 9738fbfd-6c15-4b1c-8777-54fe8ed2bcab for the company you are acting as.",
    "hint": "Check the id, and check X-Company-Id if you are acting as a different company than the one that issued it.",
    "docsUrl": "https://sandbox.bilnex.io/docs/sending-invoices.md"
  }
}
```

## Error codes

Every code the Partner API returns:

| HTTP | `error.code` | When | Retryable? | What to do |
|---|---|---|---|---|
| 400 | `validation_failed` | A body field or a parameter is missing or invalid. `message` names it, e.g. `customer.regCode: must not be blank` or `Parameter 'since' is not valid: yesterday` | No | Fix the request |
| 400 | `invalid_company_id` | `X-Company-Id` is not a UUID | No | Send a `companyId` from `GET /partner/v1/me` |
| 400 | `invoice_id_required` | `POST /partner/v1/sandbox/trigger` with `invoice_seen` and no `invoiceId` | No | Add the id of an invoice you e-mailed |
| 400 | `send_failed` | The rail refused the send: any refusal in production; in the sandbox, an e-invoice to a company outside the sandbox. A refused e-invoice goes back to `DRAFT` | No | Send it with `EMAIL`, or hand it to a person. In the sandbox, invoice your receiver company or a fixture |
| 401 | `unauthorized` | No key, a key in an unknown format, an unknown key, a key for the other host, a company key not yet activated, a revoked key, or an expired sandbox key — `message` says which | No | Follow the `hint`; see [authentication](https://sandbox.bilnex.io/docs/authentication.md) |
| 402 | `upgrade_required` | The customer's Bilnex plan does not cover the action, e.g. its monthly document quota is used up | No | Tell the customer: the company changes its plan in Bilnex |
| 403 | `company_not_owned` | The key cannot act for the company in `X-Company-Id` | No | Use a company from `GET /partner/v1/me` |
| 403 | `account_suspended` | The account the key acts for is suspended — for example after it kept reaching the e-mail limits; the e-mail send that trips the suspension can itself answer this. Every call for it answers this until Bilnex lifts the suspension | No | Stop calling for that company and contact Bilnex |
| 404 | `invoice_not_found` | No invoice with that id for the acting company | No | Check the id and `X-Company-Id` |
| 404 | `purchase_invoice_not_found` | No incoming document with that id for the acting company | No | Act as the receiving company (`X-Company-Id`) |
| 404 | `not_a_sandbox_key` | A `/partner/v1/sandbox/*` endpoint called with a key that is not a sandbox key | No | Use a key from `POST /partner/v1/sandbox` |
| 404 | `sandbox_unavailable` | A `/partner/v1/sandbox*` endpoint called on a host without a sandbox — production | No | Gate sandbox calls behind an environment flag |
| 404 | `receiver_unavailable` | A sandbox trigger could not load the sandbox's receiver company | No | Provision a new sandbox |
| 409 | `idempotency_conflict` | The first request with this `Idempotency-Key` is still running | After a pause, with the same key | Wait, then repeat the request with the same key: once the first one finishes, you get its response. Never switch to a new key — that can create or send twice |
| 422 | `customer_not_einvoice_capable` | Sandbox: `E_INVOICE` to fixture `95999902`, which cannot receive e-invoices | No | Send with `EMAIL` — the fallback your integration must have |
| 422 | `einvoice_rejected` | Sandbox: the operator of fixture `95999903` refused the e-invoice. The invoice goes back to `DRAFT` | No | Send it with `EMAIL`, or hand it to a person |
| 429 | `rate_limited` | A per-key quota is used up — 60 requests a minute, 5,000 a day, 200 sends a day — or a sandbox provisioning quota | **Yes** | Wait `Retry-After` seconds, then repeat the same request |
| 429 | `email_rate_limited` | The company reached an e-mail limit ([rate-limits](https://sandbox.bilnex.io/docs/rate-limits.md)) | **No — never automatically** | Stop e-mailing invoices for that company for a while |
| 500 | `internal_error` | Unexpected server failure | **Yes** | Back off: 1 s, 2 s, 4 s, then report it with the time of the call |
| 500 | `provisioning_failed` | The sandbox could not be created | Yes, a few times | Repeat `POST /partner/v1/sandbox` (each attempt counts toward the provisioning quotas); if it keeps failing, contact Bilnex |
| 501 | `peppol_send_unavailable` | A send with channel `PEPPOL` — not available yet | No | Send with `E_INVOICE` or `EMAIL` |
| 503 | `feature_disabled` | The Partner API is switched off for this company — or, on a `PEPPOL` send, Peppol is | **No** | Not an outage, and waiting does not help — see [go-live](https://sandbox.bilnex.io/docs/go-live.md) |
| 503 | `sandbox_capacity` | Too many live sandboxes | Not soon | Reuse your sandbox; try again later, or contact Bilnex |

## Retry policy (the only one you need)

- **Retry automatically only these:**
  - 429 `rate_limited`: sleep `Retry-After` seconds, then repeat the same request. A daily quota can mean hours.
  - 500 `internal_error`: exponential backoff — 1 s, 2 s, 4 s — then surface the failure.
  - 409 `idempotency_conflict`: after a short pause, with the same `Idempotency-Key` — never a new one, which can create or send twice.
- **Never retry automatically:**
  - 429 `email_rate_limited`. A company that keeps reaching the e-mail limits within an hour is suspended — in the sandbox too — after which every call for it answers 403 `account_suspended`. The send that trips the suspension can itself answer 403 `account_suspended` instead of 429.
  - 503 `feature_disabled`. The Partner API is switched off for that company; repeating the call changes nothing. See [go-live](https://sandbox.bilnex.io/docs/go-live.md).
  - Any other 4xx. The same request fails the same way: change it first.
  - A send that answered 200. Bilnex does not stop a second send — it sends the invoice again.
- **Make retries safe.** Send an `Idempotency-Key` on every `POST /partner/v1/invoices` and `POST /partner/v1/invoices/{id}/send`: a new key for each operation, the same key when you repeat that operation after a timeout or a 500. A repeated key returns the first response and creates or sends nothing. Without a key, read the invoice before repeating a send — `SENT` means it went out. `mark-exported` is idempotent and needs no key.
- **A refused e-invoice is not a retry case.** 422 `customer_not_einvoice_capable` or `einvoice_rejected` in the sandbox, 400 `send_failed` in production: the invoice is back in `DRAFT`. Send it with `EMAIL` or hand it to a person — never repeat the same send.

## The 429 response

The per-key request limits. The `X-RateLimit-*` headers are absent here; `Retry-After` tells you how long to wait:

```
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json;charset=UTF-8
```

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Request rate limit exceeded (per minute) for this API key.",
    "hint": "Wait for the number of seconds in the Retry-After header before retrying. Poll no faster than every 2 seconds.",
    "docsUrl": "https://sandbox.bilnex.io/docs/rate-limits.md"
  }
}
```

An e-mail limit has no `Retry-After`, and must not be retried by your code:

```json
{
  "error": {
    "code": "email_rate_limited",
    "message": "Too many invoice emails from this company in the last minute. Please slow down or contact support.",
    "hint": "Do not retry now. Stop e-mailing invoices for this company for a while, and send each invoice to an address once: a company that keeps reaching the e-mail limits within an hour is suspended.",
    "docsUrl": "https://sandbox.bilnex.io/docs/rate-limits.md"
  }
}
```

## Switched off for a company — 503 `feature_disabled`

Until Bilnex switches the Partner API on for a company, every production call for it answers:

```json
{
  "error": {
    "code": "feature_disabled",
    "message": "Feature 'partner-api' is currently disabled",
    "hint": "Not retryable: this capability is switched off for this company. Ask partners@bilnex.io to switch it on.",
    "docsUrl": "https://sandbox.bilnex.io/agents.md"
  }
}
```

It is not an outage: retrying, waiting or backing off changes nothing. Show it to the customer and see [go-live](https://sandbox.bilnex.io/docs/go-live.md).

See [rate-limits](https://sandbox.bilnex.io/docs/rate-limits.md) for the headers on authenticated responses and the e-mail limits.

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`.
