{"openapi":"3.1.0","info":{"title":"Lucanto API","description":"The Lucanto API gives you programmatic access to invoices, expenses,\nclients, and the rest of your workspace data. Use it to integrate\nwith your e-shop (Shoptet, WooCommerce), accounting software\n(Pohoda, Money S3), CRM, or custom automation.\n\nAuthentication is via Bearer token in the `Authorization` header.\nTwo token formats are supported:\n\n- **Personal access tokens** (`lct_pat_*`): user-scoped, span every\n  workspace the user has access to. Use these for personal scripts,\n  Claude Desktop, Cursor, ChatGPT, and other MCP clients.\n- **Workspace tokens** (`lct_live_*` / `lct_test_*`): account-scoped,\n  bound to a single workspace. Use these for integrations like\n  e-shops or accounting tools that act as the workspace itself.\n\nAll endpoints are scoped to a workspace via the `:account_id` URL\npath. For workspace tokens, the account is implicit in the token\nand the path account_id must match — mismatches return 403.\n\n## Idempotency\n\nAll POST endpoints accept an optional `Idempotency-Key` request\nheader (max 255 chars). Set it to a unique value (UUID v4 is\nrecommended) when retrying a request after a network error, so\nthe server can recognize the retry and return the cached response\nfrom the original call instead of creating a duplicate record.\n\n- Cache window: 24 hours per (API key, key) pair.\n- The cached response is replayed byte-for-byte. The\n  `Idempotent-Replayed: true` response header lets you detect\n  a replay programmatically; `X-Original-Request-Id` exposes the\n  original call's request_id for log correlation.\n- Reusing the same key with a different request body returns\n  `409 idempotency_key_conflict` — generate a fresh key for\n  genuinely different requests.\n- GET, PATCH, and DELETE ignore the header (those methods are\n  naturally idempotent under HTTP semantics).\n","version":"1.0.0","contact":{"name":"Lucanto Support","url":"https://lucanto.eu/support","email":"support@lucanto.eu"},"license":{"name":"Lucanto API Terms","url":"https://lucanto.eu/terms/api"}},"servers":[{"url":"https://app.lucanto.eu/api/v1","description":"Production"}],"security":[{"JwtBearer":[]},{"ApiKeyBearer":[]}],"tags":[{"name":"Accounts","description":"Workspaces (accounts) the current user belongs to. The \"account\"\nin Lucanto is a tenant — each one has its own subscription,\ninvoices, expenses, and team members.\n"},{"name":"BankAccounts","description":"Bank accounts and cash registers — the same underlying model\nwith `account_type` distinguishing them. Bank accounts hold\nIBAN/SWIFT and may sync via PSD2; cash registers track manual\ncash movements (\"pokladňa\" in SK/CZ).\n"},{"name":"Contacts","description":"Workspace customer/partner records. Each contact points at a shared\nInvoiceAccount (legal entity) — multiple workspaces can reference\nthe same legal entity without each defining it independently.\n"},{"name":"CreditNotes","description":"Correction documents that reverse line items from a source\ninvoice. Use `invoice_id` in the create request to link the\ncredit note to its source — the `corrects` document link is\nestablished automatically, queryable later as\n`credit_note.invoice`.\n\nCredit notes don't represent a payment obligation\n(`display_payment_detail` is false on the underlying model);\nthey net out against the original invoice's outstanding amount\nvia the payment_allocations table.\n"},{"name":"Expenses","description":"Incoming source documents (invoices received, cash receipts, VPDs).\nEach expense optionally has an attached source file (PDF, image,\neKasa QR receipt) that AI extraction populates fields from.\n"},{"name":"Invoices","description":"Outgoing sales invoices the workspace issues to its customers.\nEach invoice references the customer via `purchaser_id` (an\nInvoiceAccount ID) and contains line items.\n"},{"name":"Proformas","description":"Pre-invoice documents that trigger customer payment in advance.\nThe typical lifecycle is: workspace issues proforma → customer\npays it → workspace converts to a final invoice via\n`POST /:id/convert_to_invoice`. The `derives_from` link graph\npreserves the relationship (`proforma.invoices` enumerates\ndownstream invoices issued from this proforma).\n"},{"name":"Quotes","description":"Pre-invoice price offers issued to customers. A quote becomes\nlegally binding when the customer accepts it via\n`POST /:id/accept`. Quotes can be converted to invoices (or\nproformas, sales orders, delivery notes) via the document\nlink graph — this association is exposed in the response\nunder the document's derived-documents links.\n"},{"name":"User","description":"The current authenticated user. Read-only \"who am I\" endpoint —\nuseful for PAT clients bootstrapping their session and for\nmobile/SPA clients confirming their identity after auth.\n"}],"paths":{"/user/accounts":{"get":{"operationId":"listAccounts","tags":["Accounts"],"summary":"List the current user's workspaces","description":"Returns the workspaces the authenticated user has access to.\nTypical response is 1–3 accounts; users almost never belong\nto more than a handful, so this endpoint is not paginated.\n\nThe response includes a `default` flag per account: clients\nbuilding selector UIs can pre-select the user's last-used\nworkspace.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Account"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/user/accounts/{account_id}":{"parameters":[{"$ref":"#/components/parameters/AccountId"}],"get":{"operationId":"getAccount","tags":["Accounts"],"summary":"Get a single workspace","description":"Returns the same shape as `listAccounts`, scoped to a single\nworkspace. Returns `404` (not `403`) when the user does not\nbelong to the requested workspace — this prevents leaking the\nexistence of accounts the caller can't see.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/user/accounts/{account_id}/set_default":{"parameters":[{"$ref":"#/components/parameters/AccountId"}],"patch":{"operationId":"setDefaultAccount","tags":["Accounts"],"summary":"Mark a workspace as the user's default","description":"Marks the given workspace as the user's default. Subsequent\nsessions opened in the web app land in this workspace without\nan explicit selector step. The change applies to the user\nglobally — not just the API session.\n\nIdempotent: calling on an already-default account succeeds and\nreturns the same account body.\n","responses":{"200":{"description":"OK. The updated account with `default: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/bank_accounts":{"get":{"operationId":"listBankAccounts","tags":["BankAccounts"],"summary":"List bank accounts and cash registers","description":"Returns a cursor-paginated list of all payment accounts\n(both `bank` and `cash` types) for the workspace.\nSoft-deleted accounts are excluded. Ordered by `id` descending.\n","parameters":[{"$ref":"#/components/parameters/AccountId"},{"$ref":"#/components/parameters/CursorParam"},{"$ref":"#/components/parameters/LimitParam"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BankAccountList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createBankAccount","tags":["BankAccounts"],"summary":"Create a bank account or cash register","description":"Creates either a bank account (with IBAN/account number) or a\ncash register. Two validation paths driven by the model's\n`account_type` enum (currently set via attribute, not via\nthe API surface — defaults to `bank`):\n\n- **Bank**: requires `iban` OR `account_number` (one of them).\n  `iban` is unique within the workspace if provided.\n- **Cash**: `name` is unique within the workspace's cash\n  registers.\n\nPer-workspace caps: bank accounts hit the `bank_accounts`\nfeature limit. Hitting the cap returns `402` with an\n`upgrade_url` pointing at the billing plans page.\n","parameters":[{"$ref":"#/components/parameters/AccountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BankAccountCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BankAccount"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/bank_accounts/{id}":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Bank account / cash register ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"getBankAccount","tags":["BankAccounts"],"summary":"Get a bank account or cash register","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BankAccount"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateBankAccount","tags":["BankAccounts"],"summary":"Update a bank account or cash register","description":"Partial update. Suspended workspaces return `403` with a\nsuspension message — re-subscribe to mutate accounts.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BankAccountUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BankAccount"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteBankAccount","tags":["BankAccounts"],"summary":"Soft-delete a bank account","description":"Marks the account as deleted via the `deleted` timestamp.\nBank accounts referenced by historical invoices/expenses\nremain queryable by ID — the soft-delete only hides them\nfrom list responses.\n","responses":{"204":{"description":"No Content"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/contacts":{"get":{"operationId":"listContacts","tags":["Contacts"],"summary":"List contacts","description":"Returns a cursor-paginated list of contacts. Soft-deleted\ncontacts are excluded. Ordered by `id` descending — newest\ncontacts appear first on the first page.\n\n**Pagination flow**: call without a `cursor` to start. The\nresponse includes `next_cursor` (opaque base64 string) and\n`has_more` (boolean). Pass `next_cursor` as the `cursor`\nquery parameter on the next request to fetch the following\npage. When `has_more` is `false`, stop iterating.\n\n**Search**: pass `q` to filter by name. Matches\ncase-insensitive substring against the contact's\nworkspace-local label AND the canonical legal-entity name\non the linked InvoiceAccount. Useful for type-ahead and\nautocomplete UIs.\n","parameters":[{"$ref":"#/components/parameters/AccountId"},{"$ref":"#/components/parameters/CursorParam"},{"$ref":"#/components/parameters/LimitParam"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on contact name\n(workspace-local label) and on the linked legal entity's\nregistered name. SQL wildcard characters in the query\nvalue are treated as literals, not patterns.\n","schema":{"type":"string"},"example":"Acme"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createContact","tags":["Contacts"],"summary":"Create a contact","description":"Creates a new contact in this workspace pointing at an existing\nInvoiceAccount (legal entity). The `invoice_account_id` in the\nrequest body is the InvoiceAccount ID.\n","parameters":[{"$ref":"#/components/parameters/AccountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/contacts/{id}":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Contact ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"getContact","tags":["Contacts"],"summary":"Get a contact","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateContact","tags":["Contacts"],"summary":"Update a contact","description":"Updates the editable fields of a contact. The contact cannot be\nupdated while it's in a suspended state — that returns 403\nwith a suspended-contact message.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteContact","tags":["Contacts"],"summary":"Soft-delete a contact","description":"Marks the contact as deleted (soft-delete via the `deleted`\ntimestamp). The contact remains in the database for audit and\nexisting-document referential integrity, but is excluded from\nall list responses.\n","responses":{"204":{"description":"No Content. Contact soft-deleted successfully."},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/credit_notes":{"get":{"operationId":"listCreditNotes","tags":["CreditNotes"],"summary":"List credit notes","description":"Returns a cursor-paginated list of credit notes for the\nworkspace. Only documents with `type=credit_note` appear here.\nSoft-deleted credit notes are excluded. Ordered by `id`\ndescending.\n","parameters":[{"$ref":"#/components/parameters/AccountId"},{"$ref":"#/components/parameters/CursorParam"},{"$ref":"#/components/parameters/LimitParam"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditNoteList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createCreditNote","tags":["CreditNotes"],"summary":"Create a credit note","description":"Creates a new credit note. The `type` field is set by the\nroute (never sent in the request body).\n\n**Linking to a source invoice**: pass `invoice_id` in the\nrequest body. The `corrects` document link is established\nautomatically by the DocumentBuilder; consumers can later\nquery `credit_note.invoice` to navigate back to the source.\n\nPass `purchaser_id` referring to an InvoiceAccount (legal\nentity), not a Contact. The workspace's own InvoiceAccount\nis used as the supplier automatically.\n","parameters":[{"$ref":"#/components/parameters/AccountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditNoteCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditNote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/credit_notes/{id}":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Credit note ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"getCreditNote","tags":["CreditNotes"],"summary":"Get a credit note","description":"Returns 404 if the ID belongs to a document of a different\ntype (e.g. an invoice ID) — cross-type isolation per the\nPhase 2a controller-architecture decision.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditNote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateCreditNote","tags":["CreditNotes"],"summary":"Update a credit note","description":"Partial update. The `type` field is intentionally NOT\nupdatable. Updating an issued credit note is allowed but\nrare — the document has already been accounted for as a\nVAT correction.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditNoteUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditNote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteCreditNote","tags":["CreditNotes"],"summary":"Soft-delete a credit note","description":"Marks the credit note as deleted via the `deleted` timestamp.\nIssued credit notes that have already been applied to a\nsource invoice's payment allocation may refuse deletion and\nreturn 422.\n","responses":{"204":{"description":"No Content"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/credit_notes/{id}/issue":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Credit note ID","schema":{"type":"integer","format":"int64","minimum":1}}],"post":{"operationId":"issueCreditNote","tags":["CreditNotes"],"summary":"Issue a credit note (transition draft → exposed)","description":"Marks a draft credit note as issued (`status` changes from\n`draft` to `exposed`). This is the moment the document\nbecomes legally binding for VAT correction purposes — same\nsemantics as invoice issuance.\n\nReturns 422 if the credit note is in any state other than\n`draft` (guards against double-issuance).\n\n**Required scope: `manage:credit_notes`** — issuance is a\nworkflow action distinct from `:update`. CanCanCan action\nsymbol is `:issue`.\n","responses":{"200":{"description":"OK. The issued credit note (with `status: exposed`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditNote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Credit note is not in `draft` status — only drafts can\nbe issued.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/expenses":{"get":{"operationId":"listExpenses","tags":["Expenses"],"summary":"List expenses","description":"Returns a cursor-paginated list of expenses. Soft-deleted\nexpenses are excluded. Ordered by `issue_date` descending,\nthen by `id` descending as a stable tiebreaker.\n\n**Pagination flow**: call without a `cursor` to start. The\nresponse includes `next_cursor` (opaque base64 string) and\n`has_more` (boolean). Pass `next_cursor` as the `cursor`\nquery parameter on the next request to fetch the following\npage. When `has_more` is `false`, stop iterating.\n","parameters":[{"$ref":"#/components/parameters/AccountId"},{"$ref":"#/components/parameters/CursorParam"},{"$ref":"#/components/parameters/LimitParam"},{"name":"search","in":"query","required":false,"description":"Full-text search across receipt identifiers, supplier name,\nand notes. Trim-folded; case-insensitive. When provided,\nresults are still ordered by `issue_date` descending.\n","schema":{"type":"string"}},{"name":"extraction_status","in":"query","required":false,"description":"Filter by extraction state. Accepts a single value or a\ncomma-separated list (e.g. `pending,processing`). Values\noutside the allowed enum are silently dropped.\n","schema":{"type":"string","example":"completed,manual"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpenseList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createExpense","tags":["Expenses"],"summary":"Create an expense","description":"Creates a new expense. Two common flows:\n\n- **Existing supplier**: pass `expense.supplier_id` referring to\n  a Contact in this workspace. Address fields can be omitted.\n- **Inline supplier creation**: pass the top-level `address`,\n  `unit`, and `invoice_account` objects to create a new Contact\n  and InvoiceAccount in a single request. Useful when receiving\n  an invoice from a brand-new supplier.\n\nLine items are nested under `expense.expense_items_attributes`.\nFile attachments (PDF/image of the receipt) are NOT supported\non this JSON endpoint — use `POST /accounts/{account_id}/expenses/upload`\nfor multipart uploads with AI extraction (Phase 2a.6).\n","parameters":[{"$ref":"#/components/parameters/AccountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpenseCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Expense"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/expenses/{id}":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Expense ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"getExpense","tags":["Expenses"],"summary":"Get an expense","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Expense"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateExpense","tags":["Expenses"],"summary":"Update an expense","description":"Partial update. Only fields supplied in the request body are\nmodified; omitted fields retain their current values. Same\nnested-supplier flow as create — pass `address`/`unit`/\n`invoice_account` to update the linked supplier inline.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpenseUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Expense"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteExpense","tags":["Expenses"],"summary":"Soft-delete an expense","description":"Marks the expense as deleted via the `deleted` timestamp. The\nexpense remains in the database for audit purposes but is\nexcluded from all list responses.\n","responses":{"204":{"description":"No Content. Expense soft-deleted successfully."},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/expenses/{id}/retry_extraction":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Expense ID","schema":{"type":"integer","format":"int64","minimum":1}}],"post":{"operationId":"retryExpenseExtraction","tags":["Expenses"],"summary":"Retry AI extraction for an expense","description":"Resets the expense's extraction state and enqueues a new\nextraction job (sends the attached source document to the\nInsight API for re-parsing). Use this when:\n\n- An extraction completed with incorrect data and you want\n  a fresh attempt (the previous `job_id` is preserved as\n  `rejected_job_id` so the Insight API escalates to a more\n  capable model on retry).\n- An extraction failed and the underlying issue has been\n  resolved (e.g. corrupted attachment replaced).\n\nReturns `422` when the expense isn't in a state that permits\nretry (no attachment present, or extraction recently completed\nwithin the cooldown window).\n\n**Required scope: `manage:expenses`** — retry is a workflow\naction distinct from `:update` so write-scoped PATs cannot\ntrigger AI extraction billing.\n","responses":{"200":{"description":"OK. The expense with extraction_status reset to `pending`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Expense"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Cannot retry extraction for this expense (no attachment,\nrecently completed, or cooldown active).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/expenses/{id}/file":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Expense ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"downloadExpenseFile","tags":["Expenses"],"summary":"Download the expense's source-document attachment","description":"Returns the source document attached to the expense (PDF,\nimage of a receipt, etc.) as a binary stream. The file's\noriginal MIME type is preserved in `Content-Type` — it could\nbe any of `application/pdf`, `image/jpeg`, `image/png`, or\n`image/gif`.\n\nDistinct from the invoice PDF endpoint: invoice PDFs are\nserver-rendered representations; expense files are the\n*original attachment* the user uploaded (or AI extracted\nfrom). No CPU-intensive rendering, so no special rate limit\nbeyond the general API throttle.\n\nReturns **`404`** when the expense exists but has no\nattached file (vs. an empty 200). Gives SDK consumers a\nclear branch for \"no file yet\" — useful when polling an\nexpense whose extraction is `pending` and the upload\ncompletes asynchronously.\n\nDisposition control:\n\n- Default: `Content-Disposition: attachment; filename=...`\n  (browser file download).\n- `?disposition=inline`: for embedded preview.\n\n**Required scope: `read:expenses`** — file access mirrors\nthe read permission on the expense record itself.\n","parameters":[{"name":"disposition","in":"query","required":false,"description":"Whether the browser should download the file\n(`attachment`, the default) or render it inline\n(`inline`). Values other than `inline` map to\n`attachment` server-side.\n","schema":{"type":"string","enum":["attachment","inline"],"default":"attachment"}}],"responses":{"200":{"description":"OK. The source-document bytes.","headers":{"Content-Type":{"schema":{"type":"string","enum":["application/pdf","image/jpeg","image/png","image/gif"]},"description":"The original MIME type of the uploaded attachment.\n"},"Content-Disposition":{"schema":{"type":"string"},"description":"`attachment; filename=\"...\"` (default) or\n`inline; filename=\"...\"` when `?disposition=inline`.\n"}},"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Either the expense doesn't exist OR it exists but has no\nattached file. Distinct from the standard \"resource not\nfound\" because the latter case has its own clear UX:\nthe expense record is still readable, but the file isn't\navailable (yet).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/expenses/upload":{"parameters":[{"$ref":"#/components/parameters/AccountId"}],"post":{"operationId":"uploadExpense","tags":["Expenses"],"summary":"Create an expense from an uploaded file","description":"Creates a new expense by uploading a source document (PDF,\nimage of a receipt or invoice). The file is attached to the\nnew expense and an AI extraction job is enqueued automatically\n— poll the expense's `extraction_status` until `completed` or\n`failed` to know when the parsed fields are available.\n\n**Limits**:\n\n- Max file size: 10 MB\n- Allowed types: `image/jpeg`, `image/png`, `image/gif`,\n  `application/pdf`\n- Counts against the workspace's `ai_expense_extractions`\n  metered quota — subscriptions without the\n  `attachment_to_document` feature receive `403`.\n\nToday this endpoint accepts a SINGLE file per request via\n`multipart/form-data` with field name `file`. The original\nplan called for batch (1-50 files) — that's a future\nenhancement and not the current behaviour.\n\n**Required scope: `manage:expenses`** — file uploads consume\nAI extraction quota, so they require the management scope\neven when the resulting expense would be writable with\nplain `write:expenses`.\n","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"The source document. PDF or image."}}}}}},"responses":{"201":{"description":"Created. Returns the new expense with `extraction_status: pending`\n— the AI job is running asynchronously.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Expense"}}}},"400":{"description":"Missing `file` parameter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Either the workspace has no active subscription OR the\ncurrent plan does not include the `attachment_to_document`\nfeature. The `upgrade_url` points at the relevant upgrade page.\n","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"upgrade_url":{"type":"string","format":"uri"}}}]}}}},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"File validation failed: empty file, too large, disallowed\ncontent type, or content type doesn't match the file's\nactual MIME (anti-spoofing check).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/invoices":{"get":{"operationId":"listInvoices","tags":["Invoices"],"summary":"List invoices","description":"Returns a cursor-paginated list of invoices for the workspace.\nOnly documents with `type=invoice` appear here — quotes,\nproformas, and other document types have their own endpoints\n(shipping in Phase 2b). Soft-deleted invoices are excluded.\nOrdered by `id` descending.\n","parameters":[{"$ref":"#/components/parameters/AccountId"},{"$ref":"#/components/parameters/CursorParam"},{"$ref":"#/components/parameters/LimitParam"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createInvoice","tags":["Invoices"],"summary":"Create an invoice","description":"Creates a new invoice. The `type` field is set by the route\n(never sent in the request body) — this endpoint always\ncreates invoices.\n\nPass `purchaser_id` referring to an InvoiceAccount (legal\nentity) — NOT a Contact ID. The workspace's own InvoiceAccount\nis used as the supplier automatically.\n","parameters":[{"$ref":"#/components/parameters/AccountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/invoices/{id}":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"getInvoice","tags":["Invoices"],"summary":"Get an invoice","description":"Returns the invoice. Note that this endpoint returns 404\n(not 403) if the ID belongs to a document of a different\ntype (e.g. a quote ID) — type-mismatched lookups behave\nas \"doesn't exist at this URL\" rather than leaking the\nexistence of cross-type documents.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateInvoice","tags":["Invoices"],"summary":"Update an invoice","description":"Partial update. Only fields supplied in the request body are\nmodified. The `type` field is intentionally NOT updatable —\nchanging type post-creation would shift VAT recognition across\nreporting periods (SK §73 / CZ §28 audit-trail requirement).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteInvoice","tags":["Invoices"],"summary":"Soft-delete an invoice","description":"Marks the invoice as deleted via the `deleted` timestamp.\nThe invoice remains in the database for audit purposes but\nis excluded from list responses. Issued invoices with linked\npayments may refuse deletion and return `422`.\n","responses":{"204":{"description":"No Content."},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/invoices/{id}/issue":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","format":"int64","minimum":1}}],"post":{"operationId":"issueInvoice","tags":["Invoices"],"summary":"Issue an invoice (transition draft → exposed)","description":"Marks a draft invoice as issued (`status` changes from `draft`\nto `exposed`). This is the moment the invoice becomes a\nlegally-binding document for accounting purposes. Once issued,\nan invoice cannot be un-issued via the API — soft-delete\n(`DELETE`) and recreate is the supported path.\n\nReturns `422` if the invoice is in any state other than\n`draft` — guards against accidentally double-issuing.\n\n**Required scope: `manage:invoices`** — issuance is a workflow\naction distinct from `:update`, so write-scoped PATs cannot\ntrigger it. The CanCanCan action symbol on the server is\n`:issue`, and `ApiKey#scope_permits?` only allows `:manage`\nscopes through for non-CRUD action symbols.\n","responses":{"200":{"description":"OK. The issued invoice (with `status: exposed`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Invoice is not in `draft` status — only drafts can be issued.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/invoices/{id}/send":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","format":"int64","minimum":1}}],"post":{"operationId":"sendInvoice","tags":["Invoices"],"summary":"Send invoice by email","description":"Enqueues an email delivery of the invoice (with PDF attached)\nto the customer's email address. Returns immediately with the\nupdated invoice (`email_sent_at` populated) — the actual\ndelivery happens asynchronously via a background job.\n\n**Defaults**: all request-body fields are optional. When\nomitted:\n\n- `to` → the linked InvoiceAccount's email (purchaser)\n- `subject` → localized \"Invoice \u003cnumber\u003e\" template\n- `message` → localized template body referencing the invoice\n- `locale` → invoice's `pdf_language`, falling back to\n  the workspace's locale\n\nSDK consumers can call with an empty body to \"just send it\nwith defaults\" — the typical happy path for automated billing\nintegrations.\n\nReturns `422` if validation fails after defaults are merged\n(e.g. the purchaser has no email AND no `to` override was\nprovided, or the override is malformed).\n\n**Required scope: `manage:invoices`** — sending invoices is\na billing-impacting workflow action, so write-scoped PATs\ncannot trigger it. The CanCanCan action symbol is\n`:send_to_client`.\n","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceSendRequest"}}}},"responses":{"200":{"description":"OK. The invoice with `email_sent_at` updated to the\ntime the delivery was enqueued.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Email parameters failed validation after defaults were\nmerged. Typical causes: purchaser has no email and no\n`to` override was provided; provided email address is\nmalformed; provided `cc` / `bcc` contains malformed\naddresses.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/invoices/{id}/mark_paid":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","format":"int64","minimum":1}}],"post":{"operationId":"markInvoicePaid","tags":["Invoices"],"summary":"Mark an invoice as paid","description":"Marks the invoice as fully paid. The `status` changes to\n`paid` and `paid_on` is backfilled to today's date if it\nwas previously blank.\n\n**Idempotent.** Calling on an already-paid invoice returns\n`200` with the existing payload (does NOT update `paid_on`\n— keeps the original payment date if previously set).\n\n**Required scope: `manage:invoices`** — same scope reasoning\nas `issueInvoice`. The CanCanCan action symbol is `:mark_paid`.\n","responses":{"200":{"description":"OK. The paid invoice (with `status: paid`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/invoices/{id}/pdf":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"downloadInvoicePdf","tags":["Invoices"],"summary":"Download invoice PDF","description":"Returns the rendered invoice as a PDF binary stream. The\ndocument is rendered server-side via headless Chrome\n(Ferrum) — generation is CPU-intensive, so this endpoint\nhas a dedicated rate limit of **30 requests per minute per\nAPI key** (vs. the general 300/min limit for other API\noperations).\n\nCaching: `ETag` is derived from the invoice's `updated_at`.\nClients sending `If-None-Match` with a matching ETag get\na `304 Not Modified` and skip regeneration entirely.\n\nDisposition control:\n\n- Default: `Content-Disposition: attachment; filename=...`\n  (browser file download).\n- `?disposition=inline`: `Content-Disposition: inline; filename=...`\n  for embedded preview.\n\n**Required scope: `read:invoices`** — PDF is the canonical\nmachine-readable representation of an invoice, so any token\nthat can read invoices can fetch their PDF.\n","parameters":[{"name":"disposition","in":"query","required":false,"description":"Whether the browser should download the file\n(`attachment`, the default) or render it inline for\npreview (`inline`). Values other than `inline` map to\n`attachment` server-side.\n","schema":{"type":"string","enum":["attachment","inline"],"default":"attachment"}}],"responses":{"200":{"description":"OK. The rendered PDF.","headers":{"Content-Type":{"schema":{"type":"string","enum":["application/pdf"]}},"Content-Disposition":{"schema":{"type":"string"},"description":"`attachment; filename=\"FAK2026-0001.pdf\"` (default) or\n`inline; filename=\"...\"` when `?disposition=inline`.\n"},"ETag":{"schema":{"type":"string"},"description":"Strong ETag for conditional GETs."}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"Not Modified. Returned when `If-None-Match` matches the\ncurrent ETag — the invoice hasn't been edited since the\nclient's cached copy.\n"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"Rate limit exceeded on PDF generation specifically (the\ndedicated 30/min/key throttle). Distinct from the general\nAPI limit because PDF rendering is CPU-heavy.\n","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}},"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/accounts/{account_id}/proformas":{"get":{"operationId":"listProformas","tags":["Proformas"],"summary":"List proformas","description":"Returns a cursor-paginated list of proformas. Only documents\nwith `type=proforma` appear here. Soft-deleted proformas are\nexcluded. Ordered by `id` descending.\n","parameters":[{"$ref":"#/components/parameters/AccountId"},{"$ref":"#/components/parameters/CursorParam"},{"$ref":"#/components/parameters/LimitParam"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProformaList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createProforma","tags":["Proformas"],"summary":"Create a proforma","description":"Creates a new proforma. The `type` field is set by the route\n(never sent in the request body). Pass `purchaser_id`\nreferring to an InvoiceAccount (legal entity), not a Contact.\n","parameters":[{"$ref":"#/components/parameters/AccountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProformaCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proforma"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/proformas/{id}":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Proforma ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"getProforma","tags":["Proformas"],"summary":"Get a proforma","description":"Returns 404 if the ID belongs to a document of a different\ntype (e.g. an invoice ID) — cross-type isolation per the\nPhase 2a controller-architecture decision.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proforma"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateProforma","tags":["Proformas"],"summary":"Update a proforma","description":"Partial update. The `type` field is intentionally NOT updatable.\nUpdating a proforma that has already been converted to an\ninvoice is allowed but rare — the downstream invoice is\nindependent of the source proforma after conversion.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProformaUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Proforma"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteProforma","tags":["Proformas"],"summary":"Soft-delete a proforma","description":"Marks the proforma as deleted. Proformas with linked\ndownstream invoices or tax documents may refuse deletion\nand return `422`.\n","responses":{"204":{"description":"No Content."},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/proformas/{id}/convert_to_invoice":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Proforma ID","schema":{"type":"integer","format":"int64","minimum":1}}],"post":{"operationId":"convertProformaToInvoice","tags":["Proformas"],"summary":"Convert a proforma to a final invoice","description":"Builds and persists a new Invoice from this proforma. Returns\n**`201 Created`** with the new invoice's body — the response\ntype is `Invoice`, not `Proforma`. The source proforma is\nuntouched; the link graph (`proforma.invoices`) captures the\nnew derives_from relationship.\n\n**One final invoice per proforma** (SK/CZ accounting practice):\nthe second call returns `422` with a clear \"already converted\"\nmessage. Consumer code that needs idempotency should query the\nproforma's existing invoices before calling rather than\nrelying on a no-op behaviour from this endpoint.\n\nLine items are copied from the proforma. The new invoice is\ncreated in the standard initial state — consumers can `PATCH`\nthe new invoice to refine number, dates, notes etc. before\nissuing via `POST /invoices/:id/issue`.\n\n**Required scope: `manage:proformas`** — conversion is a\nresource-creating workflow distinct from `:create` on\nproformas; the new invoice is owned by the same workspace.\nCanCanCan action symbol is `:convert_to_invoice`.\n","responses":{"201":{"description":"Created. Returns the new Invoice (not the source Proforma).\nLocate the source via `invoice.proforma_id` if needed.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"One of:\n  - This proforma has already been converted (existing\n    downstream invoice).\n  - The transition is not allowed (proforma type isn't\n    eligible — e.g. it's already in a state the builder\n    refuses).\n  - The built invoice failed validation on save.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/quotes":{"get":{"operationId":"listQuotes","tags":["Quotes"],"summary":"List quotes","description":"Returns a cursor-paginated list of quotes for the workspace.\nOnly documents with `type=quote` appear here — other document\ntypes have their own endpoints. Soft-deleted quotes are\nexcluded. Ordered by `id` descending.\n","parameters":[{"$ref":"#/components/parameters/AccountId"},{"$ref":"#/components/parameters/CursorParam"},{"$ref":"#/components/parameters/LimitParam"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createQuote","tags":["Quotes"],"summary":"Create a quote","description":"Creates a new quote. The `type` field is set by the route\n(never sent in the request body) — this endpoint always\ncreates quotes.\n\n**`valid_until` is required** for quotes (unlike invoices)\nand must be a future date — quotes are time-bounded offers\nwith an expiry. The model rejects past dates on create\nunless the record originates from an import.\n\nPass `purchaser_id` referring to an InvoiceAccount (legal\nentity) — NOT a Contact ID. The workspace's own\nInvoiceAccount is used as the supplier automatically.\n","parameters":[{"$ref":"#/components/parameters/AccountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/quotes/{id}":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Quote ID","schema":{"type":"integer","format":"int64","minimum":1}}],"get":{"operationId":"getQuote","tags":["Quotes"],"summary":"Get a quote","description":"Returns the quote. Returns 404 (not 403) if the ID belongs\nto a document of a different type (e.g. an invoice ID) —\ntype-mismatched lookups behave as \"doesn't exist at this URL\"\nrather than leaking the existence of cross-type documents.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateQuote","tags":["Quotes"],"summary":"Update a quote","description":"Partial update. The `type` field is intentionally NOT\nupdatable. Updating an accepted quote is allowed but\nunusual — the customer has already agreed to the original\noffer, so consumer code should typically clone-then-update\nrather than mutate the accepted record.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteQuote","tags":["Quotes"],"summary":"Soft-delete a quote","description":"Marks the quote as deleted via the `deleted` timestamp.\nSoft-deleted quotes are excluded from list responses but\nremain in the database for audit. Quotes with linked\ndownstream documents (invoices/proformas/sales_orders)\nmay refuse deletion and return `422`.\n","responses":{"204":{"description":"No Content."},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/accounts/{account_id}/quotes/{id}/accept":{"parameters":[{"$ref":"#/components/parameters/AccountId"},{"name":"id","in":"path","required":true,"description":"Quote ID","schema":{"type":"integer","format":"int64","minimum":1}}],"post":{"operationId":"acceptQuote","tags":["Quotes"],"summary":"Mark a quote as accepted by the customer","description":"Flags customer acceptance of the quote (`status` →\n`accepted`). The natural downstream automation is to\nconvert the accepted quote into an invoice — the document\nlink graph (`quote.invoices` etc.) captures this\nrelationship.\n\n**Idempotent** for already-accepted quotes (returns 200\nwith the existing state).\n\nRefuses transitions from **terminal states**:\n\n- `expired` — past `valid_until`, no longer a valid offer\n- `canceled` — explicitly retired\n\nReturns 422 for those.\n\n**Required scope: `manage:quotes`** — acceptance is a\nworkflow action distinct from `:update`, so write-scoped\nPATs cannot trigger it. The CanCanCan action symbol is\n`:accept`.\n","responses":{"200":{"description":"OK. The accepted quote (with `status: accepted`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Quote is in a terminal state (`expired` or `canceled`)\nand cannot be accepted.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/user":{"get":{"operationId":"getCurrentUser","tags":["User"],"summary":"Get the current user","description":"Returns the profile of the user identified by the bearer token.\nWorks for both JWT and API key authentication.\n\nPersonal access tokens (`lct_pat_*`) return their owning user.\nWorkspace tokens (`lct_live_*`) return the user that issued the\ntoken — clients should not assume a 1:1 mapping between token\nand user for workspace tokens.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"JwtBearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Devise JWT issued via POST /api/v1/auth/sign_in. Used by mobile\nand SPA clients. Revoked tokens are tracked in `api_jwt_denylists`.\n"},"ApiKeyBearer":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"Lucanto API key. Three prefixes:\n\n- `lct_pat_\u003crandom\u003e\u003cchecksum\u003e` — personal access token (owner: User)\n- `lct_live_\u003crandom\u003e\u003cchecksum\u003e` — workspace token, live data\n- `lct_test_\u003crandom\u003e\u003cchecksum\u003e` — workspace token, sandbox account (Phase 3)\n\nIssue keys at `/settings/api_keys` (personal) or\n`/:account_id/settings/api_keys` (workspace). Plaintext token is\nshown once and never again.\n"}},"schemas":{"Error":{"type":"object","required":["error","message","request_id","docs_url"],"properties":{"error":{"type":"string","description":"Machine-readable error code. Stable across releases — clients\nshould branch on this rather than the human-readable message.\n","example":"forbidden"},"message":{"type":"string","description":"Human-readable error description, localized to\n`Accept-Language`. Subject to wording changes between\nreleases.\n","example":"You do not have permission to perform this action."},"request_id":{"type":"string","description":"Server-side request identifier. Same value as the\n`X-Request-Id` response header. Quote this in support\ntickets so we can pull the matching server log line.\n","example":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60"},"docs_url":{"type":"string","format":"uri","description":"URL of the per-error-code documentation page. Useful for\nSDK error subclasses to deep-link from a stack trace\nto the docs explaining the cause + remediation.\n","example":"https://docs.lucanto.eu/api/v1/errors/forbidden"}}},"ValidationError":{"type":"object","required":["error","message","errors","request_id","docs_url"],"properties":{"error":{"type":"string","enum":["unprocessable_entity"]},"message":{"type":"string","description":"Concatenated summary of all validation errors","example":"Name can't be blank, Email is invalid"},"errors":{"type":"array","description":"Individual validation error messages","items":{"type":"string"},"example":["Name can't be blank","Email is invalid"]},"request_id":{"type":"string","example":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60"},"docs_url":{"type":"string","format":"uri","example":"https://docs.lucanto.eu/api/v1/errors/unprocessable_entity"}}},"InvoiceAccountEmbed":{"type":"object","required":["id"],"properties":{"id":{"type":"integer","format":"int64"},"registration_id":{"type":"string","nullable":true,"description":"Business registration ID (IČO)."},"tax_id":{"type":"string","nullable":true,"description":"Tax ID (DIČ)."},"vat_id":{"type":"string","nullable":true,"description":"VAT ID (IČ DPH)."},"vat_payer_type":{"type":"string","nullable":true,"enum":["standard","non_payer_vat","vat_registered_eu","vat_registered_oss",null]},"vat_period":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true},"phone_number":{"type":"string","nullable":true},"web":{"type":"string","nullable":true},"invoice_address":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/AddressEmbed"}]},"postal_address":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/AddressEmbed"}]}}},"AddressEmbed":{"type":"object","required":["id"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string","nullable":true},"contact":{"type":"string","nullable":true},"street":{"type":"string","nullable":true},"municipality":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true},"country_id":{"type":"integer","format":"int64","nullable":true}}},"CursorPagination":{"type":"object","required":["next_cursor","has_more"],"properties":{"next_cursor":{"type":"string","nullable":true,"description":"Cursor to fetch the next page. Pass it as the `cursor` query\nparameter on the next request. `null` on the last page.\n","example":"eyJpZCI6MTIzfQ=="},"has_more":{"type":"boolean","description":"True when more pages exist after this one."}}},"Account":{"type":"object","required":["id","name","default","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64","description":"Workspace ID. Use this in all account-scoped paths."},"name":{"type":"string","description":"Display name of the workspace. Computed from the linked\nInvoiceAccount's legal entity name when the workspace is\nnamed after a company, or from the owner's name for\npersonal/freelancer workspaces.\n","example":"ACME s.r.o."},"accounting_type":{"type":"string","nullable":true,"enum":["simple","double_entry","flat_rate_expenses",null],"description":"The workspace's bookkeeping mode. Drives expense category\noptions and tax/VAT logic. `simple` is single-entry\nbookkeeping; `double_entry` is full double-entry; and\n`flat_rate_expenses` is the SK/CZ \"paušálne výdavky\"\nsimplified regime for sole proprietors. `null` when the\nworkspace hasn't finished onboarding yet.\n","example":"simple"},"onboarding_step":{"type":"string","nullable":true,"description":"Current step in the workspace onboarding wizard. `null`\nwhen onboarding is complete. Useful for clients that\nwant to direct the user to finish setup before issuing\ninvoices.\n","example":"company_details"},"vat_registration_alert":{"type":"boolean","nullable":true,"description":"`true` when the workspace's recent revenue suggests they\nmay have crossed the VAT registration threshold and\nshould consult an accountant. Hint for client UI to show\nan in-app banner.\n"},"default":{"type":"boolean","description":"`true` if this is the user's default workspace. Exactly one\naccount per user is the default at any time.\n"},"logo_url":{"type":"string","format":"uri","nullable":true,"description":"Signed URL for the workspace logo (ActiveStorage blob URL).\n`null` when no logo is uploaded. URLs are time-limited —\nre-fetch the account record to get a fresh URL.\n"},"legal_identity":{"nullable":true,"description":"The workspace's legal-identity facet — registered name,\ntax IDs, VAT registration, contact details, and addresses.\nThis is the same data that appears as `supplier` on every\ndocument the workspace issues.\n\nArchitecturally the legal identity is a *facet* of the\nworkspace itself, not a separately-owned resource — the\nunderlying record is the workspace's `InvoiceAccount`,\nwhich is also the model `Contact` points at for\ncustomer-side legal entities. Closer to Stripe's\nnested-fields pattern (`account.business_profile`,\n`account.individual`, `account.company`) than to a\nsub-resource shape.\n\nWrite access via the public API is a future slice; for\nnow, this surface is read-only.\n","allOf":[{"$ref":"#/components/schemas/AccountLegalIdentity"}]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"AccountLegalIdentity":{"type":"object","description":"The legal-identity nested object embedded inside the Account\nresponse. Distinct from `InvoiceAccountEmbed` (used for\ncounterparty embeds on documents) because this surface uses\n`country_alpha2` for portability instead of the internal\n`country_id`.\n","properties":{"registration_id":{"type":"string","nullable":true,"description":"Business registration ID (IČO in SK/CZ).","example":"12345678"},"tax_id":{"type":"string","nullable":true,"description":"Tax ID (DIČ in SK/CZ).","example":"2023456789"},"vat_id":{"type":"string","nullable":true,"description":"VAT ID (IČ DPH in SK / DIČ DPH in CZ). Null for non-VAT-payers.","example":"SK2023456789"},"vat_payer_type":{"type":"string","nullable":true,"enum":["standard","non_payer_vat","vat_registered_eu","vat_registered_oss",null],"description":"VAT registration status. `standard` for fully registered\nVAT payers; `non_payer_vat` for non-payers; `vat_registered_eu`\nfor EU cross-border; `vat_registered_oss` for One-Stop Shop.\n"},"vat_period":{"type":"string","nullable":true,"description":"VAT filing period (typically `monthly` or `quarterly`)."},"email":{"type":"string","format":"email","nullable":true,"description":"Contact email rendered on issued documents."},"phone_number":{"type":"string","nullable":true,"description":"Contact phone rendered on issued documents."},"web":{"type":"string","nullable":true,"description":"Company website URL rendered on issued documents."},"legal_form":{"type":"string","nullable":true,"description":"Legal-form abbreviation rendered on documents\n(e.g. `s.r.o.`, `a.s.`, `živnosť`). Derived from\n`legal_form_id`.\n","example":"s.r.o."},"legal_form_id":{"type":"integer","format":"int64","nullable":true},"address":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/AccountLegalAddress"}]},"postal_address":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/AccountLegalAddress"}]}}},"AccountLegalAddress":{"type":"object","description":"Address embedded inside the legal-identity facet. Uses\n`country_alpha2` (ISO 3166-1 two-letter code) for portability.\n","properties":{"name":{"type":"string","nullable":true,"description":"The company name on the address (often the legal entity name)."},"street":{"type":"string","nullable":true},"municipality":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true},"country_alpha2":{"type":"string","nullable":true,"minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 country code. The DB stores lowercase\n(`sk`, `cz`, `ro`); responses surface what's stored.\n","example":"sk"},"contact":{"type":"string","nullable":true,"description":"Optional secondary contact name on the address."}}},"BankAccountList":{"allOf":[{"$ref":"#/components/schemas/CursorPagination"},{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BankAccount"}}}}]},"BankAccount":{"type":"object","required":["id","account_type","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string","nullable":true,"description":"Display name. Required for cash registers (where it must\nbe unique within the workspace); optional for bank\naccounts where the IBAN/account_number identifies the\nrecord.\n","example":"Tatra Bank EUR"},"account_type":{"type":"string","enum":["bank","cash"],"description":"`bank` — traditional bank account with IBAN / account\nnumber, potentially PSD2-synced. `cash` — cash register\nfor manual cash-movement tracking.\n"},"owner_name":{"type":"string","nullable":true,"description":"Name on the account / register."},"currency":{"type":"string","nullable":true,"description":"ISO-4217 currency code. Defaults to workspace currency."},"iban":{"type":"string","nullable":true,"description":"IBAN — required for bank-type accounts (or `account_number` must be set). Null for cash registers.","example":"SK6807200002891000000018"},"swift_code":{"type":"string","nullable":true,"description":"SWIFT/BIC. Optional for bank accounts; null for cash.","example":"TATRSKBX"},"account_number":{"type":"string","nullable":true,"description":"Local account number — required for bank-type accounts\nif `iban` is not set. SK/CZ banks accept legacy\nnon-IBAN format. Null for cash registers.\n"},"prefix_number":{"type":"string","nullable":true,"description":"CZ-style prefix on legacy account numbers (e.g. `19-` in `19-2000945399/0800`)."},"bank_code":{"type":"string","nullable":true,"description":"Local bank code (SK: 4-digit, CZ: 4-digit). Null for cash registers."},"balance_available":{"type":"string","format":"decimal","nullable":true,"description":"Available balance from the most recent PSD2 sync, if\nconnected. Null for cash registers and non-synced\nbank accounts. Stringified BigDecimal.\n"},"balance_booked":{"type":"string","format":"decimal","nullable":true,"description":"Booked balance from the most recent PSD2 sync. Differs\nfrom `balance_available` for accounts with pending\ntransactions. Same null/format semantics as\n`balance_available`.\n"},"last_balance_sync_at":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp of the last successful balance sync. Null for\ncash registers and bank accounts that have never synced.\n"},"default":{"type":"boolean","nullable":true,"description":"Whether this account is the workspace's default for new\ndocuments. Each `account_type` has independent defaults\n(one default bank + one default cash register per workspace).\n"},"invoice_display":{"type":"boolean","nullable":true,"description":"Whether the account's payment details should be rendered\non invoice PDFs. Defaults to true for the workspace's own\naccounts.\n"},"tenant":{"type":"boolean","nullable":true,"description":"`true` for tenant accounts (used internally by the\nrealities/rents feature for property-management\nworkflows); `false` for the workspace's own accounts.\nMost workspaces only have own (`tenant: false`) accounts.\n"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"BankAccountCreateRequest":{"type":"object","required":["bank_account"],"description":"Wrapper required by Rails strong params.","properties":{"bank_account":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Display name. Required."},"currency":{"type":"string","description":"ISO-4217 currency code."},"iban":{"type":"string","description":"Required for bank-type if `account_number` is not provided."},"swift_code":{"type":"string"},"account_number":{"type":"string"},"prefix_number":{"type":"string"},"bank_code":{"type":"string"},"default":{"type":"boolean","description":"Mark as the workspace's default account of this account_type."},"invoice_display":{"type":"boolean"}}}}},"BankAccountUpdateRequest":{"type":"object","required":["bank_account"],"properties":{"bank_account":{"type":"object","description":"PATCH semantics — any subset of the create fields is valid.","properties":{"name":{"type":"string"},"currency":{"type":"string"},"iban":{"type":"string"},"swift_code":{"type":"string"},"account_number":{"type":"string"},"prefix_number":{"type":"string"},"bank_code":{"type":"string"},"default":{"type":"boolean"},"invoice_display":{"type":"boolean"}}}}},"ContactList":{"allOf":[{"$ref":"#/components/schemas/CursorPagination"},{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}}}}]},"Contact":{"type":"object","required":["id","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64","description":"Contact ID (workspace-scoped)"},"name":{"type":"string","maxLength":255,"nullable":true,"description":"Display name of the contact (model allows nil; typically present)","example":"ACME s.r.o."},"rate":{"type":"string","format":"decimal","nullable":true,"description":"Default hourly/unit rate applied on quotes/invoices for this\ncontact. Returned as a string to preserve decimal precision\n(Stripe-style). On write, accepts either a number or a\nstringified number.\n","example":"15.50"},"due_days":{"type":"integer","nullable":true,"description":"Default payment terms in days. Applied to issued documents.","example":14},"invoice_account_id":{"type":"integer","format":"int64","description":"ID of the linked **InvoiceAccount** (the shared legal-entity\nregistry) that holds the contact's legal name, registration\nID, VAT ID, and addresses.\n"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ContactCreateRequest":{"type":"object","required":["contact"],"properties":{"contact":{"type":"object","required":["name","invoice_account_id"],"description":"Wrapper required by the underlying Rails strong params.","properties":{"name":{"type":"string","maxLength":255},"rate":{"type":"number","format":"float"},"due_days":{"type":"integer","minimum":0},"invoice_account_id":{"type":"integer","format":"int64","description":"InvoiceAccount ID to link this contact to."}}}}},"ContactUpdateRequest":{"type":"object","required":["contact"],"properties":{"contact":{"type":"object","description":"Wrapper required by Rails strong params. All fields are\noptional in PATCH; only provided fields are updated.\n","properties":{"name":{"type":"string","maxLength":255},"rate":{"type":"number","format":"float"},"due_days":{"type":"integer","minimum":0},"invoice_account_id":{"type":"integer","format":"int64"}}}}},"CreditNoteList":{"allOf":[{"$ref":"#/components/schemas/CursorPagination"},{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CreditNote"}}}}]},"CreditNote":{"type":"object","required":["id","type","status","currency","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"type":{"type":"string","enum":["credit_note"]},"status":{"type":"string","enum":["draft","exposed","in_due","paid","unpaid","partially_paid","paid_above_price","paid_after_due_date","canceled"],"description":"Credit note lifecycle state. The same `sync_payment_status!`\ncallback that derives invoice statuses also fires on\ncredit notes (it's on the base Document model), so the\nfull payment-derived state set can appear here even\nthough credit notes are not themselves a payment\nobligation. Practically: consumers should treat `draft`\nand `exposed` as the meaningful credit-note lifecycle\nstates; the paid-family states reflect how the credit\nnote has been applied against the source invoice's\npayment allocation.\n"},"number":{"type":"string","nullable":true,"description":"Credit note number (formatted via number_series, e.g. 'DOB2026-0001'). Null for drafts."},"issue_date":{"type":"string","format":"date","nullable":true},"delivery_date":{"type":"string","format":"date","nullable":true},"currency":{"type":"string","description":"ISO-4217 currency code."},"exchange_rate":{"type":"string","format":"decimal","nullable":true},"included_in_turnover":{"type":"boolean","nullable":true},"reverse_charge":{"type":"boolean","nullable":true},"reverse_charge_text":{"type":"string","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_amount":{"type":"string","format":"decimal","nullable":true},"order_number":{"type":"string","nullable":true},"text_opening":{"type":"string","nullable":true},"text_closing":{"type":"string","nullable":true},"footer_note":{"type":"string","nullable":true},"recipient_note":{"type":"string","nullable":true},"internal_note":{"type":"string","nullable":true},"pdf_language":{"type":"string","nullable":true},"pdf_locale":{"type":"string","nullable":true},"pdf_compact_mode":{"type":"boolean","nullable":true},"hide_signature":{"type":"boolean","nullable":true},"delivery_type":{"type":"string","nullable":true},"sent_status":{"type":"string","nullable":true},"email_sent_at":{"type":"string","format":"date-time","nullable":true},"seen_timestamp":{"type":"string","format":"date-time","nullable":true},"purchaser_id":{"type":"integer","format":"int64","nullable":true,"description":"InvoiceAccount ID of the customer."},"supplier_id":{"type":"integer","format":"int64","nullable":true,"description":"InvoiceAccount ID of the workspace's own legal entity."},"bank_account_id":{"type":"integer","format":"int64","nullable":true},"project_id":{"type":"integer","format":"int64","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"document_items":{"type":"array","items":{"$ref":"#/components/schemas/CreditNoteLineItem"}},"purchaser":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/InvoiceAccountEmbed"}]},"supplier":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/InvoiceAccountEmbed"}]}}},"CreditNoteLineItem":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"quantity":{"type":"string","format":"decimal","nullable":true},"measure_unit":{"type":"string","nullable":true},"unit_price":{"type":"string","format":"decimal","nullable":true},"vat_rate":{"type":"string","format":"decimal","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_unit_price":{"type":"string","format":"decimal","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CreditNoteCreateRequest":{"type":"object","required":["document"],"description":"Wrapper structure required by Rails strong params. The\ntop-level `document` key carries the credit note fields.\n","properties":{"document":{"type":"object","required":["purchaser_id"],"properties":{"number":{"type":"string"},"number_series_id":{"type":"integer","format":"int64"},"status":{"type":"string"},"issue_date":{"type":"string","format":"date"},"delivery_date":{"type":"string","format":"date"},"currency":{"type":"string"},"exchange_rate":{"type":"string"},"order_number":{"type":"string"},"purchaser_id":{"type":"integer","format":"int64","description":"InvoiceAccount ID of the customer. Required."},"bank_account_id":{"type":"integer","format":"int64"},"project_id":{"type":"integer","format":"int64"},"invoice_id":{"type":"integer","format":"int64","description":"Optional. Source invoice ID. When provided, the\nDocumentBuilder establishes a `corrects` link from\nthe new credit note back to this invoice — queryable\nlater as `credit_note.invoice`.\n"},"included_in_turnover":{"type":"string","enum":["0","1"]},"reverse_charge":{"type":"boolean"},"reverse_charge_text":{"type":"string"},"discount_description":{"type":"string"},"discount_percentage":{"type":"string"},"discount_amount":{"type":"string"},"text_opening":{"type":"string"},"text_closing":{"type":"string"},"footer_note":{"type":"string"},"recipient_note":{"type":"string"},"internal_note":{"type":"string"},"pdf_language":{"type":"string"},"pdf_locale":{"type":"string"},"pdf_compact_mode":{"type":"boolean"},"hide_prices":{"type":"boolean"},"document_items_attributes":{"type":"array","items":{"$ref":"#/components/schemas/CreditNoteLineItemInput"}}}}}},"CreditNoteLineItemInput":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string"},"order":{"type":"integer"},"quantity":{"type":"string"},"measure_unit":{"type":"string"},"unit_price":{"type":"string"},"vat_rate":{"type":"string"},"discount_description":{"type":"string"},"discount_unit_price":{"type":"string"},"discount_percentage":{"type":"string"},"_destroy":{"type":"string","enum":["true","false"]}}},"CreditNoteUpdateRequest":{"allOf":[{"$ref":"#/components/schemas/CreditNoteCreateRequest"},{"type":"object","description":"PATCH semantics — any subset of the create request is valid."}]},"ExpenseList":{"allOf":[{"$ref":"#/components/schemas/CursorPagination"},{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Expense"}}}}]},"Expense":{"type":"object","required":["id","extraction_status","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"receipt_id":{"type":"string","nullable":true,"description":"Series prefix for receipt numbering (e.g. `A`, `B`). Combined\nwith `receipt_number` forms the full identifier shown on\npaper receipts and in accountancy reports.\n"},"receipt_number":{"type":"string","nullable":true,"description":"Sequential number within the receipt_id series."},"cash_register_code":{"type":"string","nullable":true,"description":"Slovak eKasa cash register code (Kód pokladnice). Required\nfor VPD documents (Výdavkový pokladničný doklad).\n"},"okp":{"type":"string","nullable":true,"description":"Slovak eKasa \"Overovací kód podnikateľa\" — operator\nverification code printed on cash receipts. Used to match\neKasa receipts back to their issuing register.\n"},"issue_date":{"type":"string","format":"date","nullable":true,"description":"Date the supplier issued the receipt."},"delivery_date":{"type":"string","format":"date","nullable":true},"due_date":{"type":"string","format":"date","nullable":true},"currency":{"type":"string","nullable":true,"description":"ISO-4217 currency code. Null only on expenses created via\nthe `/expenses/upload` endpoint before AI extraction has\nrun — populated by the extraction job in completed state.\n","example":"EUR"},"exchange_rate":{"type":"string","format":"decimal","nullable":true,"description":"Exchange rate to the workspace's base currency at the time\nof issue. Returned as a string for decimal precision.\n","example":"1.0850"},"order_number":{"type":"string","nullable":true,"description":"Order reference number (objednávka)."},"variable_symbol":{"type":"string","nullable":true,"description":"Variable symbol (variabilný symbol) — payment matching key."},"constant_symbol":{"type":"string","nullable":true,"description":"Constant symbol (konštantný symbol) — payment type classifier."},"specific_symbol":{"type":"string","nullable":true,"description":"Specific symbol (špecifický symbol) — additional payment reference."},"payment_status":{"type":"string","enum":["unpaid","overdue","partially_paid","paid","paid_over",null],"nullable":true,"description":"Derived from linked transactions. `null` until the first\ntransaction is attached or the due date passes.\n"},"approval_status":{"type":"string","enum":["pending","approved","rejected","returned","accounted",null],"nullable":true,"description":"Approval workflow state. `null` for workspaces that don't\nuse the approval feature; populated by approval transitions.\n"},"extraction_status":{"type":"string","enum":["pending","processing","completed","failed","manual"],"description":"AI extraction lifecycle. `manual` when the expense was\ncreated via the API/UI without an attached file. `pending`\nand `processing` indicate ongoing extraction — poll until\n`completed` or `failed`. See Phase 2a.6 retry_extraction.\n"},"paid_amount":{"type":"string","format":"decimal","nullable":true,"description":"Total paid so far (sum of transactions). Stringified BigDecimal."},"paid_at":{"type":"string","format":"date-time","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_amount":{"type":"string","format":"decimal","nullable":true},"locale":{"type":"string","nullable":true,"description":"Locale for PDF rendering and emails (sk, cz, en, ro)."},"department":{"type":"string","nullable":true},"cost_center":{"type":"string","nullable":true},"submitted_for_approval_at":{"type":"string","format":"date-time","nullable":true},"approved_at":{"type":"string","format":"date-time","nullable":true},"rejected_at":{"type":"string","format":"date-time","nullable":true},"returned_at":{"type":"string","format":"date-time","nullable":true},"supplier_id":{"type":"integer","format":"int64","nullable":true,"description":"Contact ID of the supplier. The \"from whom\" of the expense."},"purchaser_id":{"type":"integer","format":"int64","nullable":true,"description":"Contact ID for the purchaser (the workspace's own management\ncontact). Used to attribute expenses to a specific entity\nin multi-entity setups.\n"},"bank_account_id":{"type":"integer","format":"int64","nullable":true,"description":"BankAccount ID used (or expected) for payment."},"project_id":{"type":"integer","format":"int64","nullable":true},"trip_id":{"type":"integer","format":"int64","nullable":true,"description":"Travel trip ID — links travel-related expenses."},"unit_id":{"type":"integer","format":"int64","nullable":true,"description":"Supplier unit (Address) ID — branch or specific location of the supplier."},"created_by_id":{"type":"integer","format":"int64","nullable":true},"approved_by_id":{"type":"integer","format":"int64","nullable":true},"rejected_by_id":{"type":"integer","format":"int64","nullable":true},"returned_by_id":{"type":"integer","format":"int64","nullable":true},"submitted_by_id":{"type":"integer","format":"int64","nullable":true},"updated_by_id":{"type":"integer","format":"int64","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"file_url":{"type":"string","format":"uri","nullable":true,"description":"Signed URL for the attached source document (PDF, image,\netc.). `null` when no file is attached. URLs are time-limited\n— re-fetch the expense to get a fresh URL.\n"},"file_name":{"type":"string","nullable":true,"description":"Original filename of the attached source document."},"file_content_type":{"type":"string","nullable":true,"description":"MIME type of the attached source document."}}},"ExpenseLineItem":{"type":"object","description":"A single line on the expense (Účtovný riadok). Line items belong\nto the expense via `expense_items_attributes` in create/update\nrequests.\n","properties":{"id":{"type":"integer","format":"int64","nullable":true,"description":"Item ID. Omit on create; include with `_destroy: true` to delete on update."},"name":{"type":"string"},"item_type":{"type":"string","enum":["item","service","text"]},"description":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"measure_unit":{"type":"string","nullable":true,"description":"Unit of measure (e.g. ks, kg, hod)."},"quantity":{"type":"string","format":"decimal"},"unit_price":{"type":"string","format":"decimal","description":"Net price per unit. Stringified BigDecimal."},"vat_rate":{"type":"string","format":"decimal","description":"VAT rate (e.g. '20.00' for 20%). Stringified BigDecimal."},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_unit_price":{"type":"string","format":"decimal","nullable":true},"vat_deduction":{"type":"string","format":"decimal","nullable":true,"description":"Percentage of VAT deductible for tax (0-100)."},"_destroy":{"type":"boolean","description":"On update only — set true to remove this item."}}},"ExpenseCreateRequest":{"type":"object","required":["expense"],"description":"Wrapper structure required by Rails strong params. The top-level\n`expense` key carries the receipt data; `address`/`unit`/\n`invoice_account` are optional siblings used for inline supplier\ncreation when `expense.supplier_id` is not provided.\n","properties":{"expense":{"type":"object","required":["currency"],"properties":{"expense_type":{"type":"string","enum":["vpd","received_invoice","simplified_receipt","internal_document"]},"receipt_id":{"type":"string"},"cash_register_code":{"type":"string"},"receipt_number":{"type":"string"},"okp":{"type":"string"},"currency":{"type":"string","description":"ISO-4217 currency code."},"exchange_rate":{"type":"string"},"issue_date":{"type":"string","format":"date"},"delivery_date":{"type":"string","format":"date"},"due_date":{"type":"string","format":"date"},"order_number":{"type":"string"},"variable_symbol":{"type":"string"},"constant_symbol":{"type":"string"},"specific_symbol":{"type":"string"},"discount_description":{"type":"string"},"discount_percentage":{"type":"string"},"discount_amount":{"type":"string"},"supplier_id":{"type":"integer","format":"int64","description":"Existing Contact ID. Mutually preferred over inline supplier creation."},"unit_id":{"type":"integer","format":"int64"},"project_id":{"type":"integer","format":"int64"},"trip_id":{"type":"integer","format":"int64"},"internal_note":{"type":"string","description":"Internal note. Not rendered on the PDF representation."},"expense_items_attributes":{"type":"array","items":{"$ref":"#/components/schemas/ExpenseLineItem"}}}},"address":{"type":"object","description":"Optional. Supplier's invoicing address (when creating a\nsupplier inline rather than referencing `expense.supplier_id`).\n","properties":{"name":{"type":"string"},"street":{"type":"string"},"municipality":{"type":"string"},"postal_code":{"type":"string"},"country_id":{"type":"integer","format":"int64"}}},"unit":{"type":"object","description":"Optional. The supplier's specific branch/unit address.\nUsed together with `address` for inline supplier creation\nwhen the supplier operates multiple locations.\n","properties":{"name":{"type":"string"},"street":{"type":"string"},"municipality":{"type":"string"},"postal_code":{"type":"string"},"country_id":{"type":"integer","format":"int64"}}},"invoice_account":{"type":"object","description":"Optional. Legal-entity registry fields for inline supplier\ncreation. Looked up by `registration_id` or `vat_id` first;\ncreated if no match exists.\n","properties":{"vat_payer_type":{"type":"string","enum":["standard","non_payer_vat","vat_registered_eu","vat_registered_oss"]},"registration_id":{"type":"string","description":"Business registration ID (IČO in SK/CZ)."},"tax_id":{"type":"string","description":"Tax ID (DIČ in SK/CZ)."},"vat_id":{"type":"string","description":"VAT ID (IČ DPH in SK / DIČ DPH in CZ)."},"phone_number":{"type":"string"},"email":{"type":"string","format":"email"},"web":{"type":"string"}}},"address_match":{"type":"string","enum":["0","1"],"description":"Pass '1' when the `unit` address is identical to `address`."}}},"ExpenseUpdateRequest":{"allOf":[{"$ref":"#/components/schemas/ExpenseCreateRequest"},{"type":"object","description":"PATCH semantics: any subset of the create request is valid.\nOmitted fields keep their current values. Use\n`expense.expense_items_attributes[i]._destroy: true` to\nremove a specific line item.\n"}]},"InvoiceList":{"allOf":[{"$ref":"#/components/schemas/CursorPagination"},{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}}}}]},"Invoice":{"type":"object","required":["id","type","status","currency","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"type":{"type":"string","enum":["invoice"],"description":"STI type discriminator. Always `invoice` on this endpoint —\nother types are exposed via their own endpoints (quotes,\nproformas, credit_notes ship in Phase 2b).\n"},"status":{"type":"string","enum":["draft","exposed","in_due","paid","unpaid","partially_paid","paid_above_price","paid_after_due_date","canceled"],"description":"Invoice lifecycle state. `draft` for in-progress unfinished\ninvoices; `exposed` for issued. The paid-family states\n(`paid`, `partially_paid`, `paid_above_price`,\n`paid_after_due_date`) are derived from linked payments.\nStatus transitions ship in Phase 2a.9b via dedicated\nactions (`POST /:id/issue`, `POST /:id/mark_paid`).\n"},"number":{"type":"string","nullable":true,"description":"Invoice number (formatted via number_series, e.g. 'FAK2026-0001'). Null for drafts."},"issue_date":{"type":"string","format":"date","nullable":true},"delivery_date":{"type":"string","format":"date","nullable":true},"due_date":{"type":"string","format":"date","nullable":true},"valid_until":{"type":"string","format":"date","nullable":true},"paid_on":{"type":"string","format":"date","nullable":true,"description":"Date the invoice was fully paid (set when status flips to paid)."},"days_to_due":{"type":"integer","nullable":true,"description":"Computed: days remaining until due_date."},"currency":{"type":"string","description":"ISO-4217 currency code."},"exchange_rate":{"type":"string","format":"decimal","nullable":true},"included_in_turnover":{"type":"boolean","nullable":true,"description":"Whether this invoice counts toward the VAT registration threshold turnover calculation."},"reverse_charge":{"type":"boolean","nullable":true,"description":"Whether VAT reverse-charge applies (cross-border B2B)."},"reverse_charge_text":{"type":"string","nullable":true},"advance_payment_deduction":{"type":"string","format":"decimal","nullable":true,"description":"Amount deducted because of an advance payment / proforma."},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_amount":{"type":"string","format":"decimal","nullable":true},"payment_method":{"type":"string","nullable":true,"description":"Payment channel (bank_transfer, cash, card, etc.). Defaults to bank_transfer on create.","example":"bank_transfer"},"variable_symbol":{"type":"string","nullable":true},"constant_symbol":{"type":"string","nullable":true},"specific_symbol":{"type":"string","nullable":true},"order_number":{"type":"string","nullable":true},"text_opening":{"type":"string","nullable":true},"text_closing":{"type":"string","nullable":true},"footer_note":{"type":"string","nullable":true},"recipient_note":{"type":"string","nullable":true,"description":"Visible to recipient on the PDF."},"internal_note":{"type":"string","nullable":true,"description":"Internal-only note. Not rendered on PDF or sent to recipient."},"pdf_language":{"type":"string","nullable":true},"pdf_locale":{"type":"string","nullable":true},"pdf_compact_mode":{"type":"boolean","nullable":true},"hide_signature":{"type":"boolean","nullable":true},"delivery_type":{"type":"string","nullable":true},"sent_status":{"type":"string","nullable":true,"description":"Tracks delivery state when sent by email (queued / sent / failed)."},"email_sent_at":{"type":"string","format":"date-time","nullable":true},"seen_timestamp":{"type":"string","format":"date-time","nullable":true,"description":"First time the recipient opened the PDF (when read-tracking is enabled)."},"reminder_count":{"type":"integer","nullable":true,"description":"Number of payment reminders sent for this invoice."},"lock_type":{"type":"string","nullable":true,"description":"Restricts which fields can be edited (e.g. 'tax_lock' after VAT period closure)."},"auto_on_payment":{"type":"boolean","nullable":true,"description":"Auto-transition rules tied to payment events."},"purchaser_id":{"type":"integer","format":"int64","nullable":true,"description":"InvoiceAccount ID of the customer."},"supplier_id":{"type":"integer","format":"int64","nullable":true,"description":"InvoiceAccount ID of the workspace's own legal entity."},"bank_account_id":{"type":"integer","format":"int64","nullable":true},"project_id":{"type":"integer","format":"int64","nullable":true},"rent_id":{"type":"integer","format":"int64","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"document_items":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceLineItem"}},"purchaser":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/InvoiceAccountEmbed"}]},"supplier":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/InvoiceAccountEmbed"}]}}},"InvoiceLineItem":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"quantity":{"type":"string","format":"decimal","nullable":true},"measure_unit":{"type":"string","nullable":true},"unit_price":{"type":"string","format":"decimal","nullable":true},"vat_rate":{"type":"string","format":"decimal","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_unit_price":{"type":"string","format":"decimal","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"InvoiceCreateRequest":{"type":"object","required":["document"],"description":"Wrapper structure required by Rails strong params. The\ntop-level `document` key carries the invoice fields. The\nSTI `type` is set by the route, never sent in the body.\n","properties":{"document":{"type":"object","required":["purchaser_id"],"properties":{"number":{"type":"string"},"number_series_id":{"type":"integer","format":"int64"},"status":{"type":"string"},"issue_date":{"type":"string","format":"date"},"delivery_date":{"type":"string","format":"date"},"due_date":{"type":"string","format":"date"},"days_to_due":{"type":"integer","minimum":0},"valid_until":{"type":"string","format":"date"},"currency":{"type":"string"},"exchange_rate":{"type":"string"},"payment_method":{"type":"string"},"payment_type":{"type":"string"},"variable_symbol":{"type":"string"},"constant_symbol":{"type":"string"},"specific_symbol":{"type":"string"},"order_number":{"type":"string"},"purchaser_id":{"type":"integer","format":"int64","description":"InvoiceAccount ID of the customer. Required."},"bank_account_id":{"type":"integer","format":"int64"},"project_id":{"type":"integer","format":"int64"},"included_in_turnover":{"type":"string","enum":["0","1"]},"reverse_charge":{"type":"boolean"},"reverse_charge_text":{"type":"string"},"advance_payment_deduction":{"type":"string"},"discount_description":{"type":"string"},"discount_percentage":{"type":"string"},"discount_amount":{"type":"string"},"text_opening":{"type":"string"},"text_closing":{"type":"string"},"footer_note":{"type":"string"},"recipient_note":{"type":"string"},"internal_note":{"type":"string"},"pdf_language":{"type":"string"},"pdf_locale":{"type":"string"},"pdf_compact_mode":{"type":"boolean"},"mandatory_text":{"type":"string"},"hide_prices":{"type":"boolean"},"reminder_count":{"type":"integer"},"proforma_id":{"type":"integer","format":"int64","description":"Source proforma ID if converting from a proforma."},"tax_document_ids":{"type":"array","items":{"type":"integer","format":"int64"}},"delivery_note_ids":{"type":"array","items":{"type":"integer","format":"int64"}},"document_items_attributes":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceLineItemInput"}},"auto_on_payment":{"type":"boolean"}}}}},"InvoiceLineItemInput":{"type":"object","properties":{"id":{"type":"integer","format":"int64","description":"Item ID. Omit on create; include with `_destroy: true` to delete on update."},"name":{"type":"string"},"description":{"type":"string"},"order":{"type":"integer"},"quantity":{"type":"string"},"measure_unit":{"type":"string"},"unit_price":{"type":"string"},"vat_rate":{"type":"string"},"discount_description":{"type":"string"},"discount_unit_price":{"type":"string"},"discount_percentage":{"type":"string"},"_destroy":{"type":"string","enum":["true","false"],"description":"On update only — set 'true' to remove this item."}}},"InvoiceUpdateRequest":{"allOf":[{"$ref":"#/components/schemas/InvoiceCreateRequest"},{"type":"object","description":"PATCH semantics: any subset of the create request is valid.\n"}]},"InvoiceSendRequest":{"type":"object","description":"Optional overrides for invoice email delivery. Omit fields\nto use defaults computed from the invoice + purchaser.\n","properties":{"invoice_email":{"type":"object","properties":{"to":{"type":"string","format":"email","description":"Recipient address. Defaults to the purchaser's email.\n","example":"billing@customer.example"},"cc":{"type":"string","description":"Comma-separated list of CC recipients. Each address\nmust be a valid email; the whole field is rejected\non the first malformed entry.\n"},"bcc":{"type":"string","description":"Comma-separated list of BCC recipients. Same\nvalidation rules as `cc`.\n"},"subject":{"type":"string","description":"Custom subject line. Defaults to a localized\ntemplate like \"Faktúra FAK2026-0001\".\n"},"message":{"type":"string","description":"Custom body text. Defaults to a localized template\nreferencing the invoice number and supplier name.\n"},"locale":{"type":"string","enum":["sk","cz","en","ro"],"description":"Locale for default subject/message templates.\nDefaults to the invoice's `pdf_language`.\n"},"attachment_blob_ids":{"type":"array","description":"Optional ActiveStorage blob IDs to attach in addition\nto the auto-generated PDF.\n","items":{"type":"string"}}}}}},"ProformaList":{"allOf":[{"$ref":"#/components/schemas/CursorPagination"},{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Proforma"}}}}]},"Proforma":{"type":"object","required":["id","type","status","currency","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"type":{"type":"string","enum":["proforma"]},"status":{"type":"string","enum":["draft","exposed","paid","unpaid","partially_paid","paid_above_price","canceled"],"description":"Proforma lifecycle state. Mirrors invoice status enums\nbecause proformas are payment-triggering documents.\n"},"number":{"type":"string","nullable":true},"issue_date":{"type":"string","format":"date","nullable":true},"delivery_date":{"type":"string","format":"date","nullable":true},"due_date":{"type":"string","format":"date","nullable":true},"paid_on":{"type":"string","format":"date","nullable":true},"days_to_due":{"type":"integer","nullable":true},"currency":{"type":"string"},"exchange_rate":{"type":"string","format":"decimal","nullable":true},"included_in_turnover":{"type":"boolean","nullable":true},"reverse_charge":{"type":"boolean","nullable":true},"reverse_charge_text":{"type":"string","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_amount":{"type":"string","format":"decimal","nullable":true},"payment_method":{"type":"string","nullable":true},"variable_symbol":{"type":"string","nullable":true},"constant_symbol":{"type":"string","nullable":true},"specific_symbol":{"type":"string","nullable":true},"order_number":{"type":"string","nullable":true},"text_opening":{"type":"string","nullable":true},"text_closing":{"type":"string","nullable":true},"footer_note":{"type":"string","nullable":true},"recipient_note":{"type":"string","nullable":true},"internal_note":{"type":"string","nullable":true},"pdf_language":{"type":"string","nullable":true},"pdf_locale":{"type":"string","nullable":true},"pdf_compact_mode":{"type":"boolean","nullable":true},"hide_signature":{"type":"boolean","nullable":true},"delivery_type":{"type":"string","nullable":true},"sent_status":{"type":"string","nullable":true},"email_sent_at":{"type":"string","format":"date-time","nullable":true},"seen_timestamp":{"type":"string","format":"date-time","nullable":true},"reminder_count":{"type":"integer","nullable":true},"auto_on_payment":{"type":"string","nullable":true,"description":"What should happen when a payment lands on this proforma.\nValues include `regular_invoice` (issue a final invoice),\n`tax_document` (issue a DPP / tax document for received\npayment), `nothing` (no auto-flow). When converting via\n`/convert_to_invoice`, this value is overridden to\n`regular_invoice` regardless of the stored setting.\n"},"purchaser_id":{"type":"integer","format":"int64","nullable":true},"supplier_id":{"type":"integer","format":"int64","nullable":true},"bank_account_id":{"type":"integer","format":"int64","nullable":true},"project_id":{"type":"integer","format":"int64","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"document_items":{"type":"array","items":{"$ref":"#/components/schemas/ProformaLineItem"}},"purchaser":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/InvoiceAccountEmbed"}]},"supplier":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/InvoiceAccountEmbed"}]}}},"ProformaLineItem":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"quantity":{"type":"string","format":"decimal","nullable":true},"measure_unit":{"type":"string","nullable":true},"unit_price":{"type":"string","format":"decimal","nullable":true},"vat_rate":{"type":"string","format":"decimal","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_unit_price":{"type":"string","format":"decimal","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ProformaCreateRequest":{"type":"object","required":["document"],"properties":{"document":{"type":"object","required":["purchaser_id"],"properties":{"number":{"type":"string"},"number_series_id":{"type":"integer","format":"int64"},"status":{"type":"string"},"issue_date":{"type":"string","format":"date"},"delivery_date":{"type":"string","format":"date"},"due_date":{"type":"string","format":"date"},"days_to_due":{"type":"integer","minimum":0},"currency":{"type":"string"},"exchange_rate":{"type":"string"},"payment_method":{"type":"string"},"variable_symbol":{"type":"string"},"constant_symbol":{"type":"string"},"specific_symbol":{"type":"string"},"order_number":{"type":"string"},"purchaser_id":{"type":"integer","format":"int64"},"bank_account_id":{"type":"integer","format":"int64"},"project_id":{"type":"integer","format":"int64"},"included_in_turnover":{"type":"string","enum":["0","1"]},"reverse_charge":{"type":"boolean"},"reverse_charge_text":{"type":"string"},"discount_description":{"type":"string"},"discount_percentage":{"type":"string"},"discount_amount":{"type":"string"},"text_opening":{"type":"string"},"text_closing":{"type":"string"},"footer_note":{"type":"string"},"recipient_note":{"type":"string"},"internal_note":{"type":"string"},"pdf_language":{"type":"string"},"pdf_locale":{"type":"string"},"auto_on_payment":{"type":"string","enum":["regular_invoice","regular_invoice_paid","tax_document","nothing"]},"document_items_attributes":{"type":"array","items":{"$ref":"#/components/schemas/ProformaLineItemInput"}}}}}},"ProformaLineItemInput":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string"},"order":{"type":"integer"},"quantity":{"type":"string"},"measure_unit":{"type":"string"},"unit_price":{"type":"string"},"vat_rate":{"type":"string"},"discount_description":{"type":"string"},"discount_unit_price":{"type":"string"},"discount_percentage":{"type":"string"},"_destroy":{"type":"string","enum":["true","false"]}}},"ProformaUpdateRequest":{"allOf":[{"$ref":"#/components/schemas/ProformaCreateRequest"},{"type":"object","description":"PATCH semantics — any subset of create request is valid."}]},"QuoteList":{"allOf":[{"$ref":"#/components/schemas/CursorPagination"},{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Quote"}}}}]},"Quote":{"type":"object","required":["id","type","status","currency","valid_until","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"type":{"type":"string","enum":["quote"],"description":"STI type discriminator. Always `quote` on this endpoint.\n"},"status":{"type":"string","enum":["draft","active","accepted","expired","canceled","exposed"],"description":"Quote lifecycle state. `active` for live offers sent to\nthe customer; `accepted` when the customer has agreed\n(set via `POST /:id/accept`); `expired` past\n`valid_until`; `canceled` for explicit retirement.\n`draft` for in-progress unfinished quotes; `exposed`\nis the model-wide default state.\n"},"number":{"type":"string","nullable":true,"description":"Quote number (formatted via number_series, e.g. 'CEN2026-0001'). Null for drafts."},"issue_date":{"type":"string","format":"date","nullable":true},"delivery_date":{"type":"string","format":"date","nullable":true},"valid_until":{"type":"string","format":"date","description":"Quote validity end date. Required at creation. The model\nrejects past dates on create unless the record originates\nfrom an import.\n"},"currency":{"type":"string","description":"ISO-4217 currency code."},"exchange_rate":{"type":"string","format":"decimal","nullable":true},"included_in_turnover":{"type":"boolean","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_amount":{"type":"string","format":"decimal","nullable":true},"text_opening":{"type":"string","nullable":true},"text_closing":{"type":"string","nullable":true},"footer_note":{"type":"string","nullable":true},"recipient_note":{"type":"string","nullable":true},"internal_note":{"type":"string","nullable":true},"pdf_language":{"type":"string","nullable":true},"pdf_locale":{"type":"string","nullable":true},"pdf_compact_mode":{"type":"boolean","nullable":true},"hide_signature":{"type":"boolean","nullable":true},"delivery_type":{"type":"string","nullable":true},"sent_status":{"type":"string","nullable":true},"email_sent_at":{"type":"string","format":"date-time","nullable":true},"seen_timestamp":{"type":"string","format":"date-time","nullable":true},"purchaser_id":{"type":"integer","format":"int64","nullable":true,"description":"InvoiceAccount ID of the customer."},"supplier_id":{"type":"integer","format":"int64","nullable":true,"description":"InvoiceAccount ID of the workspace's own legal entity."},"bank_account_id":{"type":"integer","format":"int64","nullable":true},"project_id":{"type":"integer","format":"int64","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"document_items":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"}},"purchaser":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/InvoiceAccountEmbed"}]},"supplier":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/InvoiceAccountEmbed"}]}}},"QuoteLineItem":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"order":{"type":"integer","nullable":true},"quantity":{"type":"string","format":"decimal","nullable":true},"measure_unit":{"type":"string","nullable":true},"unit_price":{"type":"string","format":"decimal","nullable":true},"vat_rate":{"type":"string","format":"decimal","nullable":true},"discount_description":{"type":"string","nullable":true},"discount_percentage":{"type":"string","format":"decimal","nullable":true},"discount_unit_price":{"type":"string","format":"decimal","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"QuoteCreateRequest":{"type":"object","required":["document"],"description":"Wrapper structure required by Rails strong params. The\ntop-level `document` key carries the quote fields.\n","properties":{"document":{"type":"object","required":["purchaser_id","valid_until"],"properties":{"number":{"type":"string"},"number_series_id":{"type":"integer","format":"int64"},"status":{"type":"string"},"issue_date":{"type":"string","format":"date"},"delivery_date":{"type":"string","format":"date"},"valid_until":{"type":"string","format":"date","description":"Required. Must be in the future."},"currency":{"type":"string"},"exchange_rate":{"type":"string"},"order_number":{"type":"string"},"purchaser_id":{"type":"integer","format":"int64","description":"InvoiceAccount ID of the customer. Required."},"bank_account_id":{"type":"integer","format":"int64"},"project_id":{"type":"integer","format":"int64"},"included_in_turnover":{"type":"string","enum":["0","1"]},"discount_description":{"type":"string"},"discount_percentage":{"type":"string"},"discount_amount":{"type":"string"},"text_opening":{"type":"string"},"text_closing":{"type":"string"},"footer_note":{"type":"string"},"recipient_note":{"type":"string"},"internal_note":{"type":"string"},"pdf_language":{"type":"string"},"pdf_locale":{"type":"string"},"pdf_compact_mode":{"type":"boolean"},"mandatory_text":{"type":"string"},"hide_prices":{"type":"boolean"},"document_items_attributes":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItemInput"}}}}}},"QuoteLineItemInput":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string"},"order":{"type":"integer"},"quantity":{"type":"string"},"measure_unit":{"type":"string"},"unit_price":{"type":"string"},"vat_rate":{"type":"string"},"discount_description":{"type":"string"},"discount_unit_price":{"type":"string"},"discount_percentage":{"type":"string"},"_destroy":{"type":"string","enum":["true","false"]}}},"QuoteUpdateRequest":{"allOf":[{"$ref":"#/components/schemas/QuoteCreateRequest"},{"type":"object","description":"PATCH semantics: any subset of the create request is valid.\n`valid_until` may be omitted on update; only provided fields\nare modified.\n"}]},"User":{"type":"object","required":["id","email","language","locale","created_at","updated_at","connected_providers","password_set"],"properties":{"id":{"type":"integer","format":"int64","description":"Internal user ID. Stable across token rotations."},"email":{"type":"string","format":"email","description":"The user's primary email address (used as the login identifier).","example":"jane@example.com"},"name":{"type":"string","nullable":true,"description":"The user's display name. Nil for users who registered\nvia OAuth and have not yet completed their profile.\n","example":"Jane Doe"},"language":{"type":"string","enum":["sk","cz","en","ro"],"description":"The user's preferred UI language. Drives `Accept-Language`\nfor emails and PDF documents when not overridden per-document.\n","example":"sk"},"locale":{"type":"string","description":"The user's preferred locale identifier (BCP 47-ish). Currently\nmirrors `language` but may diverge in the future for\nregional formatting (e.g. `cz-CZ` vs `sk-SK` decimal separators).\n","example":"sk"},"avatar_url":{"type":"string","format":"uri","nullable":true,"description":"Signed URL for the user's avatar image (ActiveStorage blob URL).\n`null` when no avatar is attached. URLs are time-limited —\nre-fetch the user record to get a fresh URL.\n","example":"https://app.lucanto.eu/rails/active_storage/blobs/.../avatar.png"},"connected_providers":{"type":"array","description":"List of OAuth providers connected to this account. Empty\narray if the user registered with email/password and never\nconnected any provider.\n","items":{"type":"string","example":"google_oauth2"},"example":["google_oauth2","apple"]},"password_set":{"type":"boolean","description":"`true` if the user has set a password (either at registration\nor after the fact). `false` only for users who registered via\nOAuth and have never set a password — they can't use the\npassword reset flow without first setting one.\n"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"parameters":{"AccountId":{"name":"account_id","in":"path","required":true,"description":"Workspace (account) ID. The numeric ID shown in your URLs.","schema":{"type":"integer","format":"int64","minimum":1},"example":42},"LimitParam":{"name":"limit","in":"query","required":false,"description":"Number of items per page. Default 25, maximum 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},"CursorParam":{"name":"cursor","in":"query","required":false,"description":"Pagination cursor from the previous response's `next_cursor`.","schema":{"type":"string"}},"IdempotencyKeyHeader":{"name":"Idempotency-Key","in":"header","required":false,"description":"Client-supplied unique key (recommended: UUID v4) for safe\nretries. Within a 24-hour window per (API key, key), repeating\nthe same key replays the original response instead of\nre-executing the action. Reusing a key with a different request\nbody returns `409 idempotency_key_conflict`.\n","schema":{"type":"string","maxLength":255},"example":"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"}},"responses":{"Unauthorized":{"description":"Missing or invalid authentication credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unauthenticated","message":"Authentication required.","request_id":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60","docs_url":"https://docs.lucanto.eu/api/v1/errors/unauthenticated"}}}},"Forbidden":{"description":"Authenticated, but the token's scope or the user's role does not\npermit this action. Returned by the two-layer scope enforcement\n(token scope cap AND CanCanCan role).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"forbidden","message":"You do not have permission to perform this action.","request_id":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60","docs_url":"https://docs.lucanto.eu/api/v1/errors/forbidden"}}}},"NotFound":{"description":"Resource does not exist, or exists but is not visible to the\nauthenticated principal (cross-account isolation returns 404,\nnot 403, to avoid leaking existence).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"not_found","message":"Resource not found.","request_id":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60","docs_url":"https://docs.lucanto.eu/api/v1/errors/not_found"}}}},"Conflict":{"description":"Resource conflict (e.g. duplicate that violates a unique constraint).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"conflict","message":"A record with these attributes already exists.","request_id":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60","docs_url":"https://docs.lucanto.eu/api/v1/errors/conflict"}}}},"IdempotencyKeyConflict":{"description":"The `Idempotency-Key` was previously used with a different\nrequest body. Generate a fresh key for genuinely different\nrequests.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"idempotency_key_conflict","message":"The Idempotency-Key was previously used with a different request body. Use a fresh key for a different request.","request_id":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60","docs_url":"https://docs.lucanto.eu/api/v1/errors/idempotency_key_conflict"}}}},"UnprocessableEntity":{"description":"Request validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"RateLimited":{"description":"Rate limit exceeded. Retry after the window resets.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests remaining in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix timestamp when the window resets."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"rate_limited","message":"Rate limit exceeded. Retry after 60 seconds.","request_id":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60","docs_url":"https://docs.lucanto.eu/api/v1/errors/rate_limited"}}}},"PaymentRequired":{"description":"Workspace has no active subscription. Visit the upgrade URL to\nsubscribe.\n","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"upgrade_url":{"type":"string","format":"uri","description":"URL to the billing/plans page"}}}]},"example":{"error":"subscription_required","message":"An active subscription is required to use the API.","request_id":"8f9e4c2b-0c1a-4f3e-9a8d-1b2c3d4e5f60","docs_url":"https://docs.lucanto.eu/api/v1/errors/subscription_required","upgrade_url":"https://app.lucanto.eu/42/billing/plans"}}}}}}}