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.

curl
# 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+ (fetch)
// 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.

GET/api/public/v1/invoicesINVOICES

List invoices

Returns mandant-scoped invoices in receipt order (newest first). Cursor-paginated.

ParamInTypeNotes
cursorquerystringOpaque cursor from a previous response's `next_cursor`.
limitqueryinteger
statusqueryvalid | invalid | pdf_only
workflow_statusquerynew | reviewed | approved | paused | rejected | in_progress | exported | paid
supplier_idquerystring<uuid>
fromquerystring<date-time>
toquerystring<date-time>
has_iban_changequerybooleanFilter to invoices with a flagged IBAN change.

200OK

POST/api/public/v1/invoicesINVOICES

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

ParamInTypeNotes
Idempotency-Key*headerstringStable 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.

200OK

GET/api/public/v1/invoices/{id}INVOICES

Get a single invoice

ParamInTypeNotes
id*pathstring<uuid>
includequerystringComma-separated extras. `pdf_url` adds a 5-minute signed PDF URL.

200OK

PATCH/api/public/v1/invoices/{id}INVOICES

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

ParamInTypeNotes
id*pathstring<uuid>
Idempotency-Key*headerstringStable 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.

200Updated invoice.

POST/api/public/v1/mcpMCP

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

200JSON-RPC result for `tools/list` or `tools/call`.

GET/api/public/v1/suppliersSUPPLIERS

List suppliers

Returns mandant-scoped suppliers in name-ascending order. Cursor-paginated.

ParamInTypeNotes
cursorquerystring
limitqueryinteger
has_open_invoicesqueryboolean
vat_idquerystring

200OK

Code samples

Minimal calls to list invoices:

Python
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"])
curl (upload with idempotency)
# 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"
curl (workflow decision)
# 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.created
  • invoice.validated
  • invoice.flagged
  • invoice.workflow_status_changed
  • invoice.exported
  • supplier.created
  • supplier.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.

invoice.created
{
  "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"
  }
}
supplier.iban_changed
{
  "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):

JavaScript / Node
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'),
  );
}
Python
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:

PlanRequests / minuteRequests / hourWebhook subscription cap
Starter305001
Standard6010003
Premium120300010
Platinum3001000025
Kanzlei3001000025

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.

HTTPCodeDescription
400INVALID_INPUTParameters or JSON body are invalid.
401AUTH_UNAUTHORIZEDAPI key is missing, invalid, or revoked.
403AUTH_FORBIDDENThe API key does not have the required permission for this resource.
404NOT_FOUNDThe resource does not exist in the current mandant or is not visible.
429rate_limit_exceededTier-aware rate limit exceeded. Retry after a short delay.
503rate_limit_backend_unavailableRate-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)

This website uses cookies to improve your experience and analyze traffic. You can decline individual categories at any time. Privacy Policy