---
name: bilnex-erp-integration
description: Build a Bilnex e-invoicing integration into an ERP codebase — outbound invoices (Estonian operator-channel e-invoices, with PDF-by-email when the capability check says the customer cannot receive them) and inbound purchase-invoice polling with mark-exported, verified end-to-end against the self-serve Bilnex sandbox. Peppol sending and digitization are not available through the API yet. Use when asked to "integrate Bilnex", "add e-invoicing/e-arved to an ERP", or implement Estonian e-invoicing.
license: CC-BY-4.0
compatibility: Any coding agent with HTTPS access and file-editing ability (Claude Code, Cursor, Codex). No local runtime requirements; all testing runs against the hosted sandbox.
metadata:
  api-version: "2026-08-01"
  docs: "https://sandbox.bilnex.io/llms.txt"
  publisher: "Bilnex"
---

# Bilnex ERP integration

Pinned API version: **2026-08-01** (additive-only within `/partner/v1`).
Base URLs: sandbox **`https://sandbox.bilnex.io`** (build + verify here), production **`https://api.app.bilnex.io`** (same paths). Keys: `blx_sbx_...` sandbox (self-serve, 2-day TTL, `claim` extends once to 30 days) / `blx_live_...` production (generated by each customer company in Bilnex, one company per key — Bilnex does not issue keys to vendors). Keys are host-bound — the wrong host returns 401.
Auth header: `Authorization: Bearer <YOUR_API_KEY>` (or `X-API-Key`). The provisioned key (`apiKey`) acts for the sandbox's sender company by default; `X-Company-Id: <uuid>` selects the receiver (a company the key does not own → 403 `company_not_owned`). A company key acts for its one company and needs no `X-Company-Id`.

Canonical human-readable docs: `https://sandbox.bilnex.io/agents.md`. Every docs page is plain markdown (append `.md`). OpenAPI: `https://sandbox.bilnex.io/partner/v1/openapi.json`.

## Routing table — what the ERP needs → what to build

| ERP need | Flow | Endpoints | Reference |
|---|---|---|---|
| Send sales invoices as e-invoices | capability check → create → send `E_INVOICE` | `GET /partner/v1/customers/{regCode}/e-invoice-capability` · `POST /partner/v1/invoices` · `POST /partner/v1/invoices/{id}/send` | [references/flows.md](references/flows.md) |
| Send when customer can't receive e-invoices | same, branch to `EMAIL` on `eInvoiceCapable: false` | same + `GET /partner/v1/sandbox/emails` (sandbox assert) | [references/flows.md](references/flows.md) |
| Send over Peppol | not available yet — channel `PEPPOL` answers 501 `peppol_send_unavailable`; read `recommendedChannel: PEPPOL` as `EMAIL` | — | [references/flows.md](references/flows.md) |
| Store/print the invoice PDF | fetch presigned PDF (valid 15 minutes) | `GET /partner/v1/invoices/{id}/pdf` | [references/flows.md](references/flows.md) |
| Show "customer opened it" | read invoices, handle `SEEN` (EMAIL only) | `GET /partner/v1/invoices?status=` · `GET /partner/v1/invoices/{id}` | [references/flows.md](references/flows.md) |
| Import incoming purchase invoices | poll with `since` cursor → original PDF → ERP write → mark-exported | `GET /partner/v1/purchase-invoices` · `GET .../{id}` · `GET .../{id}/original` · `POST .../{id}/mark-exported` | [references/receiving.md](references/receiving.md) |
| Receive Peppol documents | not listed by the API yet | — | [references/receiving.md](references/receiving.md) |
| Digitization | not an API capability: no upload endpoint. Documents digitized in Bilnex arrive in the receiving flow as `DIGITIZED_PENDING` until a person confirms them | the receiving flow | [references/receiving.md](references/receiving.md) |
| Robust error/retry behavior | envelope, code table, 429 `Retry-After`, idempotency, e-mail limits | all | [references/errors-and-limits.md](references/errors-and-limits.md) |
| Ship to production | verification passes → Bilnex opens production for your integration on request → each customer generates its key in Bilnex Settings → ERP integration and enters URL + key in the ERP | `GET /partner/v1/sandbox/verification` · `GET /partner/v1/me` | [references/go-live.md](references/go-live.md) |
| Understand the Estonian context | — | — | [references/estonia-einvoicing-context.md](references/estonia-einvoicing-context.md) |

## Build procedure (8 steps)

1. **Provision** — `POST https://sandbox.bilnex.io/partner/v1/sandbox` with `{"email":"<dev email>","erpName":"<this ERP>","ref":"agent"}` (no auth, no account). The response contains the API key (shown ONCE), a sender company, a receiver company, one company key per company (`companyKeys`, also shown once), and three fixture counterparties (`95999901` accepts e-invoices, `95999902` cannot receive them, `95999903` refuses every e-invoice). The `email` is kept so Bilnex can follow up by hand; nothing is sent to it.
2. **Config** — store the key, base URL, and receiver companyId in the codebase's existing server-side config mechanism (env var / secrets). Never in frontend code, mobile code, or committed files. Add an `environment` flag that gates all `/partner/v1/sandbox/*` calls (they answer 404 in production). In the ERP product itself, expose two **customer-editable settings — Bilnex URL and Bilnex API key** — plus a "test connection" action on `GET /partner/v1/me`: production keys are generated by each customer company in Bilnex (Settings → ERP integration), never issued to the vendor, and one key acts for exactly one company (no `X-Company-Id`). The sandbox's `companyKeys.sender`/`companyKeys.receiver` are keys of that kind — use them to test the settings screen.
3. **HTTP client module** — one thin client used by everything else: injects the auth header and optional `X-Company-Id`; parses the error envelope `{"error":{code,message,hint,docsUrl}}` into typed errors; on 429 `rate_limited` sleeps `Retry-After` then retries; on 500 `internal_error` retries with backoff (1s/2s/4s, max 3); on 409 `idempotency_conflict` waits briefly and retries with the same key; never retries 429 `email_rate_limited`, 503 `feature_disabled` or any other 4xx; sends an `Idempotency-Key` (UUID) on `POST /invoices` and `POST /invoices/{id}/send` — a new key per operation, the same key on its retries; tolerates unknown response fields.
4. **Auth ping** — call `GET /partner/v1/me` through the client; confirm `environment`, acting company, and limits. (Flips `AUTH_OK`.)
5. **Outbound mapping** — map the ERP's invoice model to `POST /partner/v1/invoices` (customer{name, regCode, email?, address?}, date, dueDate, currency, language EN/ET/LV/LT, items[{description, quantity, unit, price, vatRate}]). The server computes all totals, and the invoice object it returns is `{id, number, status, category, customerName, customerRegCode, date, dueDate, totalAmount}` — no lines, no net amount, no VAT breakdown. Keep your own lines and VAT breakdown; store Bilnex's `id`, `number` and `totalAmount` (gross). Channel selection: capability check → `eInvoiceCapable: true` → `E_INVOICE`, otherwise `EMAIL` (`recommendedChannel` can also say `PEPPOL`, which cannot be sent yet — read it as `EMAIL`). Send each invoice once and persist `status`/`category`/`sentAt`. Fetch and store the PDF.
6. **Inbound poller** — scheduled job as the receiving company (`X-Company-Id`): `GET /partner/v1/purchase-invoices?since=<cursor>`, read every page while `hasMore` (same `since`), dedupe by `id`, fetch the document's PDF from `.../{id}/original`, write to the ERP, then `POST .../mark-exported` (mandatory, idempotent — only after the ERP write is durable). The list is newest first: after the last page, store page 1's `nextSince` as the new cursor. Items carry header fields only (sender name, number, dates, currency, gross total) — no lines, no sender registry code, no IBAN. `DIGITIZED_PENDING` items were digitized in Bilnex and not yet confirmed by a person: import them if the ERP has its own review step, otherwise re-read them later by id.
7. **Exercise every flow through the integration code, not curls** — run the ERP's own send path against your receiver company's regCode (`companies.receiver.regCode`, e-invoice) and `95999902` (EMAIL fallback after a `false` capability check), and the poller against the receiver company; read `/partner/v1/sandbox/emails` to assert the EMAIL send; use `POST /partner/v1/sandbox/trigger` for `inbound_einvoice` and `invoice_seen` (`inbound_peppol` passes its optional check, but Peppol documents are not listed). Optionally drill error handling against `95999903` (422 `einvoice_rejected`).
8. **Verify** — `GET /partner/v1/sandbox/verification`; iterate on pending checks.

**Stopping condition:** Do not report the integration complete until GET /partner/v1/sandbox/verification returns readyForProduction: true. If a check is pending, read its fixHint and docsUrl, fix, re-run.

## After `readyForProduction: true` — production

Your integration code does not change for production: point it at `https://api.app.bilnex.io/partner/v1` and use the key each customer generates. What is not self-serve yet is switching it on — Bilnex opens the Partner API per integration. Tell the developer to:

1. Email partners@bilnex.io the verification response and the ERP's name.
2. Open a Bilnex account for their own company at https://app.bilnex.io, activate e-invoice sending and add a bank account.
3. Once Bilnex has switched the Partner API on for that company, generate its key under Settings → ERP integration and run the production smoke test ([references/go-live.md](references/go-live.md)).
4. Confirm it works. Bilnex then switches it on for their customers; each customer generates its own key under Settings → ERP integration and enters it in the ERP together with the URL.

Until the Partner API is switched on for a company, every production call for it answers 503 `feature_disabled` — not retryable. Nothing is e-mailed automatically, on readiness or otherwise.

## Hard rules (condensed)

1. Never assume e-invoice capability — always call the capability endpoint; `false` → `EMAIL`.
2. Never send with channel `PEPPOL` — it answers 501; read `recommendedChannel: PEPPOL` as `EMAIL`.
3. Never wait for `DELIVERED` — `SENT` is terminal for `E_INVOICE`; only `EMAIL` reaches `SEEN`.
4. Never expect webhooks — receiving is `since`-cursor polling; dedupe by `id`.
5. Never poll faster than every 2 seconds; always honor `Retry-After`.
6. Never send an invoice twice — a second send sends it again. Retry a timed-out send only with the same `Idempotency-Key`.
7. Never replace Bilnex's `totalAmount` with your own sum — the server computes the totals of the document it issues. The response has no lines or VAT breakdown; keep your own.
8. Never skip `mark-exported` after an ERP import — idempotent and mandatory.
9. Never auto-retry 429 `email_rate_limited` or 503 `feature_disabled` — a company that keeps reaching the e-mail limits is suspended.
10. Never put the API key in frontend/mobile code or the repo — server-side config only.
11. Never use `blx_sbx_` on `api.app.bilnex.io` or `blx_live_` on `sandbox.bilnex.io`.
12. Never call `/partner/v1/sandbox/*` from production code paths — gate behind the env flag.

## References

- [references/flows.md](references/flows.md) — outbound flows end-to-end with real responses: capability branching, the invoice object, `E_INVOICE` and `EMAIL` sends, e-mail limits, refusals and the rejector drill, Peppol (not available).
- [references/receiving.md](references/receiving.md) — inbound importer pattern: statuses, cursor polling + dedupe + pagination, `/original`, mark-exported idempotency, Peppol and digitized documents, sandbox triggers, reference poller pseudocode.
- [references/errors-and-limits.md](references/errors-and-limits.md) — error envelope, complete code table with retryability, 429 + `Retry-After`, limits/usage, e-mail limits, sandbox expiry, claim and reset semantics.
- [references/go-live.md](references/go-live.md) — production opened per integration on request, customer-generated keys, real channel activation, sandbox-vs-production differences, smoke test, key replacement (condensed from the full [go-live docs](https://sandbox.bilnex.io/docs/go-live.md)).
- [references/estonia-einvoicing-context.md](references/estonia-einvoicing-context.md) — operator-channel e-invoices, the registry, Peppol `0191:<regCode>`, reg codes, VAT, IBANs.
