{"openapi":"3.1.0","info":{"title":"Scribo Invoice API","version":"1.0.0","description":"Compliant e-invoice generation. **Phase 1 supports Germany (XRechnung / ZUGFeRD) and the United States (plain PDF) only** — any other sender jurisdiction is rejected with `unsupported_jurisdiction`. The `/api/v1/*` namespace served from `scribo.causaprima.ai` is the public surface — anonymous and rate-limited. The internal `/internal/scribo/*` namespace is used by the Causa Prima web app and MCP server and is not part of this contract.","contact":{"name":"Causa Prima","url":"https://causaprima.ai"}},"servers":[{"url":"https://scribo.causaprima.ai","description":"Production"},{"url":"http://localhost:3200","description":"Local development (scribo Next.js proxy)"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Optional API key for elevated quotas. Unauthenticated requests are accepted and rate-limited per IP."}},"schemas":{"CreateInvoiceInput":{"type":"object","properties":{"sender":{"type":"object","properties":{"legal_name":{"type":"string","minLength":1,"maxLength":200},"country_code":{"type":"string","pattern":"^[A-Z]{2}$"},"address_line1":{"type":"string","minLength":1,"maxLength":200},"address_line2":{"type":"string","maxLength":200},"postcode":{"type":"string","minLength":1,"maxLength":20},"city":{"type":"string","minLength":1,"maxLength":100},"tax_id":{"type":["string","null"],"minLength":1,"maxLength":40},"contact_email":{"type":"string","format":"email"},"contact_phone":{"type":["string","null"],"minLength":3,"maxLength":40},"contact_name":{"type":["string","null"],"minLength":1,"maxLength":200},"tax_registration_id":{"type":["string","null"],"minLength":1,"maxLength":50}},"required":["legal_name","country_code","address_line1","postcode","city","contact_email"]},"recipient":{"type":"object","properties":{"legal_name":{"type":"string","minLength":1,"maxLength":200},"country_code":{"type":"string","pattern":"^[A-Z]{2}$"},"address_line1":{"type":"string","minLength":1,"maxLength":200},"address_line2":{"type":"string","maxLength":200},"postcode":{"type":"string","minLength":1,"maxLength":20},"city":{"type":"string","minLength":1,"maxLength":100},"tax_id":{"type":["string","null"],"minLength":1,"maxLength":40},"contact_email":{"type":"string","format":"email"},"leitweg_id":{"type":["string","null"],"minLength":1,"maxLength":50}},"required":["legal_name","country_code","address_line1","postcode","city","contact_email"]},"line_items":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":500},"quantity":{"type":"string","pattern":"^(?:[1-9][0-9]*|0\\.[0-9]*[1-9][0-9]*|[1-9][0-9]*\\.[0-9]+)$"},"unit_code":{"type":["string","null"],"minLength":1,"maxLength":10,"default":"EA"},"unit_price":{"type":"string","pattern":"^(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?$"},"tax_rate":{"type":"string","pattern":"^(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?$"},"tax_category_code":{"type":"string","enum":["S","Z","E","AE","K","G","O"]},"discount":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["amount"]},"value":{"type":"string","pattern":"^(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?$"},"reason":{"type":"string","minLength":1,"maxLength":200}},"required":["type","value","reason"]},{"type":"object","properties":{"type":{"type":"string","enum":["percent"]},"value":{"type":"string","pattern":"^(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?$"},"reason":{"type":"string","minLength":1,"maxLength":200}},"required":["type","value","reason"]}]},"tax_exemption_code":{"type":["string","null"],"pattern":"^VATEX-(EU|[A-Z]{2})-[A-Z0-9-]+$"},"tax_exemption_reason":{"type":["string","null"],"minLength":1,"maxLength":1000}},"required":["description","quantity","unit_price","tax_rate","tax_category_code"]},"minItems":1,"maxItems":500},"currency":{"type":"string","pattern":"^[A-Z]{3}$"},"jurisdiction":{"type":["string","null"],"pattern":"^[A-Z]{2}$"},"format_override":{"type":"string","enum":["zugferd_comfort","zugferd_basic","xrechnung_cii","xrechnung_ubl","plain_pdf"]},"notes":{"type":"string","maxLength":1000},"invoice_number":{"type":["string","null"],"description":"Optional invoice number printed on the document (BT-1). Omit the field entirely to have the server assign one — do NOT send a placeholder like \"auto\", \"TBD\", or \"N/A\" (these are ignored and a real code is assigned). When present: 1-64 chars of letters, digits, space, or - / . # _."},"issue_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Optional ISO date (BT-2) — the invoice's issue date. Defaults to today when omitted."},"due_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"payment_terms":{"type":["string","null"],"minLength":1,"maxLength":200},"payment_means":{"type":"object","properties":{"type":{"type":"string","enum":["credit_transfer"]},"iban":{"type":["string","null"],"pattern":"^[A-Z]{2}\\d{2}[A-Z0-9]{11,30}$"},"bic":{"type":["string","null"],"pattern":"^[A-Z][A-Z0-9]{7}([A-Z0-9]{3})?$"},"account_number":{"type":["string","null"],"pattern":"^\\d{4,17}$"},"routing_number":{"type":["string","null"],"pattern":"^\\d{9}$"},"account_name":{"type":["string","null"],"minLength":1,"maxLength":200},"bank":{"type":["string","null"],"minLength":1,"maxLength":300}},"required":["type"],"description":"BG-16 payment instructions for a credit transfer. Provide EXACTLY ONE of: a SEPA `iban`, OR US domestic details (`account_number` + 9-digit ABA `routing_number`). An optional `bic` (SWIFT) may accompany either form — US accounts carry a SWIFT/BIC for inbound international wires. An optional free-text `bank` (beneficiary bank name + address) may also be included; it's shown on the invoice but not validated structurally. An object carrying no account details at all is treated as absent; supplying both account forms, or only half of the US pair, is rejected. XRechnung (German B2G) requires the `iban` form (BR-DE-1)."},"delivery_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"delivery_period":{"type":"object","properties":{"start":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"end":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["start","end"]}},"required":["sender","recipient","line_items","currency"]},"Invoice":{"type":"object","properties":{"invoice_id":{"type":"string","format":"uuid"},"document_id":{"type":"string","format":"uuid"},"format":{"type":"string","enum":["zugferd_comfort","zugferd_basic","xrechnung_cii","xrechnung_ubl","plain_pdf"]},"download_url":{"type":"string","format":"uri"},"download_url_expires_at":{"type":"string","format":"date-time"},"preview_url":{"type":"string","format":"uri"},"preview_url_expires_at":{"type":"string","format":"date-time"},"submission":{"type":"object","properties":{"status":{"type":"string","enum":["not_provided"]},"message":{"type":"string"},"manual_upload_hints":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"url":{"type":"string","format":"uri"},"leitweg_prefix":{"type":"string"}},"required":["label","url"]},"default":[]},"planned":{"type":"array","items":{"type":"string"},"default":[]}},"required":["status","message"]},"validator_summary":{"type":"object","properties":{"valid":{"type":"boolean"},"validator":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"rule":{"type":"string"},"message":{"type":"string"}},"required":["message"]},"default":[]}},"required":["valid","validator"]},"magic_link_sent":{"type":"boolean"},"recipient_email_sent":{"type":"boolean"}},"required":["invoice_id","document_id","format","download_url","download_url_expires_at","validator_summary","magic_link_sent"]},"ErrorEnvelope":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"invalid_input"},"message":{"type":"string"},"details":{}},"required":["code","message"]}},"required":["error"]},"RateLimitError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["rate_limited","tenant_invoice_quota_exceeded"],"example":"tenant_invoice_quota_exceeded"},"message":{"type":"string"},"retry_after_seconds":{"type":"integer","minimum":0},"reset_at":{"type":"string","format":"date-time"},"limit_code":{"type":"string"}},"required":["code","message","retry_after_seconds","reset_at","limit_code"]}},"required":["error"]},"TenantSoftBlockError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["tenant_soft_blocked"]},"message":{"type":"string"},"retry_after_seconds":{"type":"integer","minimum":0},"reset_at":{"type":"string","format":"date-time"},"limit_code":{"type":"string","enum":["tenant_hard_limit_hits_per_hour"]},"details":{"type":"object","properties":{"reason_code":{"type":"string","enum":["tenant_hard_limit_hits_per_hour"]},"auto_lift_at":{"type":"string","format":"date-time"}},"required":["reason_code","auto_lift_at"]}},"required":["code","message","retry_after_seconds","reset_at","limit_code","details"]}},"required":["error"]},"JurisdictionEntry":{"type":"object","properties":{"jurisdiction":{"type":"string","pattern":"^[A-Z]{2}$"},"formats":{"type":"array","items":{"type":"string","enum":["zugferd_comfort","zugferd_basic","xrechnung_cii","xrechnung_ubl","plain_pdf"]}},"default_format":{"type":"string","enum":["zugferd_comfort","zugferd_basic","xrechnung_cii","xrechnung_ubl","plain_pdf"]}},"required":["jurisdiction","formats","default_format"]},"RequestVerificationInput":{"type":"object","properties":{"email":{"type":"string","minLength":3,"maxLength":254,"format":"email"}},"required":["email"],"additionalProperties":false},"RedeemVerificationInput":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":64}},"required":["code"],"additionalProperties":false},"VerificationChallenge":{"type":"object","properties":{"challenge_id":{"type":"string","format":"uuid"},"expires_at":{"type":"string","format":"date-time"},"next_request_allowed_at":{"type":"string","format":"date-time"}},"required":["challenge_id","expires_at","next_request_allowed_at"]},"VerificationToken":{"type":"object","properties":{"verification_token":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}},"required":["verification_token","expires_at"]},"VerificationSnapshot":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","enum":["pending"]},"expires_at":{"type":"string"}},"required":["status","expires_at"]},{"type":"object","properties":{"status":{"type":"string","enum":["verified"]},"verification_token":{"type":"string"},"expires_at":{"type":"string"}},"required":["status","verification_token","expires_at"]},{"type":"object","properties":{"status":{"type":"string","enum":["expired"]}},"required":["status"]},{"type":"object","properties":{"status":{"type":"string","enum":["revoked"]}},"required":["status"]}]}},"parameters":{}},"paths":{"/api/v1/invoices":{"post":{"summary":"Generate a compliant e-invoice","description":"Anonymous endpoint. Rate-limited per source IP. Returns 201 with a download URL.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255,"description":"Client-supplied key. Replaying the same key with identical inputs returns the cached invoice; replaying with different inputs returns 422 idempotency_key_mismatch."},"required":false,"name":"Idempotency-Key","in":"header"},{"schema":{"type":"string","minLength":1,"description":"Email-ownership token from POST /api/v1/scribo/email-verifications/{id}/redeem. Required for the first invoice per sender email — without it (or a scribo session cookie) an unverified sender receives 401 email_verification_required."},"required":false,"name":"X-Email-Verification-Token","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvoiceInput"}}}},"responses":{"201":{"description":"Invoice generated and persisted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"400":{"description":"Invalid input or downstream validator rejection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Sender email not verified yet (email_verification_required) — run the email-verification flow and retry with X-Email-Verification-Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Verified email does not match sender.contact_email (verification_email_mismatch), or tenant is temporarily soft-blocked after repeated hard-limit hits (tenant_soft_blocked)","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ErrorEnvelope"},{"$ref":"#/components/schemas/TenantSoftBlockError"}]}}}},"422":{"description":"Idempotency-Key mismatch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the limit resets."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}}}}},"/api/v1/invoices/{id}":{"get":{"summary":"Fetch invoice metadata","description":"Returns the Invoice for a previously-generated invoice. Read path is wired alongside the scribo session cookie; cross-tenant probes return 404.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Invoice UUID"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Invoice metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"404":{"description":"Invoice not found or not visible to this caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/invoices/{id}/download":{"get":{"summary":"Download the invoice file","description":"Streams the generated PDF (or hybrid PDF/A-3 with embedded XML) bytes. Future revisions may 302 to a signed object-storage URL.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Invoice bytes","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Artifact not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/scribo/email-verifications":{"post":{"summary":"Request a sender email-ownership challenge","description":"Emails a 6-character verification code (and magic link) to the address. Always returns a uniform 202 — new, returning, throttled, and send-failed callers are indistinguishable (anti-enumeration). Sends are rate-limited per email (resend cooldown + hourly cap).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestVerificationInput"}}}},"responses":{"202":{"description":"Challenge accepted (uniform anti-enumeration shape)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationChallenge"}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/scribo/email-verifications/{id}/redeem":{"post":{"summary":"Redeem the emailed code for a verification token","description":"Exchanges the 6-character code for a verification_token, sent as X-Email-Verification-Token on POST /invoices. The token is reusable for ~30 minutes for the same sender email.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Challenge UUID"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedeemVerificationInput"}}}},"responses":{"200":{"description":"Code accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationToken"}}}},"400":{"description":"Wrong, expired, or revoked code — uniform verification_invalid across all three causes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/scribo/email-verifications/{id}":{"get":{"summary":"Verification challenge status snapshot","description":"Polling fallback for browser flows waiting on the emailed magic link. verification_token is present only when status is `verified`.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Challenge UUID"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Challenge status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationSnapshot"}}}}}}},"/api/v1/scribo/email-verifications/{id}/events":{"get":{"summary":"Verification challenge event stream","description":"Server-Sent Events stream of challenge status transitions; closes after `verified`. Browser flows use this instead of polling the snapshot endpoint.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Challenge UUID"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"SSE stream","content":{"text/event-stream":{"schema":{"type":"string"}}}}}}},"/api/v1/jurisdictions":{"get":{"summary":"List supported jurisdictions and formats","responses":{"200":{"description":"Supported jurisdictions","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/JurisdictionEntry"}}}}}}}}},"webhooks":{}}