#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:
POST /api/v1/scribo/email-verificationswith{ "email": "<sender.contact_email>" }— Scribo emails a 6-character code. Always returns a uniform202with{ 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).POST /api/v1/scribo/email-verifications/{challenge_id}/redeemwith{ "code": "..." }— returns{ verification_token, expires_at }. A wrong, expired, or revoked code returns400 verification_invalid.- Retry
POST /api/v1/invoiceswith theX-Email-Verification-Tokenheader. One token covers ~30 minutes of invoices for the same sender. A token whose email doesn't matchsender.contact_emailreturns403 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. |
Note —
GET /api/v1/invoices/{id}(metadata re-fetch) is not yet wired in Phase 1 and returns404 not_found. Thedownload_urlreturned byPOST /api/v1/invoicesis durable; the canonical way to re-download is to use thedownload_urlfrom the create response as-is — don't construct your own. Today that URL points at/i/{id}/downloadon the scribo domain (a thin proxy in front of/api/v1/invoices/{id}/downloadon 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 body — CreateInvoiceInput
Required fields:
sender—{ legal_name, country_code, address_line1, postcode, city, contact_email }; optionaltax_id,contact_phone,contact_name,address_line2.country_codeis checked against the ISO 3166-1 alpha-2 list — reserved / unassigned codes (XX,ZZ,QQ) are rejected.recipient— same shape as sender;contact_emailis required (the accounts-payable address Scribo's transparency / opt-out channel uses); optionaltax_id,leitweg_id(present auto-selects XRechnung UBL).line_items[]—{ description, quantity, unit_price, tax_rate, tax_category_code }. Optionalunit_code(UN/ECE Rec 20, defaultEA),discount({ type: 'percent' | 'amount', value, reason }—reasonis mandatory per EN 16931 BR-41),tax_exemption_code(VATEX-EU-*, required fortax_category_code = E), andtax_exemption_reason(free-form BT-120 note for the buyer). 1 to 500 items.tax_rateis bounded[0, 100];quantitybounded at999_999.999. CategoriesZ/E/AE/K/G/Omust havetax_rate = "0".currency— ISO 4217 (e.g.EUR,USD).
Optional:
jurisdiction— ISO 3166-1 alpha-2 override. Phase 1: onlyDEorUSresolve successfully; everything else returnsunsupported_jurisdiction.format_override— one ofzugferd_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 SEPAibanor US domestic details (account_number4–17 digits + 9-digit ABArouting_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, orformat_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 201 — InvoiceRecord
{
"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_email — true 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.
- Skill —
curl-based helper scripts for Claude Code and OpenAI Codex.