Following system theme

#Scribo HTTP API

The /api/v1/* namespace is the public Scribo contract — anonymous and rate-limited. The OpenAPI 3.1 spec is canonical and lives at /api/v1/openapi.json.

Base URL (production): https://scribo.causaprima.ai
Base URL (local dev):  http://localhost:3200

The internal /internal/scribo/* namespace is used only by the Causa Prima web app and the MCP server. It is not part of this contract — do not depend on it.

#Authentication

The /api/v1/* API is anonymous by default. Optional bearer tokens unlock elevated quotas for partners:

Authorization: Bearer <api-key>

Cloudflare Turnstile is enforced only on the Scribo web chat before its first LLM call. Public API callers do not send a CAPTCHA token.

#Email verification

The first invoice for each sender email must prove address ownership (one-time per sender). A tokenless POST /api/v1/invoices returns 401 email_verification_required. Flow:

  1. POST /api/v1/scribo/email-verifications with { "email": "<sender.contact_email>" } — Scribo emails a 6-character code. Always returns a uniform 202 with { challenge_id, expires_at, next_request_allowed_at } (anti-enumeration). The code expires in 15 minutes; sends are rate-limited (1 per 30 s, 5 per hour per email).
  2. POST /api/v1/scribo/email-verifications/{challenge_id}/redeem with { "code": "..." } — returns { verification_token, expires_at }. A wrong, expired, or revoked code returns 400 verification_invalid.
  3. Retry POST /api/v1/invoices with the X-Email-Verification-Token header. One token covers ~30 minutes of invoices for the same sender. A token whose email doesn't match sender.contact_email returns 403 verification_email_mismatch.

GET …/{id} (snapshot) and GET …/{id}/events (SSE) support browser flows that wait for the user to click the emailed magic link instead of typing the code.

#Endpoints

Method Path Description
POST /api/v1/invoices Generate a compliant e-invoice.
GET /api/v1/invoices/{id}/download Stream the invoice file bytes.
GET /api/v1/jurisdictions List supported jurisdictions and formats.
GET /api/v1/openapi.json Canonical OpenAPI 3.1 document.
POST /api/v1/scribo/email-verifications Request a sender email-ownership challenge.
POST /api/v1/scribo/email-verifications/{id}/redeem Exchange the 6-character code for a verification_token.
GET /api/v1/scribo/email-verifications/{id} Challenge status snapshot (polling fallback).
GET /api/v1/scribo/email-verifications/{id}/events SSE stream of challenge events.

NoteGET /api/v1/invoices/{id} (metadata re-fetch) is not yet wired in Phase 1 and returns 404 not_found. The download_url returned by POST /api/v1/invoices is durable; the canonical way to re-download is to use the download_url from the create response as-is — don't construct your own. Today that URL points at /i/{id}/download on the scribo domain (a thin proxy in front of /api/v1/invoices/{id}/download on the core API), but the path layout is an internal detail that may change.

#POST /api/v1/invoices — generate an invoice

Anonymous endpoint. Rate-limited per source IP.

Headers

Header Required Notes
Content-Type: application/json yes
Idempotency-Key recommended Same key + same payload returns the original invoice.
Authorization: Bearer <key> no Partner key for elevated quotas.
X-Email-Verification-Token first invoice per sender Token from the email verification redeem step. Omitted for an unverified sender → 401 email_verification_required.
X-Scribo-Locale no BCP-47 UI-language tag (e.g. de-DE, en-US). Sets the language of Scribo's outbound emails — both this endpoint's "invoice ready" notification and the email-verification message (whose confirmation page renders in the same language). Send the locale of the human you're acting for. Omitted or unsupported → English. UI-language only; it does not affect invoice content, currency, or jurisdiction.

Request bodyCreateInvoiceInput

Required fields:

  • sender{ legal_name, country_code, address_line1, postcode, city, contact_email }; optional tax_id, contact_phone, contact_name, address_line2. country_code is checked against the ISO 3166-1 alpha-2 list — reserved / unassigned codes (XX, ZZ, QQ) are rejected.
  • recipient — same shape as sender; contact_email is required (the accounts-payable address Scribo's transparency / opt-out channel uses); optional tax_id, leitweg_id (present auto-selects XRechnung UBL).
  • line_items[]{ description, quantity, unit_price, tax_rate, tax_category_code }. Optional unit_code (UN/ECE Rec 20, default EA), discount ({ type: 'percent' | 'amount', value, reason }reason is mandatory per EN 16931 BR-41), tax_exemption_code (VATEX-EU-*, required for tax_category_code = E), and tax_exemption_reason (free-form BT-120 note for the buyer). 1 to 500 items. tax_rate is bounded [0, 100]; quantity bounded at 999_999.999. Categories Z / E / AE / K / G / O must have tax_rate = "0".
  • currency — ISO 4217 (e.g. EUR, USD).

Optional:

  • jurisdiction — ISO 3166-1 alpha-2 override. Phase 1: only DE or US resolve successfully; everything else returns unsupported_jurisdiction.
  • format_override — one of zugferd_comfort, zugferd_basic, xrechnung_cii, xrechnung_ubl, plain_pdf. Phase 2 formats (factur_x, facturae, peppol_bis_ubl) are not yet accepted at the public boundary.
  • payment_means{ type: "credit_transfer", … } carrying either a SEPA iban or US domestic details (account_number 4–17 digits + 9-digit ABA routing_number) — not both. Optional on either path: bic (SWIFT), account_name (holder), bank (free-text beneficiary bank name + address). Mandatory with an IBAN when the resolved format is XRechnung (Leitweg-ID present, or format_override ∈ {xrechnung_cii, xrechnung_ubl}) — BR-DE-1; a US account cannot satisfy German B2G. Optional on every other format; ZUGFeRD embeds the IBAN, US plain PDFs render the payment details when supplied.
  • notes (≤ 1000 chars), due_date, payment_terms, delivery_date, delivery_period.

Numeric fields (quantity, unit_price, tax_rate) are strings holding decimal numbers — this preserves precision through JSON. The API rejects floats.

Example — already-verified sender. On the first invoice for a sender email, also send X-Email-Verification-Token (see Email verification) or the call returns 401 email_verification_required.

POST /api/v1/invoices HTTP/1.1
Host: scribo.causaprima.ai
Content-Type: application/json
Idempotency-Key: 0a3f2b4c-...
{
  "sender": {
    "legal_name": "Example GmbH",
    "country_code": "DE",
    "address_line1": "Example Allee 1",
    "postcode": "10115",
    "city": "Berlin",
    "tax_id": "DE123456788",
    "contact_email": "billing@example.com"
  },
  "recipient": {
    "legal_name": "Acme GmbH",
    "country_code": "DE",
    "address_line1": "Hauptstrasse 1",
    "postcode": "10117",
    "city": "Berlin",
    "tax_id": "DE987654321",
    "contact_email": "ap@acme.example"
  },
  "line_items": [
    {
      "description": "Senior consulting, 3 days",
      "quantity": "3",
      "unit_code": "DAY",
      "unit_price": "1200.00",
      "tax_rate": "19",
      "tax_category_code": "S"
    }
  ],
  "currency": "EUR",
  "due_date": "2026-06-01"
}

Response 201InvoiceRecord

{
  "invoice_id": "f0b3c1e2-...",
  "document_id": "9a7e1d2c-...",
  "format": "zugferd_comfort",
  "download_url": "https://scribo.causaprima.ai/i/f0b3c1e2-.../download",
  "download_url_expires_at": "2026-05-18T14:15:00Z",
  "validator_summary": { "valid": true, "validator": "invopop", "errors": [] },
  "magic_link_sent": true
}

The download_url is durable — links remain accessible and you can re-download from any device at any time. (download_url_expires_at is reserved for future use and currently emits a far-future timestamp; do not key business logic off it.) The magic_link_sent flag indicates whether Scribo emailed the invoice notification (summary + download link) to sender.contact_emailtrue when the email was queued, false if it was throttled or the send failed. (Email ownership is proven earlier in the flow via the verification challenge, not by this notification.)

XRechnung outputs additionally return preview_url + preview_url_expires_at (signed URL to a human-readable PDF rendering of the XML — the XML at download_url is the legal artifact) and a submission object (status, message, manual_upload_hints for the federal procurement portals; automated B2G submission is not implemented in Phase 1). Both are absent on ZUGFeRD and plain PDF.

Response codes

Status Meaning See
201 Invoice generated and persisted.
400 Invalid input or downstream validator rejection. validator_failed, unsupported_jurisdiction
401 Sender email not verified yet. email_verification_required
403 Verified email ≠ sender email, or tenant soft-blocked. verification_email_mismatch, tenant_soft_blocked
422 Idempotency-Key mismatch. idempotency_key_mismatch
429 Rate limit or tenant quota exceeded. rate_limited, tenant_invoice_quota_exceeded

#GET /api/v1/invoices/{id}/download — download bytes

Streams the generated PDF (or hybrid PDF/A-3 with embedded XML). Future revisions may 302 to a signed object-storage URL. Visibility is wired alongside the magic-link session cookie — cross-tenant probes (any caller without a matching session) return 404 (invoice_not_found / artifact_not_found).

#GET /api/v1/invoices/{id} — fetch metadata (deferred)

A metadata re-fetch endpoint that returns the InvoiceRecord with a freshly-signed download_url is reserved for a future revision; the current Phase 1 implementation returns 404 not_found and points callers at the durable download_url returned by POST /api/v1/invoices. The 200 shape stays compatible when it lands.

#GET /api/v1/jurisdictions — list supported jurisdictions

Returns an array of { jurisdiction, formats[], default_format }. Useful to confirm support before collecting recipient details.

#Error envelope

Every non-2xx response shares the same shape:

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded",
    "details": { "...": "..." }
  }
}

See troubleshooting for every error code Scribo emits, when it fires, and how to recover.

#Rate Limits

  • 600 requests per minute per source IP — cheap flood gate before validation.
  • 200 invoice-generation requests per hour per source IP.
  • 5 invoice-generation requests per minute per sender tenant.
  • 50 invoices per 24 hours per sender tenant.

On 429 the response body includes retry_after_seconds, reset_at, and limit_code (ip_requests_per_min, ip_invoices_per_hour, tenant_burst_per_min, or tenant_invoices_per_24h) so callers can back off correctly.

After 3 tenant hard-limit hits in 1 hour, invoice issuing for that sender is temporarily soft-blocked for 24 hours. Soft-blocked requests return 403 tenant_soft_blocked with retry_after_seconds, reset_at, and details.auto_lift_at. The shared anonymous LLM tenant is excluded from tenant soft-blocking; anonymous traffic is governed by IP limits, Turnstile, and LLM spend caps instead.

#Format selection

The output format is picked from a deterministic priority chain. See jurisdictions for the full table and the format_override enum.

#OpenAPI

The full OpenAPI 3.1 document is served at /api/v1/openapi.json. Generate clients with openapi-typescript, oapi-codegen, or any tool that consumes OpenAPI 3.1.

#See also

  • CLI reference — the same API wrapped in a commander-based npm CLI.
  • MCP server — the same API exposed as MCP tools for LLM clients.
  • Skillcurl-based helper scripts for Claude Code and OpenAI Codex.