Public REST API v1
API documentation
Read-only mandant-scoped REST API for e-invoice data. Stable URL, cursor pagination, signed webhooks.
Quickstart
Issue an API key from your workspace settings, then call any endpoint with an Authorization header.
# 1. Issue an API key in workspace settings → API keys.
# 2. Export it locally so curl can read it:
export ERI_API_KEY='eri_live_…'
# 3. List the 10 most recent invoices:
curl -s 'https://www.e-rechnung-inbox.de/api/public/v1/invoices?limit=10' \
-H "Authorization: Bearer $ERI_API_KEY"
// Node 18+ (built-in fetch). No SDK required.
const res = await fetch(
'https://www.e-rechnung-inbox.de/api/public/v1/invoices?limit=10',
{ headers: { Authorization: `Bearer ${process.env.ERI_API_KEY}` } },
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { data, next_cursor } = await res.json();
console.log(data.length, 'invoices,', next_cursor ? 'more available' : 'end of list');
All endpoints are read-only and mandant-scoped. The list below uses /invoices but the same pattern works for /invoices/:id and /suppliers.
Authentication
Every request to /api/public/v1/* requires an API key — either the Authorization Bearer header or the X-API-Key header. The two are equivalent.
Authorization: Bearer eri_live_…X-API-Key: eri_live_…
Issue keys from the workspace settings under "API keys". Keys are mandant-scoped — a tax advisor with access to multiple mandants creates one key per mandant.
Plaintext keys are shown exactly once at creation. Store them in a secrets vault.
Endpoints
Full OpenAPI 3.1 spec: /api/public/v1/openapi.json.
/api/public/v1/invoicesINVOICESList invoices
Returns mandant-scoped invoices in receipt order (newest first). Cursor-paginated.
| Param | In | Type | Notes |
|---|---|---|---|
| cursor | query | string | Opaque cursor from a previous response's `next_cursor`. |
| limit | query | integer | |
| status | query | valid | invalid | pdf_only | |
| workflow_status | query | new | reviewed | approved | paused | rejected | in_progress | exported | paid | |
| supplier_id | query | string<uuid> | |
| from | query | string<date-time> | |
| to | query | string<date-time> | |
| has_iban_change | query | boolean | Filter to invoices with a flagged IBAN change. |
200 — OK
/api/public/v1/invoicesINVOICESUpload an invoice
Accepts a PDF/XML invoice file for asynchronous processing. Requires an API key with write scope, an `Idempotency-Key` header, and that public API write endpoints are enabled for the workspace/environment. CSV+ZIP export remains the supported fallback for downstream accounting.
| Param | In | Type | Notes |
|---|---|---|---|
| Idempotency-Key* | header | string | Stable client-generated retry key for this write. Reuse only for the same method, route, and request body/file; use a new key for any different upload or workflow decision. The service stores only a scoped SHA-256 reservation key, never the raw header value. |
200 — OK
/api/public/v1/invoices/{id}INVOICESGet a single invoice
| Param | In | Type | Notes |
|---|---|---|---|
| id* | path | string<uuid> | |
| include | query | string | Comma-separated extras. `pdf_url` adds a 5-minute signed PDF URL. |
200 — OK
/api/public/v1/invoices/{id}INVOICESUpdate invoice workflow status
Narrow write endpoint for workflow_status changes only. Requires an API key with `write` scope and an `Idempotency-Key` header; all writes are mandant-scoped and audit-stamped. Optional decision_note values are normalized, redacted, and stored as bounded audit evidence.
| Param | In | Type | Notes |
|---|---|---|---|
| id* | path | string<uuid> | |
| Idempotency-Key* | header | string | Stable client-generated retry key for this write. Reuse only for the same method, route, and request body/file; use a new key for any different upload or workflow decision. The service stores only a scoped SHA-256 reservation key, never the raw header value. |
200 — Updated invoice.
/api/public/v1/mcpMCPInvoke Hosted MCP tools
Hosted MCP endpoint using JSON-RPC 2.0 over `application/json`. Supports `tools/list` for discovery and `tools/call` for the `validate_xrechnung` and `parse_xrechnung` tools. Requires a Public API key and shares Public API rate limits and payload-size limits.
200 — JSON-RPC result for `tools/list` or `tools/call`.
/api/public/v1/suppliersSUPPLIERSList suppliers
Returns mandant-scoped suppliers in name-ascending order. Cursor-paginated.
| Param | In | Type | Notes |
|---|---|---|---|
| cursor | query | string | |
| limit | query | integer | |
| has_open_invoices | query | boolean | |
| vat_id | query | string |
200 — OK
Code samples
Minimal calls to list invoices:
import os, requests
res = requests.get(
"https://www.e-rechnung-inbox.de/api/public/v1/invoices",
params={"limit": 10},
headers={"Authorization": f"Bearer {os.environ['ERI_API_KEY']}"},
timeout=10,
)
res.raise_for_status()
print(res.json()["data"])
# Requires a write-scope API key. Reuse the same Idempotency-Key for retries
# of the same upload; use a new key for a different file/body.
export ERI_IDEMPOTENCY_KEY='upload-RE-2026-0142-001'
curl -sS -X POST 'https://www.e-rechnung-inbox.de/api/public/v1/invoices' \
-H "Authorization: Bearer $ERI_API_KEY" \
-H "Idempotency-Key: $ERI_IDEMPOTENCY_KEY" \
-F "file=@./rechnung.xml;type=application/xml"
# Workflow writes are audit-stamped and require their own stable retry key.
export ERI_WORKFLOW_IDEMPOTENCY_KEY='workflow-RE-2026-0142-approve-001'
curl -sS -X PATCH 'https://www.e-rechnung-inbox.de/api/public/v1/invoices/$INVOICE_ID' \
-H "Authorization: Bearer $ERI_API_KEY" \
-H "Idempotency-Key: $ERI_WORKFLOW_IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
--data '{"workflow_status":"approved","decision_note":"Approved after supplier and IBAN check"}'
Hosted MCP
Use the Public API key with Claude Desktop, Cursor, or Smithery via the hosted MCP endpoint. How to connect.
Webhooks
Create a subscription via the workspace settings. We POST a signed JSON payload over HTTPS. Supported event types:
invoice.createdinvoice.validatedinvoice.flaggedinvoice.workflow_status_changedinvoice.exportedsupplier.createdsupplier.iban_changed
Payload examples
Every event uses the same envelope: id, type, created_at, and data. Fields inside data depend on the event; sensitive bank data is minimized.
{
"id": "evt_01HV0CK7M5W9XGZN2Y9EGZ7T9R",
"type": "invoice.created",
"created_at": "2026-05-08T11:42:18.317Z",
"data": {
"invoice_id": "f3a3c5e0-3d3a-4f6e-9e9c-2d2dabec3d31",
"invoice_number": "RE-2026-0142",
"supplier": { "id": "8a2…", "name": "ACME GmbH" },
"gross_total": 1428.0,
"currency": "EUR"
}
}{
"id": "evt_01HV0CK7M5W9XGZN2Y9EGZ7T9S",
"type": "supplier.iban_changed",
"created_at": "2026-05-08T11:42:18.317Z",
"data": {
"supplier_id": "8a2…",
"supplier_name": "ACME GmbH",
"previous_iban_last4": "0145",
"new_iban_last4": "9921",
"first_seen_in_invoice_id": "f3a3c5e0-3d3a-4f6e-9e9c-2d2dabec3d31"
}
}Verify signatures
Signature verification (X-ERI-Signature header in the format t=UNIX_SECONDS,v1=HMAC_HEX):
import crypto from 'node:crypto';
export function verifyEriWebhook(payload, signatureHeader, signingSecret) {
// signatureHeader format: "t=<unix>,v1=<hex>"
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('=')),
);
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac('sha256', signingSecret)
.update(`${t}.${payload}`)
.digest('hex');
const actual = parts.v1 ?? '';
return actual.length === expected.length && crypto.timingSafeEqual(
Buffer.from(expected, 'utf8'),
Buffer.from(actual, 'utf8'),
);
}
import hashlib, hmac, time
def verify_eri_webhook(payload: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(int(time.time()) - t) > 300:
return False
signed = f"{t}.{payload.decode('utf-8')}"
expected = hmac.new(secret.encode("utf-8"), signed.encode("utf-8"),
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Retries and auto-disable
On failure we back off 1 min, 5 min, 30 min, 2 h, 12 h — max 5 attempts. After 5 consecutive failures the subscription auto-disables.
- Failed deliveries are retried with exponential backoff: 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours.
- After 5 consecutive failed deliveries, the subscription is disabled automatically.
- After auto-disable: fix the endpoint, create a new subscription, and store the new signing secret safely.
Rate limits (tier-aware)
Per API key, two buckets are checked (minute + hour). Limits depend on your plan:
| Plan | Requests / minute | Requests / hour | Webhook subscription cap |
|---|---|---|---|
| Starter | 30 | 500 | 1 |
| Standard | 60 | 1000 | 3 |
| Premium | 120 | 3000 | 10 |
| Platinum | 300 | 10000 | 25 |
| Kanzlei | 300 | 10000 | 25 |
If the rate-limit backend is unavailable, the API responds with HTTP 503 + Retry-After (fail-closed). Tier-lookup failure falls back to the Starter limit.
Successful responses include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (unix timestamp), and X-RateLimit-Tier (active plan).
Errors
Error responses are JSON and include a stable code plus a human-readable message.
| HTTP | Code | Description |
|---|---|---|
| 400 | INVALID_INPUT | Parameters or JSON body are invalid. |
| 401 | AUTH_UNAUTHORIZED | API key is missing, invalid, or revoked. |
| 403 | AUTH_FORBIDDEN | The API key does not have the required permission for this resource. |
| 404 | NOT_FOUND | The resource does not exist in the current mandant or is not visible. |
| 429 | rate_limit_exceeded | Tier-aware rate limit exceeded. Retry after a short delay. |
| 503 | rate_limit_backend_unavailable | Rate-limit backend unavailable; the API fails closed. |
Typical shape: JSON with error, message, and request_id fields.
Changelog
Public API changes are documented here by version.
2026-05-08
v1: read-only endpoints for invoices and suppliers, mandant-scoped API keys, and signed webhooks.
Not in v1
These features are intentionally outside v1:
- Write endpoints (POST/PATCH/DELETE)
- Tax advisor cross-mandant access (planned for v2 once partner demand surfaces)
- Metered billing (ships with the hosted MCP commercial tier)
- Separate sandbox environment (the eri_test_ prefix is reserved)