{"openapi":"3.1.0","info":{"title":"PII Anonymization Pipeline","version":"1.0.0","x-chut-surfaces":[{"id":"upii","label":{"en":"PII Anonymization","fr":"Anonymisation PII"},"icon":"shield-check","category":"privacy","order":100,"description":"Detect personally identifiable information in text and documents and protect it with placeholders."}]},"paths":{"/auth/otp/request":{"post":{"tags":["auth"],"summary":"Otp Request","description":"Proxy OTP request to Chut Auth (avoids CORS).","operationId":"otp_request_auth_otp_request_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/otp/verify":{"post":{"tags":["auth"],"summary":"Otp Verify","description":"Proxy OTP verify to Chut Auth, then set cookies.\n\nR20: the sign-up form also posts optional ``profession`` (accountant|lawyer|notary|other), ``language`` (en|fr|nl)\nand, when its checkbox was shown, ``marketing_opt_in`` + ``marketing_opt_in_text`` (the exact wording displayed).\nThey are validated BEFORE the code is spent: a profession outside the list or a consent text that is not the\nconfigured wording is a 422 (``invalid_profession`` / ``invalid_marketing_consent``). They are used only to build\nthe HubSpot ``account.created`` event when this call creates the account; with HubSpot off nothing is stored.","operationId":"otp_verify_auth_otp_verify_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/otp-callback":{"post":{"tags":["auth"],"summary":"Otp Callback","description":"Handle OTP login — receives tokens directly from the frontend.","operationId":"otp_callback_auth_otp_callback_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","operationId":"logout_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[]}},"/auth/me":{"get":{"tags":["auth"],"summary":"Me","operationId":"me_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfile"}}}}},"security":[]}},"/keys":{"get":{"tags":["keys"],"summary":"List Keys","operationId":"list_keys_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/APIKeyInfo"},"type":"array","title":"Response List Keys Keys Get"}}}}}},"post":{"tags":["keys"],"summary":"Create Key","operationId":"create_key_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/keys/{key_id}":{"delete":{"tags":["keys"],"summary":"Revoke Key","operationId":"revoke_key_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["keys"],"summary":"Rename Key","operationId":"rename_key_keys__key_id__patch","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/overview":{"get":{"tags":["dashboard"],"summary":"Overview","operationId":"overview_dashboard_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardOverview"}}}}}}},"/dashboard/usage":{"get":{"tags":["dashboard"],"summary":"Usage","operationId":"usage_dashboard_usage_get","parameters":[{"name":"period","in":"query","required":false,"schema":{"type":"string","default":"7d","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageStats"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/matters":{"post":{"tags":["matters"],"summary":"Create Matter","description":"Create a new pseudonymisation workspace (matter / dossier / case file).\n\nA **matter** (also called *dossier*, *case file*, *mission*, *workspace*,\nor *pseudonymisation workspace*) is a scoped container for a client's\ndocuments.  All files uploaded to the same matter share a consistent\nentity map — the same person or company always receives the same pseudonym\nacross every document in the dossier.\n\nUse this when you need to:\n- Start a new RGPD / GDPR anonymisation project for a client\n- Create a dossier for données personnelles processing\n- Open a pseudonymisation workspace for a client file or case\n- Organise documents under a single matter / mission / affaire\n\n**Keywords**: matter · dossier · case file · workspace · client file ·\nmission · pseudonymisation workspace · RGPD · GDPR · données personnelles ·\nanonymisation · pseudonymisation · affaire · project\n\nCaptures the deploy's ``git_sha`` and the bundled corpus version at\ncreation time (reproducibility lock for EU AI Act Art.19).  Those fields\nare immutable through PATCH — to switch to a newer model/corpus the caller\nmust create a new matter.","operationId":"create_matter_matters_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMatterRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-tool-name":"upii.create_matter","x-side-effects":"writes","x-requires-confirmation":false,"x-execution-mode":"sync","x-surface":"upii","x-result-renders":["entity"],"x-idempotent":false,"x-upii-risk-class":"workspace-config","x-upii-retention":"indefinite","x-upii-writes-audit":true},"get":{"tags":["matters"],"summary":"List Matters","description":"List matters owned by the caller (same user, same tenant), newest first.\nA gateway caller first adopts all of its user's ``legacy:`` dossiers.\n\nArchived matters are hidden by default — pass ``include_archived=true``\nto see them. We deliberately do NOT support cross-user listing; that's\na future v3 collaboration feature.","operationId":"list_matters_matters_get","parameters":[{"name":"include_archived","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Archived"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MatterResponse"},"title":"Response List Matters Matters Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-tool-name":"upii.list_matters","x-side-effects":"none","x-requires-confirmation":false,"x-execution-mode":"sync","x-surface":"upii","x-result-renders":["entity"],"x-idempotent":true,"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false}},"/matters/{matter_id}":{"get":{"tags":["matters"],"summary":"Get Matter","operationId":"get_matter_matters__matter_id__get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false},"patch":{"tags":["matters"],"summary":"Update Matter","description":"Partial update. Only ``name``, ``description``, ``is_archived`` may\nchange — ``locale``, ``pseudonym_mode``, ``git_sha``, ``corpus_version``\nare immutable on purpose (reproducibility lock).","operationId":"update_matter_matters__matter_id__patch","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMatterRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"workspace-config","x-upii-retention":"indefinite","x-upii-writes-audit":true},"delete":{"tags":["matters"],"summary":"Delete Matter","description":"Archive the matter, or with ``purge=true`` erase it permanently.\n\nWithout ``purge`` this is the soft delete: the dossier is archived, an\narchived dossier is unreachable (``_get_user_matter`` answers 404 for it on\nevery content route) and nothing is erased; PATCH ``is_archived=false``\nrestores it. ``purge=true`` is the second, deliberate step (X4, C10): see\n``_purge_matter``. To erase a single file, use\n``DELETE /matters/{id}/files/{file_id}``.","operationId":"delete_matter_matters__matter_id__delete","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"purge","in":"query","required":false,"schema":{"type":"boolean","description":"true = erase an archived dossier permanently; absent/false = archive it.","default":false,"title":"Purge"},"description":"true = erase an archived dossier permanently; absent/false = archive it."}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"workspace-config","x-upii-retention":"indefinite","x-upii-writes-audit":true,"x-upii-notes":"Without purge: archives the dossier (idempotent, nothing erased). With purge=true: erases an ARCHIVED dossier for real (409 if it is not archived or a file is processing; gateway callers get 403): ONE matter.purge audit row (dossier id only) first, fail closed; then every original and anonymized object, then the entity map, rules, files and the dossier. A storage failure is a fixed 502 and nothing in the database is removed, so the call can be retried. The audit trail stays."}},"/matters/{matter_id}/usage":{"get":{"tags":["matters"],"summary":"Get Matter Usage","description":"Per-matter rollup for the detail UI's stat strip.\n\nReads from ``usage_log`` (one row per file-anonymize), not from\n``matter_files``, because usage_log is the source of truth for\naggregate counters and is indexed by ``(matter_id, created_at DESC)``\n— the same shape the regulator's PDF export will use in W7.","operationId":"get_matter_usage_matters__matter_id__usage_get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatterUsageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false}},"/matters/{matter_id}/files":{"post":{"tags":["matters"],"summary":"Upload File","description":"Accept a document, persist to Object Storage, enqueue for the worker.\n\nSide effects:\n    * INSERT into ``matter_files`` (status='pending')\n    * PUT to ``upii-matter-originals``\n    * RQ enqueue (best-effort)","operationId":"upload_file_matters__matter_id__files_post","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_file_matters__matter_id__files_post"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileSummary"}}}},"402":{"description":"Page quota used up or trial ended (`code` is `quota_exceeded`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-handling","x-upii-retention":"90d","x-upii-writes-audit":true,"x-upii-notes":"Originals are encrypted-at-rest and auto-expire at 90d."},"get":{"tags":["matters"],"summary":"List Matter Files","operationId":"list_matter_files_matters__matter_id__files_get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FileSummary"},"title":"Response List Matter Files Matters  Matter Id  Files Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false}},"/matters/{matter_id}/files/reprocess":{"post":{"tags":["matters"],"summary":"Reprocess Matter Files","description":"Queue the finished (or failed) files of the dossier again.\n\nFiles processed early in a dossier were anonymised before later files\ntaught the dossier its names. A re-run applies the dossier's whole\nentity map to every file (#81). Files still pending or processing are\nleft alone. Tokens are stable, so re-running changes coverage only.\n\nWith ``{\"file_ids\": [...]}`` only those files of THIS dossier that are\n``done`` or ``error`` are queued (J1c). Any other id (unknown, another\ndossier, another tenant, still running) is counted in ``skipped`` and the\nanswer is the same for all of them. No body keeps the old behaviour.","operationId":"reprocess_matter_files_matters__matter_id__files_reprocess_post","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ReprocessRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReprocessResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-handling","x-upii-retention":"90d","x-upii-writes-audit":true,"x-upii-notes":"Re-anonymises every finished file, or only the selected ones, with everything the dossier knows now."}},"/matters/{matter_id}/files/download-all":{"get":{"tags":["matters"],"summary":"Download All Files","description":"Zip of every ready (``done``) anonymised file in the dossier.\n\n``ids`` (comma-separated file ids) restricts the archive to a subset\n— the home page uses it for \"Download all ready\" on today's queue.\n404 when nothing is ready. Originals are never included.","operationId":"download_all_files_matters__matter_id__files_download_all_get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ids"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-handling","x-upii-retention":"transient","x-upii-writes-audit":true,"x-upii-notes":"Streams a zip of the dossier's anonymised outputs (status=done). Writes one file.download_all audit row."}},"/matters/{matter_id}/files/{file_id}":{"get":{"tags":["matters"],"summary":"Get Matter File","operationId":"get_matter_file_matters__matter_id__files__file_id__get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileSummary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false},"delete":{"tags":["matters"],"summary":"Delete Matter File","description":"Delete one file for real.\n\nOrder: ownership (404 across tenants and for an archived dossier) → file\nin this dossier (404) → not ``processing`` (409) → ONE ``file.delete``\naudit row, fail closed → the original and the anonymized object are\ndeleted (idempotent; any failure is a fixed 502 and the row stays so the\nuser can retry) → in one transaction the entity rows stop naming the file\n(``entity_map.first_file_id`` has no ON DELETE) and the row is deleted.\nA repeat is 404.\n\nThe ``status <> 'processing'`` condition on the final DELETE closes the\nwindow between the status check and the object deletes: a worker that\nclaimed the file in between keeps its row (409), so the output it is\nwriting is never orphaned by a row that no longer exists.","operationId":"delete_matter_file_matters__matter_id__files__file_id__delete","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-handling","x-upii-retention":"indefinite","x-upii-writes-audit":true,"x-upii-notes":"Real delete of one file: the original and the anonymized object are deleted from Object Storage, then the row. ONE file.delete audit_log row (detail = file id only) is written BEFORE anything is deleted; if that write fails the call returns 500 and nothing is deleted. A storage failure is a fixed 502 and the row is kept so the call can be retried. 409 while the file is processing. Gateway (platform / AI) callers get 403. The dossier's entity map is not touched (only first_file_id is cleared): it stays until the dossier is purged."}},"/matters/{matter_id}/files/{file_id}/download":{"get":{"tags":["matters"],"summary":"Download Matter File","description":"Return a short-lived presigned URL for the requested artifact.\n\n``type=anonymized`` (default) — the worker's output. 404 if the file\nis still ``pending`` or ``processing``; 409 if it errored.\n\n``type=original`` — the as-uploaded file. Available immediately after\nupload, locked behind the matter-ownership check; gateway\n(platform / AI) callers get 403. Every successful call writes one\n``file.download_original`` audit row (file id only, never the file\nname) after the ownership/file/key checks and before the URL is\nsigned; if the row cannot be written the call fails with a fixed 500\nand no URL is signed. ``type=anonymized`` is not audited here.","operationId":"download_matter_file_matters__matter_id__files__file_id__download_get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}},{"name":"type","in":"query","required":false,"schema":{"type":"string","default":"anonymized","title":"Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileDownloadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-reveal","x-upii-retention":"transient","x-upii-writes-audit":true,"x-upii-notes":"Returns a presigned URL (≤ 1 h). Original-file download surfaces non-anonymized PII — caller scope is the matter owner only. type=original writes ONE file.download_original audit_log row (detail = file id only) BEFORE the URL is signed; if that write fails the call returns 500 and no URL. type=anonymized writes no audit row."}},"/matters/{matter_id}/files/{file_id}/text":{"get":{"tags":["matters"],"summary":"Preview the protected text of a finished file","description":"Return the protected text of a ``done`` file, page by page.\n\nReads the delivered output object (so what you see is what a download gives) and returns its text\nas view pages: PDF page by page, DOCX paragraphs and table cells in document order, TXT/CSV as is.\nA long page is split into consecutive pages. At most ``preview_max_pages`` pages and\n``preview_max_chars`` characters are returned per call; ``truncated`` is true when more follows,\nthen ask again with ``page_from`` set to the last returned page + 1.\n\n404 for another tenant, an archived dossier, an unknown file or a file whose object is missing;\n409 unless the file is ``done``; 413 for an object over the cap; 415 for a type that cannot be\npreviewed; 422 for an object that cannot be read. Messages are fixed text, never storage detail.","operationId":"preview_matter_file_text","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}},{"name":"page_from","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"First view page to return (1-based).","default":1,"title":"Page From"},"description":"First view page to return (1-based)."},{"name":"page_to","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Last view page to return; the response is still capped by the server's per-response limits.","title":"Page To"},"description":"Last view page to return; the response is still capped by the server's per-response limits."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FilePreviewResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-handling","x-upii-retention":"transient","x-upii-writes-audit":false,"x-upii-notes":"Protected text only: extracted from the DELIVERED object (what the download gives), never from the original or the entity map. Detected personal data is replaced by tokens, but context can still identify someone. Capped per response (preview_max_pages / preview_max_chars). Writes no audit row, like the anonymized download. Allowed for session, API-key and gateway callers."}},"/files":{"post":{"tags":["files"],"summary":"Upload File To Default","description":"Upload without choosing a dossier first.\n\n``matter_id`` (form field) targets an existing dossier — same as\n``POST /matters/{id}/files`` but with the dossier echoed back.\nWithout it, the file lands in the caller's default dossier; ``locale``\nonly matters the first time, when that dossier is created.","operationId":"upload_file_to_default_files_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_file_to_default_files_post"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileSummaryWithMatter"}}}},"402":{"description":"Page quota used up or trial ended (`code` is `quota_exceeded`)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-handling","x-upii-retention":"90d","x-upii-writes-audit":true,"x-upii-notes":"Drop-zone shortcut: no matter_id → the caller's default dossier (\"Inbox\") is used, created on first upload."}},"/files/recent":{"get":{"tags":["files"],"summary":"Recent Files","description":"Newest files across every dossier the caller owns (same user, same tenant).\n\nBacks the home page's Today / Earlier lists. ``limit`` is clamped to\n1..100; ``status`` optionally filters on the worker state machine.","operationId":"recent_files_files_recent_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FileSummaryWithMatter"},"title":"Response Recent Files Files Recent Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false}},"/files/download-all":{"get":{"tags":["files"],"summary":"Download All Across Dossiers","description":"Zip of the given ready files, whatever dossier they belong to.\n\nThe home page's Today queue can span dossiers; one click must give one\narchive, because browsers block a page that starts several downloads\nat once. ``ids`` is required (comma-separated, ≤ 200) and every id is\nchecked against the caller — foreign, unknown and archived-dossier ids are\nsimply absent.","operationId":"download_all_across_dossiers_files_download_all_get","parameters":[{"name":"ids","in":"query","required":true,"schema":{"type":"string","title":"Ids"}}],"responses":{"200":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-handling","x-upii-retention":"transient","x-upii-writes-audit":true,"x-upii-notes":"Cross-dossier zip of the caller's ready outputs (ids required). One file.download_all audit row per dossier touched."}},"/matters/{matter_id}/text":{"post":{"tags":["matters"],"summary":"Protect Text","operationId":"protect_text_matters__matter_id__text_post","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextProtectRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextProtectResponse"}}}},"402":{"description":"Page quota used up or trial ended (`code` is `quota_exceeded`)."},"202":{"description":"Not finished in time: poll the file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextQueued"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-handling","x-upii-retention":"90d","x-upii-writes-audit":true,"x-upii-notes":"Protects a pasted text inline (20 s budget, else queued like an upload). Pooled metering: 1 page per 3,000 characters over the month, no minimum per paste."}},"/matters/{matter_id}/entities":{"get":{"tags":["matters"],"summary":"List Entities","description":"List entity-map rows for a matter. NEVER returns original_enc.","operationId":"list_entities_matters__matter_id__entities_get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EntityListItem"},"title":"Response List Entities Matters  Matter Id  Entities Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false,"x-upii-notes":"Returns placeholder + display_value only. original_enc never leaves the DB on this route."}},"/matters/{matter_id}/repersonalize":{"post":{"tags":["matters"],"summary":"Repersonalize","description":"Replace every ``{{TYPE:tag}}`` token in the input with its original value.\n\nA token the AI damaged slightly (``{{ person:... }}``, ``[[ORG:...]]``) is restored too, but only when its exact form\nis a token of THIS dossier; the grammar is ``api/restore_tokens.py`` (PLAN.md, T2). What looks like a token and\ncannot be resolved is returned as ``damaged`` and left as written.\n\nTokens that have no matching entity_map row are returned as\n``orphans`` so the UI can warn the user — typical cause is that the\nuser is re-personalizing a different matter than the one the text\nwas anonymized in (the v2 plan flagged this as the #1 UX risk).\n\nAudit policy: one ``entity.repersonalize`` row per successful call\n(a call with no tokens included), written before the text is\nreturned. ``detail`` holds the two counts only. Fail closed: if the\nrow cannot be written the caller gets a fixed 500 and no text.","operationId":"repersonalize_matters__matter_id__repersonalize_post","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepersonalizeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepersonalizeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-reveal","x-upii-retention":"transient","x-upii-writes-audit":true,"x-upii-notes":"Bulk reveal: decrypts every bracket-form token in the input, and every damaged one that names exactly one token of THIS dossier (api/restore_tokens.py). Every successful call writes ONE summary audit_log row (action entity.repersonalize, detail = counts only: substituted, orphans, damaged, normalised) BEFORE the text is returned; no token or original value is ever recorded. If the audit write fails the call returns 500 and no text. Append-only — DDL trigger blocks UPDATE/DELETE."}},"/matters/{matter_id}/entities/{placeholder}/reveal":{"post":{"tags":["matters"],"summary":"Reveal Entity","description":"Unmask one entity. EVERY call writes an immutable audit row.\n\nAuth: caller must own the matter (404 otherwise; we don't leak\nexistence across tenants).\n\nAudit policy:\n  * One row per call, written *before* the response is sent. A\n    request that 500s after the audit row is acceptable — auditors\n    prefer over-logging to under-logging.\n  * Failed reveals (decrypt errors, missing entity_map row, missing\n    master key) are also logged with ``action='entity.reveal.error'``\n    so a regulator can see attempted unmasks even when they didn't\n    succeed.\n  * ``detail`` carries the entity type and the reason CODE (closed set,\n    ``RevealReason``; null when omitted) — never free text, never a plaintext\n    value. The table is immutable and survives a dossier purge (S3/F4).","operationId":"reveal_entity_matters__matter_id__entities__placeholder__reveal_post","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"placeholder","in":"path","required":true,"schema":{"type":"string","title":"Placeholder"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RevealRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevealResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"pii-reveal","x-upii-retention":"indefinite","x-upii-writes-audit":true,"x-upii-notes":"Every call writes one audit_log row (success or failure). Append-only — DDL trigger blocks UPDATE/DELETE."}},"/matters/{matter_id}/audit":{"get":{"tags":["matters"],"summary":"List Audit","description":"Return the matter's audit timeline, newest first.\n\nPagination: limit + offset (max 200/page, capped offset 100K — beyond\nthat a regulator wants the PDF export, not an infinite scroll).","operationId":"list_audit_matters__matter_id__audit_get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":100000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditPage"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false,"x-upii-notes":"JSON timeline. PDF register lives at /matters/{id}/audit/export.pdf."}},"/matters/{matter_id}/audit/export.pdf":{"get":{"tags":["matters"],"summary":"Export Audit Pdf","description":"Render the matter's complete audit timeline as a PDF.\n\nThe export itself is logged (``audit.exported``) — meta-auditable\nby design. The endpoint renders up to ``AUDIT_EXPORT_MAX_ROWS`` (50 000) rows; beyond that, ask\nthe regulator if they want a paginated archive (a CSV companion is\na v3 backlog item, not v2).","operationId":"export_audit_pdf_matters__matter_id__audit_export_pdf_get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"responses":{"200":{"description":"Audit register as a self-contained PDF.","content":{"application/json":{"schema":{}},"application/pdf":{}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"regulated-output","x-upii-retention":"indefinite","x-upii-writes-audit":true}},"/matters/{matter_id}/rules":{"get":{"tags":["matters"],"summary":"List Rules","description":"List rules ordered by priority asc (so the UI matches the worker's\nevaluation order).","operationId":"list_rules_matters__matter_id__rules_get","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RuleResponse"},"title":"Response List Rules Matters  Matter Id  Rules Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false},"post":{"tags":["matters"],"summary":"Create Rule","description":"Create one literal rule. Pydantic has already validated that the\nmatch type is ``literal`` (regex is a 422) and the action/replacement\ncombination.","operationId":"create_rule_matters__matter_id__rules_post","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RuleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"workspace-config","x-upii-retention":"indefinite","x-upii-writes-audit":true,"x-upii-notes":"Writes one word.added audit_log row (no word text); application emits rule.applied."}},"/matters/{matter_id}/rules/{rule_id}":{"delete":{"tags":["matters"],"summary":"Delete Rule","description":"Hard delete. Rules are tiny; soft-delete adds complexity for no\nregulatory benefit (the audit_log 'rule.applied' history is the\ndurable record).\n\nReturns a 204 No Content. The ``response_class=Response`` +\nexplicit ``Response`` return are required: FastAPI refuses to start\na 204 route that would otherwise infer a JSON body from the return\nannotation.","operationId":"delete_rule_matters__matter_id__rules__rule_id__delete","parameters":[{"name":"matter_id","in":"path","required":true,"schema":{"type":"string","title":"Matter Id"}},{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","title":"Rule Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"workspace-config","x-upii-retention":"indefinite","x-upii-writes-audit":false}},"/words":{"get":{"tags":["words"],"summary":"List Words","description":"The caller's account words in the order the worker evaluates them.","operationId":"list_words_words_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/WordResponse"},"type":"array","title":"Response List Words Words Get"}}}}},"x-upii-risk-class":"read-only-meta","x-upii-retention":"transient","x-upii-writes-audit":false},"post":{"tags":["words"],"summary":"Create Word","description":"Create one literal account word. 409 for a duplicate and at the 200-word cap.","operationId":"create_word_words_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WordResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"workspace-config","x-upii-retention":"indefinite","x-upii-writes-audit":true,"x-upii-notes":"Writes one word.added audit_log row (no word text) into each live project of the account, plus a counts-only log record."}},"/words/{word_id}":{"delete":{"tags":["words"],"summary":"Delete Word","description":"Hard delete. Somebody else's word and an unknown id are the same 404.","operationId":"delete_word_words__word_id__delete","parameters":[{"name":"word_id","in":"path","required":true,"schema":{"type":"string","title":"Word Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-upii-risk-class":"workspace-config","x-upii-retention":"indefinite","x-upii-writes-audit":true}},"/billing/checkout":{"post":{"tags":["billing"],"summary":"Start a subscription checkout","description":"Creates (or reuses) the signed-in user's Mollie customer and a first payment for the chosen plan and interval, and returns the hosted checkout URL. Nothing about the user's plan changes until Mollie reports the payment paid on the webhook. The amount is the GROSS: the plan price (excluding VAT) plus the VAT of the buyer's saved billing profile (see `/billing/quote`), recorded in the payment metadata. Session only. 409 for a comp-tier account or a user who already has a running subscription, and 409 with `code` `billing_profile_required` when no billing profile is saved yet (`PUT /billing/profile` first); 503 when billing is not configured; 502 when Mollie fails.","operationId":"checkout_billing_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/quote":{"get":{"tags":["billing"],"summary":"The price a checkout would charge, with VAT","description":"For a plan and interval, the net price (excluding VAT), the VAT rate and amount, and the TOTAL that `POST /billing/checkout` would ask Mollie for, worked out from the signed-in user's SAVED billing profile (country, and `vat_valid` which only a VIES confirmation sets): Belgium 21%, an EU business with a confirmed VAT id 0% (reverse charge), an EU buyer without one the OSS rate of the buyer's country, outside the EU 0%. Read-only, calls no one, answers even when billing is not configured. 409 with `code` `billing_profile_required` when no profile is saved; 422 for an unknown plan or interval. Session only.","operationId":"quote_billing_quote_get","parameters":[{"name":"plan","in":"query","required":true,"schema":{"enum":["starter","pro","studio"],"type":"string","description":"The paid plan.","title":"Plan"},"description":"The paid plan."},{"name":"interval","in":"query","required":true,"schema":{"enum":["month","year"],"type":"string","description":"Billing interval.","title":"Interval"},"description":"Billing interval."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/change-quote":{"get":{"tags":["billing"],"summary":"What changing to another plan would do","description":"Read-only, calls no one. For a running paid subscription and a target plan of the SAME interval: the direction (`upgrade` takes effect as soon as the prorated difference is paid, `downgrade` at the next renewal), for an upgrade the net, VAT rate, VAT and GROSS charged today ((new price - old price) x remaining days / total days, VAT as checkout) and the gross charged at every renewal from the next period on, for a downgrade the renewal date and that new gross. 409 with `code` (`no_running_subscription`, `comp_account`, `subscription_canceled`, `subscription_past_due`, `same_plan`, `interval_mismatch`, `billing_profile_required`, `period_nearly_over`). Session only.","operationId":"change_quote_billing_change_quote_get","parameters":[{"name":"plan","in":"query","required":true,"schema":{"enum":["starter","pro","studio"],"type":"string","description":"The plan to change to.","title":"Plan"},"description":"The plan to change to."},{"name":"interval","in":"query","required":false,"schema":{"anyOf":[{"enum":["month","year"],"type":"string"},{"type":"null"}],"description":"Must equal the current interval.","title":"Interval"},"description":"Must equal the current interval."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeQuoteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/change-plan":{"post":{"tags":["billing"],"summary":"Change the plan of a running subscription","description":"Upgrade: records the change, recomputes the prorated gross on the server (the request carries no amount) and asks Mollie for ONE one-off payment on the customer's valid mandate; the plan switches only when the webhook sees that payment paid (`change_state` `awaiting_payment` until then; SEPA takes days). Repeating the request returns the open change and never creates a second payment. Downgrade: scheduled for the next renewal (`change_state` `scheduled`), no charge, no refund; the current plan stays until then. 409 with `code`: see `/billing/change-quote`, plus `no_valid_mandate` (start a fresh `/billing/checkout`), `plan_change_in_progress`, `pending_downgrade_exists`, `subscription_changed`. 503 when billing is not configured; 502 when Mollie fails (an upgrade may then be left `creating`: repeat the request). Session only.","operationId":"change_plan_billing_change_plan_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePlanResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/change-plan/cancel":{"post":{"tags":["billing"],"summary":"Drop the scheduled downgrade","description":"Cancels the pending downgrade, so the current plan continues at renewal; repeating it is harmless (`change_state` `none`). An upgrade whose payment is already created cannot be cancelled. Same 409 codes as `/billing/change-plan`; 503 when billing is not configured. Session only.","operationId":"cancel_change_plan_billing_change_plan_cancel_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelChangeResponse"}}}}}}},"/billing/status":{"get":{"tags":["billing"],"summary":"Current plan, quota and the plan table","description":"The signed-in user's entitlements (tier, plan, page quota and use, period, state, read_only; the quota is monthly on every paid plan, `pages_used` counts the current monthly window and `usage_resets_at` is when it restarts, null for the trial) and the fixed plan table, plus `plan_change` (W6b): `pending_plan` and `pending_effective_at` of a scheduled downgrade, `change_state` (`none`, `creating`, `awaiting_payment`, `scheduled`, `applied`, `failed`, `paid_unapplied`), `change_target` and `needs_provider_sync` (a payment went through but the Mollie subscription still has to be updated), and `last_first_payment` (`status` and `age_seconds` of the newest first payment the webhook finished, else null: how the page learns that a checkout was cancelled, failed or expired; it changes nothing). Read-only; answers even when billing is not configured (`billing_available` false). Session only.","operationId":"status_billing_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing/cancel":{"post":{"tags":["billing"],"summary":"Cancel the subscription at the end of the paid period","description":"Cancels the Mollie subscription and marks the user's subscription `canceled` with `cancel_at_period_end`; access continues until `period_end` (C30). Repeating the call is harmless. 409 when there is no paid subscription to cancel; 503 when billing is not configured; 502 when Mollie fails. Session only.","operationId":"cancel_billing_cancel_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelResponse"}}}}}}},"/billing/resume":{"post":{"tags":["billing"],"summary":"Undo a cancellation while the paid period still runs","description":"For a subscription cancelled with `/billing/cancel` whose paid period has not ended: creates a new Mollie subscription from the customer's valid mandate, with the same plan and interval, starting on the day the period ends, and marks the subscription `active` again. Repeating the call, or calling it on a running subscription, is harmless and calls nothing. The new subscription charges the amount (net + VAT) of the user's latest invoice for this plan and interval. 409 when there is nothing to resume (no cancelled subscription, the period is over or ends today, no such invoice to take the price from, or Mollie holds no valid mandate any more: subscribe again with `/billing/checkout`); 503 when billing is not configured; 502 when Mollie fails. Session only.","operationId":"resume_billing_resume_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResumeResponse"}}}}}}},"/billing/profile":{"get":{"tags":["billing"],"summary":"The invoice details of the signed-in user","description":"Name, address, country and VAT id the invoices are made out to, with `vat_valid` (true only when VIES confirmed the id). `{\"profile\": null}` until the user has saved one; a payment made before that is invoiced under the account email at Belgian VAT. Read-only. Session only.","operationId":"get_profile_billing_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"put":{"tags":["billing"],"summary":"Save the invoice details","description":"Replaces the signed-in user's invoice details. Country is an ISO 3166-1 alpha-2 code; a VAT id is shape-checked for its country (422 on a wrong shape) and, for an EU country, checked against VIES. `vat_status` is `valid`, `invalid`, `unverified` (VIES did not answer: the message says it could not be verified), `not_checked` (non-EU) or `none`. Only `valid` sets `vat_valid`, which is what reverse charge requires. Write-limited (group billing). Session only.","operationId":"put_profile_billing_profile_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileSaved"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/invoices":{"get":{"tags":["billing"],"summary":"The signed-in user's invoices and credit notes","description":"Newest first, at most 200: id, number, kind (`invoice` or `credit_note`), issue date, gross amount in cents, currency, status (`paid`, `credited` when a credit note exists, `credit_note`) and mail_status (`not_sent`, `sent`, `failed`). No PDF bytes: fetch them with `/billing/invoices/{invoice_id}/pdf`. Read-only. Session only.","operationId":"list_invoices_billing_invoices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceList"}}}}}}},"/billing/invoices/{invoice_id}/pdf":{"get":{"tags":["billing"],"summary":"Download one invoice as a PDF","description":"The stored PDF of one of the signed-in user's invoices or credit notes, as an attachment named after its number. 404 for an unknown id, for an id that belongs to someone else and for a malformed id (no existence oracle). Not cached. Session only.","operationId":"invoice_pdf_billing_invoices__invoice_id__pdf_get","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string","title":"Invoice Id"}}],"responses":{"200":{"description":"The invoice PDF.","content":{"application/json":{"schema":{}},"application/pdf":{}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health":{"get":{"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[]}},"/health/detailed":{"get":{"summary":"Health Detailed","description":"Per-worker memory + inference queue depth + recent 5xx count.\n\nPublic (no auth). Use to correlate degradation with backend pressure.\nSnapshot is taken from this worker only; behind a load balancer you\nwill see different snapshots on consecutive calls — that is intended,\neach worker reports its own state.","operationId":"health_detailed_health_detailed_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[]}},"/version":{"get":{"summary":"Version","description":"Build + corpus + model fingerprint for audit-log pinning.\n\nReturned fields are stable across requests and across the lifetime of\na single container. A new deploy produces a new ``git_sha`` and a new\n``corpus_version``; if either changes, callers that depend on stable\npseudonymization output should re-run their pipeline.","operationId":"version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[]}},"/platform/health":{"get":{"summary":"Platform Health","description":"Auth-gated health probe for Chut platform contract suite.","operationId":"platform_health_platform_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/protect":{"post":{"summary":"Protect text","description":"Protect a text: replace the personal data detected in it with placeholders.\n\n``POST /anonymize`` is the same handler, kept as a deprecated alias for\nexisting integrations.","operationId":"protect_protect_post","parameters":[{"name":"include_values","in":"query","required":false,"schema":{"type":"boolean","description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values.","default":false,"title":"Include Values"},"description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnonymizeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnonymizeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-tool-name":"upii.protect_text","x-side-effects":"none","x-requires-confirmation":false,"x-execution-mode":"sync","x-surface":"upii","x-result-renders":["entity","text"]}},"/anonymize":{"post":{"summary":"Protect text (deprecated alias of POST /protect)","description":"Deprecated, kept for existing integrations: identical to `POST /protect`. Use `/protect`.","operationId":"anonymize_anonymize_post","deprecated":true,"parameters":[{"name":"include_values","in":"query","required":false,"schema":{"type":"boolean","description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values.","default":false,"title":"Include Values"},"description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnonymizeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnonymizeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-tool-name":"upii.anonymize_text","x-side-effects":"none","x-requires-confirmation":false,"x-execution-mode":"sync","x-surface":"upii","x-result-renders":["entity","text"]}},"/batch":{"post":{"summary":"Batch Anonymize","operationId":"batch_anonymize_batch_post","parameters":[{"name":"include_values","in":"query","required":false,"schema":{"type":"boolean","description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values.","default":false,"title":"Include Values"},"description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnonymizeResponse"},"title":"Response Batch Anonymize Batch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-tool-name":"upii.anonymize_batch","x-side-effects":"none","x-requires-confirmation":false,"x-execution-mode":"sync","x-surface":"upii","x-result-renders":["entity","text"]}},"/protect-document":{"post":{"summary":"Protect a document","description":"Protect a document (PDF, DOCX, XLSX, CSV, TXT).\n\nExtracts text page-by-page, pseudonymises each page (chunked detection),\nand returns per-page results. ``POST /anonymize-document`` is the same\nhandler, kept as a deprecated alias for existing integrations.\n\n``pseudonym`` controls placeholder format and ``locale`` selects the\nrealistic-name corpus (ignored unless ``pseudonym=realistic``) — see\n/protect.","operationId":"protect_document_protect_document_post","parameters":[{"name":"include_values","in":"query","required":false,"schema":{"type":"boolean","description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values.","default":false,"title":"Include Values"},"description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_protect_document_protect_document_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-tool-name":"upii.protect_document","x-side-effects":"none","x-requires-confirmation":false,"x-execution-mode":"async","x-surface":"upii","x-result-renders":["entity","table","doc_link"]}},"/anonymize-document":{"post":{"summary":"Protect a document (deprecated alias of POST /protect-document)","description":"Deprecated, kept for existing integrations: identical to `POST /protect-document`. Use `/protect-document`.","operationId":"anonymize_document_anonymize_document_post","deprecated":true,"parameters":[{"name":"include_values","in":"query","required":false,"schema":{"type":"boolean","description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values.","default":false,"title":"Include Values"},"description":"API-key callers only: add the original value of every entity (entities[].value), for trusted local review. Offsets keep their meaning either way — start/end index the submitted text, out_start/out_end the protected text. Gateway-signed, session-cookie and demo requests never carry originals and are rejected with 400 when they ask. Default: no original values."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_anonymize_document_anonymize_document_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-tool-name":"upii.anonymize_document","x-side-effects":"none","x-requires-confirmation":false,"x-execution-mode":"async","x-surface":"upii","x-result-renders":["entity","table","doc_link"]}}},"components":{"schemas":{"APIKeyInfo":{"properties":{"id":{"type":"string","title":"Id"},"key_prefix":{"type":"string","title":"Key Prefix"},"name":{"type":"string","title":"Name"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"type":"string","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"}},"type":"object","required":["id","key_prefix","name","is_active","created_at","last_used_at"],"title":"APIKeyInfo"},"AnonymizeRequest":{"properties":{"text":{"type":"string","maxLength":50000,"minLength":1,"title":"Text"},"mode":{"type":"string","pattern":"^(anonymize|detect|redact)$","title":"Mode","default":"anonymize"},"generalization":{"anyOf":[{"type":"string","pattern":"^(none|moderate|strict)$"},{"type":"null"}],"title":"Generalization"},"pseudonym":{"type":"string","pattern":"^(counter|stable|realistic)$","title":"Pseudonym","description":"Placeholder strategy. 'counter' (default) gives per-request positional labels (PERSON_1, PERSON_2…). 'stable' gives a per-tenant deterministic tag (PERSON_<10-hex>) so the same input value maps to the same placeholder across requests within your account. 'realistic' substitutes a synthetic real-looking name from the locale corpus (Pierre Lemaire), still deterministic per tenant — useful for downstream LLM training on pseudonymised text. Frequency analysis on tags is possible — see API docs.","default":"counter"},"locale":{"type":"string","pattern":"^(fr_BE|nl_BE|de_BE)$","title":"Locale","description":"Locale used by 'pseudonym=realistic' to pick a name corpus. Ignored for other pseudonym modes. Defaults to 'fr_BE'.","default":"fr_BE"}},"type":"object","required":["text"],"title":"AnonymizeRequest"},"AnonymizeResponse":{"properties":{"anonymized":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anonymized"},"entities":{"items":{"$ref":"#/components/schemas/EntityOut"},"type":"array","title":"Entities"},"relationships":{"items":{"$ref":"#/components/schemas/RelationshipOut"},"type":"array","title":"Relationships"},"coverage":{"$ref":"#/components/schemas/CoverageOut"},"latency":{"$ref":"#/components/schemas/LatencyOut"}},"type":"object","required":["entities","relationships","coverage","latency"],"title":"AnonymizeResponse"},"AuditEntry":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"action":{"type":"string","title":"Action"},"user_id":{"type":"string","title":"User Id"},"user_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Email"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"},"entity_placeholder":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Placeholder"},"entity_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Type"},"detail":{"additionalProperties":true,"type":"object","title":"Detail"},"reason_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason Label"}},"type":"object","required":["id","created_at","action","user_id","ip_address","entity_placeholder","entity_type","detail"],"title":"AuditEntry","description":"One row of the per-matter timeline.\n\n``entity_placeholder`` is populated from ``entity_map`` via LEFT JOIN\nwhen the row carries an ``entity_map_id`` — the UI uses it to render\n\"Reveal of {{PERSON:abc1234567}}\" without a second round-trip."},"AuditPage":{"properties":{"entries":{"items":{"$ref":"#/components/schemas/AuditEntry"},"type":"array","title":"Entries"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["entries","total","limit","offset"],"title":"AuditPage"},"BatchRequest":{"properties":{"texts":{"items":{"type":"string","maxLength":50000,"minLength":1},"type":"array","maxItems":100,"minItems":1,"title":"Texts"},"mode":{"type":"string","pattern":"^(anonymize|detect|redact)$","title":"Mode","default":"anonymize"},"pseudonym":{"type":"string","pattern":"^(counter|stable|realistic)$","title":"Pseudonym","description":"See /protect. Applied to every text in the batch under the same caller's tenant secret.","default":"counter"},"locale":{"type":"string","pattern":"^(fr_BE|nl_BE|de_BE)$","title":"Locale","description":"Locale for realistic substitution; ignored for other pseudonym modes.","default":"fr_BE"}},"type":"object","required":["texts"],"title":"BatchRequest"},"Body_anonymize_document_anonymize_document_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"pseudonym":{"type":"string","pattern":"^(counter|stable|realistic)$","title":"Pseudonym","default":"counter"},"locale":{"type":"string","pattern":"^(fr_BE|nl_BE|de_BE)$","title":"Locale","default":"fr_BE"}},"type":"object","required":["file"],"title":"Body_anonymize_document_anonymize_document_post"},"Body_protect_document_protect_document_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"pseudonym":{"type":"string","pattern":"^(counter|stable|realistic)$","title":"Pseudonym","default":"counter"},"locale":{"type":"string","pattern":"^(fr_BE|nl_BE|de_BE)$","title":"Locale","default":"fr_BE"}},"type":"object","required":["file"],"title":"Body_protect_document_protect_document_post"},"Body_upload_file_matters__matter_id__files_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_file_matters__matter_id__files_post"},"Body_upload_file_to_default_files_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"matter_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Matter Id"},"locale":{"type":"string","pattern":"^(fr_BE|nl_BE|de_BE)$","title":"Locale","default":"fr_BE"}},"type":"object","required":["file"],"title":"Body_upload_file_to_default_files_post"},"CancelChangeResponse":{"properties":{"change_state":{"type":"string","enum":["none","canceled"],"title":"Change State"},"needs_provider_sync":{"type":"boolean","title":"Needs Provider Sync"}},"type":"object","required":["change_state","needs_provider_sync"],"title":"CancelChangeResponse"},"CancelResponse":{"properties":{"state":{"type":"string","title":"State"},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End"},"period_end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period End"}},"type":"object","required":["state","cancel_at_period_end","period_end"],"title":"CancelResponse"},"ChangePlanRequest":{"properties":{"plan":{"type":"string","enum":["starter","pro","studio"],"title":"Plan","description":"The plan to change to."},"interval":{"anyOf":[{"type":"string","enum":["month","year"]},{"type":"null"}],"title":"Interval","description":"Optional; when given it must equal the subscription's interval (no interval switch)."},"expected_gross_cents":{"anyOf":[{"type":"integer","maximum":10000000.0,"minimum":0.0},{"type":"null"}],"title":"Expected Gross Cents","description":"Optional guard, never an amount: the gross the customer saw in `/billing/change-quote`. A new upgrade whose gross is now different answers 409 `amount_changed` before anything is charged."}},"additionalProperties":false,"type":"object","required":["plan"],"title":"ChangePlanRequest"},"ChangePlanResponse":{"properties":{"direction":{"type":"string","enum":["upgrade","downgrade"],"title":"Direction"},"from_plan":{"type":"string","title":"From Plan"},"to_plan":{"type":"string","title":"To Plan"},"interval":{"type":"string","enum":["month","year"],"title":"Interval"},"change_state":{"type":"string","enum":["creating","awaiting_payment","scheduled","applied","failed","paid_unapplied"],"title":"Change State"},"effective":{"type":"string","enum":["now","renewal"],"title":"Effective"},"effective_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Effective At"},"charge_now":{"anyOf":[{"$ref":"#/components/schemas/ChargeNowOut"},{"type":"null"}]},"recurring_gross_cents":{"type":"integer","title":"Recurring Gross Cents"},"needs_provider_sync":{"type":"boolean","title":"Needs Provider Sync"}},"type":"object","required":["direction","from_plan","to_plan","interval","change_state","effective","effective_at","charge_now","recurring_gross_cents","needs_provider_sync"],"title":"ChangePlanResponse"},"ChangeQuoteResponse":{"properties":{"current_plan":{"type":"string","enum":["starter","pro","studio"],"title":"Current Plan"},"target_plan":{"type":"string","enum":["starter","pro","studio"],"title":"Target Plan"},"interval":{"type":"string","enum":["month","year"],"title":"Interval"},"direction":{"type":"string","enum":["upgrade","downgrade"],"title":"Direction"},"effective":{"type":"string","enum":["now","renewal"],"title":"Effective"},"currency":{"type":"string","title":"Currency","description":"Always EUR."},"period_end":{"type":"string","title":"Period End","description":"End of the paid period (the next renewal)."},"recurring":{"$ref":"#/components/schemas/MoneyOut","description":"The amount charged at every renewal from the next period on (new plan)."},"charge_now":{"anyOf":[{"$ref":"#/components/schemas/ChargeNowOut"},{"type":"null"}],"description":"Upgrade only: the prorated amount charged today."},"renewal_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Renewal Date","description":"Downgrade only: when the lower plan starts."}},"type":"object","required":["current_plan","target_plan","interval","direction","effective","currency","period_end","recurring","charge_now","renewal_date"],"title":"ChangeQuoteResponse"},"ChargeNowOut":{"properties":{"net_cents":{"type":"integer","title":"Net Cents"},"vat_rate_bp":{"type":"integer","title":"Vat Rate Bp","description":"Basis points (2100 = 21%)."},"vat_cents":{"type":"integer","title":"Vat Cents"},"gross_cents":{"type":"integer","title":"Gross Cents","description":"What Mollie is asked for, in euro cents."},"treatment":{"type":"string","enum":["be_standard","eu_reverse_charge","eu_oss","non_eu_zero"],"title":"Treatment"},"remaining_days":{"type":"integer","title":"Remaining Days"},"total_days":{"type":"integer","title":"Total Days"}},"type":"object","required":["net_cents","vat_rate_bp","vat_cents","gross_cents","treatment","remaining_days","total_days"],"title":"ChargeNowOut"},"CheckoutRequest":{"properties":{"plan":{"type":"string","enum":["starter","pro","studio"],"title":"Plan","description":"The paid plan to subscribe to."},"interval":{"type":"string","enum":["month","year"],"title":"Interval","description":"Billing interval: monthly or annual (about 20% off)."}},"additionalProperties":false,"type":"object","required":["plan","interval"],"title":"CheckoutRequest"},"CheckoutResponse":{"properties":{"checkout_url":{"type":"string","title":"Checkout Url","description":"Mollie's hosted checkout page; send the browser there."},"test_mode":{"type":"boolean","title":"Test Mode","description":"True when Mollie runs in test mode: no real money moves."}},"type":"object","required":["checkout_url","test_mode"],"title":"CheckoutResponse"},"CoverageOut":{"properties":{"status":{"type":"string","title":"Status"},"chars":{"type":"integer","title":"Chars"},"entities":{"type":"integer","title":"Entities"},"relationships":{"type":"integer","title":"Relationships"}},"type":"object","required":["status","chars","entities","relationships"],"title":"CoverageOut","description":"What the result covers, so a consumer can tell a clean text from an\nunscanned one without seeing any input."},"CreateKeyRequest":{"properties":{"name":{"type":"string","maxLength":64,"title":"Name","default":""}},"type":"object","title":"CreateKeyRequest"},"CreateKeyResponse":{"properties":{"key":{"type":"string","title":"Key"},"key_prefix":{"type":"string","title":"Key Prefix"},"name":{"type":"string","title":"Name"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["key","key_prefix","name","created_at"],"title":"CreateKeyResponse"},"CreateMatterRequest":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"description":{"type":"string","maxLength":2000,"title":"Description","default":""},"locale":{"type":"string","pattern":"^(fr_BE|nl_BE|de_BE)$","title":"Locale","default":"fr_BE"},"pseudonym_mode":{"type":"string","pattern":"^(counter|stable|realistic)$","title":"Pseudonym Mode","default":"stable"}},"type":"object","required":["name"],"title":"CreateMatterRequest","description":"Payload for POST /matters."},"CreateRuleRequest":{"properties":{"priority":{"type":"integer","maximum":10000.0,"minimum":1.0,"title":"Priority","default":100},"match_type":{"type":"string","title":"Match Type"},"pattern":{"type":"string","maxLength":500,"minLength":1,"title":"Pattern"},"action":{"type":"string","pattern":"^(redact|keep)$","title":"Action"},"replacement":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Replacement"},"label":{"anyOf":[{"type":"string","pattern":"^(PERSON|ORG|ADDRESS|ID|CUSTOM)$"},{"type":"null"}],"title":"Label"}},"type":"object","required":["match_type","pattern","action"],"title":"CreateRuleRequest","description":"Payload for POST /matters/{id}/rules."},"DailyUsage":{"properties":{"date":{"type":"string","title":"Date"},"requests":{"type":"integer","title":"Requests"},"chars":{"type":"integer","title":"Chars"},"entities":{"type":"integer","title":"Entities"}},"type":"object","required":["date","requests","chars","entities"],"title":"DailyUsage"},"DashboardOverview":{"properties":{"tier":{"type":"string","title":"Tier"},"tier_limits":{"additionalProperties":true,"type":"object","title":"Tier Limits"},"key_count":{"type":"integer","title":"Key Count"},"total_requests":{"type":"integer","title":"Total Requests"},"total_chars":{"type":"integer","title":"Total Chars"}},"type":"object","required":["tier","tier_limits","key_count","total_requests","total_chars"],"title":"DashboardOverview"},"DocumentCoverageOut":{"properties":{"status":{"type":"string","title":"Status"},"chars":{"type":"integer","title":"Chars"},"entities":{"type":"integer","title":"Entities"},"relationships":{"type":"integer","title":"Relationships"},"pages_total":{"type":"integer","title":"Pages Total"},"pages_scanned":{"type":"integer","title":"Pages Scanned"},"pages_unread_raster":{"type":"integer","title":"Pages Unread Raster","default":0}},"type":"object","required":["status","chars","entities","relationships","pages_total","pages_scanned"],"title":"DocumentCoverageOut"},"DocumentPageOut":{"properties":{"page":{"type":"integer","title":"Page"},"anonymized":{"type":"string","title":"Anonymized"},"entities":{"items":{"$ref":"#/components/schemas/EntityOut"},"type":"array","title":"Entities"},"relationships":{"items":{"$ref":"#/components/schemas/RelationshipOut"},"type":"array","title":"Relationships"}},"type":"object","required":["page","anonymized","entities","relationships"],"title":"DocumentPageOut"},"DocumentResponse":{"properties":{"filename":{"type":"string","title":"Filename","description":"Safe identifier for the upload — `document` plus an allowlisted extension (document.pdf, document.txt…). Never the submitted name, which is caller data and may itself be an original value."},"pages":{"items":{"$ref":"#/components/schemas/DocumentPageOut"},"type":"array","title":"Pages"},"totals":{"additionalProperties":true,"type":"object","title":"Totals"},"coverage":{"$ref":"#/components/schemas/DocumentCoverageOut"},"latency":{"$ref":"#/components/schemas/LatencyOut"}},"type":"object","required":["filename","pages","totals","coverage","latency"],"title":"DocumentResponse"},"EntityListItem":{"properties":{"placeholder":{"type":"string","title":"Placeholder"},"entity_type":{"type":"string","title":"Entity Type"},"display_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Value"},"occurrence_count":{"type":"integer","title":"Occurrence Count"},"first_file_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First File Id"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["placeholder","entity_type","display_value","occurrence_count","first_file_id","created_at"],"title":"EntityListItem","description":"Public shape — no plaintext. ``POST /reveal`` hands out the\noriginal_value behind an audit-logged action."},"EntityOut":{"properties":{"type":{"type":"string","title":"Type"},"index":{"type":"integer","title":"Index"},"placeholder":{"type":"string","title":"Placeholder"},"start":{"type":"integer","title":"Start"},"end":{"type":"integer","title":"End"},"out_start":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Out Start"},"out_end":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Out End"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},"type":"object","required":["type","index","placeholder","start","end"],"title":"EntityOut","description":"AI-safe entity reference (#95, audit R14).\n\nCarries no original value. Two spans, produced by one render:\n``start``/``end`` are the entity's offsets in the **submitted** text —\nthe pre-#95 meaning, for every caller kind and mode — and\n``out_start``/``out_end`` are the offsets of its bracketed token in\n``anonymized`` (``None`` in ``detect`` mode, which has no output text).\n``source`` is the detection layer. See OPERATIONS.md §API → *Result shape*."},"FileDownloadResponse":{"properties":{"url":{"type":"string","title":"Url"},"expires_in":{"type":"integer","title":"Expires In"},"kind":{"type":"string","title":"Kind"},"filename":{"type":"string","title":"Filename"}},"type":"object","required":["url","expires_in","kind","filename"],"title":"FileDownloadResponse"},"FilePreviewPage":{"properties":{"page":{"type":"integer","title":"Page"},"text":{"type":"string","title":"Text"}},"type":"object","required":["page","text"],"title":"FilePreviewPage"},"FilePreviewResponse":{"properties":{"file_id":{"type":"string","title":"File Id"},"pages":{"items":{"$ref":"#/components/schemas/FilePreviewPage"},"type":"array","title":"Pages"},"truncated":{"type":"boolean","title":"Truncated"},"total_pages":{"type":"integer","title":"Total Pages"}},"type":"object","required":["file_id","pages","truncated","total_pages"],"title":"FilePreviewResponse"},"FileSummary":{"properties":{"id":{"type":"string","title":"Id"},"original_name":{"type":"string","title":"Original Name"},"mime_type":{"type":"string","title":"Mime Type"},"size_bytes":{"type":"integer","title":"Size Bytes"},"status":{"type":"string","title":"Status"},"error_msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Msg"},"page_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page Count"},"entity_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Entity Count"},"created_at":{"type":"string","title":"Created At"},"processed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Processed At"},"anonymized_mime":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anonymized Mime"},"anonymized_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anonymized Name"},"reprocessed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reprocessed At"},"images_removed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Images Removed"}},"type":"object","required":["id","original_name","mime_type","size_bytes","status","created_at"],"title":"FileSummary"},"FileSummaryWithMatter":{"properties":{"id":{"type":"string","title":"Id"},"original_name":{"type":"string","title":"Original Name"},"mime_type":{"type":"string","title":"Mime Type"},"size_bytes":{"type":"integer","title":"Size Bytes"},"status":{"type":"string","title":"Status"},"error_msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Msg"},"page_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Page Count"},"entity_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Entity Count"},"created_at":{"type":"string","title":"Created At"},"processed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Processed At"},"anonymized_mime":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anonymized Mime"},"anonymized_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anonymized Name"},"reprocessed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reprocessed At"},"images_removed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Images Removed"},"matter_id":{"type":"string","title":"Matter Id"},"matter_name":{"type":"string","title":"Matter Name"}},"type":"object","required":["id","original_name","mime_type","size_bytes","status","created_at","matter_id","matter_name"],"title":"FileSummaryWithMatter","description":"``FileSummary`` plus the dossier it belongs to — for cross-dossier\nlistings and the Inbox shortcut, where the client did not choose the\nmatter and needs it back to build links."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InvoiceItem":{"properties":{"id":{"type":"string","title":"Id"},"number":{"type":"string","title":"Number"},"kind":{"type":"string","enum":["invoice","credit_note"],"title":"Kind"},"issued_at":{"type":"string","title":"Issued At"},"gross_cents":{"type":"integer","title":"Gross Cents"},"currency":{"type":"string","title":"Currency"},"status":{"type":"string","enum":["paid","credited","credit_note"],"title":"Status"},"mail_status":{"type":"string","enum":["not_sent","sent","failed"],"title":"Mail Status"}},"type":"object","required":["id","number","kind","issued_at","gross_cents","currency","status","mail_status"],"title":"InvoiceItem"},"InvoiceList":{"properties":{"invoices":{"items":{"$ref":"#/components/schemas/InvoiceItem"},"type":"array","title":"Invoices"}},"type":"object","required":["invoices"],"title":"InvoiceList"},"LatencyOut":{"properties":{"l1_ms":{"type":"number","title":"L1 Ms"},"l2_ms":{"type":"number","title":"L2 Ms"},"l3_ms":{"type":"number","title":"L3 Ms"},"total_ms":{"type":"number","title":"Total Ms"},"chunks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Chunks"}},"type":"object","required":["l1_ms","l2_ms","l3_ms","total_ms"],"title":"LatencyOut"},"MatterResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"locale":{"type":"string","title":"Locale"},"pseudonym_mode":{"type":"string","title":"Pseudonym Mode"},"git_sha":{"type":"string","title":"Git Sha"},"corpus_version":{"type":"string","title":"Corpus Version"},"is_archived":{"type":"boolean","title":"Is Archived"},"is_default":{"type":"boolean","title":"Is Default","default":false},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["id","name","description","locale","pseudonym_mode","git_sha","corpus_version","is_archived","created_at","updated_at"],"title":"MatterResponse","description":"Public matter shape. Pseudonym secret and DEKs never appear here."},"MatterUsageResponse":{"properties":{"matter_id":{"type":"string","title":"Matter Id"},"files_processed":{"type":"integer","title":"Files Processed"},"entities_total":{"type":"integer","title":"Entities Total"},"chars_total":{"type":"integer","title":"Chars Total"},"last_activity":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Activity"}},"type":"object","required":["matter_id","files_processed","entities_total","chars_total","last_activity"],"title":"MatterUsageResponse","description":"Aggregated stats for the matter detail header.\n\nThree counts (files / entities / chars) + one timestamp. Computed\nserver-side in a single SQL query — the matter detail page reads it\nin parallel with its other fetches, no client recomputation."},"MoneyOut":{"properties":{"net_cents":{"type":"integer","title":"Net Cents"},"vat_rate_bp":{"type":"integer","title":"Vat Rate Bp","description":"Basis points (2100 = 21%)."},"vat_cents":{"type":"integer","title":"Vat Cents"},"gross_cents":{"type":"integer","title":"Gross Cents","description":"What Mollie is asked for, in euro cents."},"treatment":{"type":"string","enum":["be_standard","eu_reverse_charge","eu_oss","non_eu_zero"],"title":"Treatment"}},"type":"object","required":["net_cents","vat_rate_bp","vat_cents","gross_cents","treatment"],"title":"MoneyOut"},"OrphanToken":{"properties":{"placeholder":{"type":"string","title":"Placeholder"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["placeholder","count"],"title":"OrphanToken"},"ProfileIn":{"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name","description":"Name of the person or company the invoice is made out to."},"address":{"type":"string","maxLength":500,"minLength":1,"title":"Address","description":"Postal address, lines separated by a line break."},"country":{"type":"string","title":"Country","description":"ISO 3166-1 alpha-2 country code of the buyer, for example BE or DE."},"vat_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vat Id","description":"VAT id with or without the country prefix; empty = none."}},"additionalProperties":false,"type":"object","required":["name","address","country"],"title":"ProfileIn"},"ProfileOut":{"properties":{"name":{"type":"string","title":"Name"},"address":{"type":"string","title":"Address"},"country":{"type":"string","title":"Country"},"vat_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vat Id"},"vat_valid":{"type":"boolean","title":"Vat Valid","description":"True only when VIES confirmed the VAT id; reverse charge needs this."},"vat_checked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vat Checked At"}},"type":"object","required":["name","address","country","vat_id","vat_valid","vat_checked_at"],"title":"ProfileOut"},"ProfileResponse":{"properties":{"profile":{"anyOf":[{"$ref":"#/components/schemas/ProfileOut"},{"type":"null"}]}},"type":"object","required":["profile"],"title":"ProfileResponse"},"ProfileSaved":{"properties":{"profile":{"anyOf":[{"$ref":"#/components/schemas/ProfileOut"},{"type":"null"}]},"vat_status":{"type":"string","enum":["none","valid","invalid","unverified","not_checked"],"title":"Vat Status"},"vat_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vat Message"}},"type":"object","required":["profile","vat_status","vat_message"],"title":"ProfileSaved"},"QuoteResponse":{"properties":{"plan":{"type":"string","enum":["starter","pro","studio"],"title":"Plan"},"interval":{"type":"string","enum":["month","year"],"title":"Interval"},"currency":{"type":"string","title":"Currency","description":"Always EUR."},"net_cents":{"type":"integer","title":"Net Cents","description":"The plan price, excluding VAT, in euro cents."},"vat_rate_bp":{"type":"integer","title":"Vat Rate Bp","description":"The VAT rate that applies to this buyer, in basis points (2100 = 21%)."},"vat_cents":{"type":"integer","title":"Vat Cents","description":"The VAT amount in euro cents."},"gross_cents":{"type":"integer","title":"Gross Cents","description":"The TOTAL charged by Mollie (and stated on the invoice), in euro cents."},"treatment":{"type":"string","enum":["be_standard","eu_reverse_charge","eu_oss","non_eu_zero"],"title":"Treatment","description":"Why this rate: Belgian VAT, EU reverse charge (VIES-confirmed VAT id), OSS rate of the buyer's EU country, or outside the EU."}},"type":"object","required":["plan","interval","currency","net_cents","vat_rate_bp","vat_cents","gross_cents","treatment"],"title":"QuoteResponse"},"RelationshipOut":{"properties":{"entity1":{"type":"string","title":"Entity1"},"entity2":{"type":"string","title":"Entity2"},"type":{"type":"string","title":"Type"}},"type":"object","required":["entity1","entity2","type"],"title":"RelationshipOut"},"RepersonalizeRequest":{"properties":{"text":{"type":"string","maxLength":200000,"minLength":1,"title":"Text"}},"type":"object","required":["text"],"title":"RepersonalizeRequest"},"RepersonalizeResponse":{"properties":{"text":{"type":"string","title":"Text"},"substituted":{"type":"integer","title":"Substituted"},"orphans":{"items":{"$ref":"#/components/schemas/OrphanToken"},"type":"array","title":"Orphans"},"damaged":{"items":{"$ref":"#/components/schemas/OrphanToken"},"type":"array","title":"Damaged","default":[]},"normalised":{"type":"integer","title":"Normalised","default":0},"normalised_answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Normalised Answer"}},"type":"object","required":["text","substituted","orphans"],"title":"RepersonalizeResponse","description":"``substituted`` counts every token put back, ``normalised`` how many of those were damaged but unambiguous\n(T2: ``{{ person:... }}``, ``[[ORG:...]]``); ``orphans`` = exact tokens this project does not know; ``damaged`` =\nplaceholder-shaped text that could not be resolved, left as typed (``api/restore_tokens.py`` has the grammar).\n``normalised_answer`` is the pasted text with the normalised tokens rewritten to their exact form (null when\nnone was), so a client can align the answer with ``text``."},"ReprocessRequest":{"properties":{"file_ids":{"anyOf":[{"items":{"type":"string","maxLength":64,"minLength":1},"type":"array","maxItems":200},{"type":"null"}],"title":"File Ids","description":"Only these files of the dossier are re-run (at most 200; duplicates collapse). Omit it to re-run every finished file."}},"type":"object","title":"ReprocessRequest","description":"Optional body of the re-run route (J1c / C25). ``file_ids`` null or missing means every finished file."},"ReprocessResponse":{"properties":{"requeued":{"type":"integer","title":"Requeued"},"skipped":{"type":"integer","title":"Skipped"}},"type":"object","required":["requeued","skipped"],"title":"ReprocessResponse"},"ResumeResponse":{"properties":{"state":{"type":"string","title":"State"},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End"},"period_end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period End"}},"type":"object","required":["state","cancel_at_period_end","period_end"],"title":"ResumeResponse"},"RevealRequest":{"properties":{"reason":{"anyOf":[{"type":"string","enum":["client_request","legal_obligation","quality_check","other"]},{"type":"null"}],"title":"Reason","description":"Why the original is revealed: client_request, legal_obligation, quality_check or other. Omit it when none applies. Stored as the code."}},"type":"object","title":"RevealRequest","description":"Optional reason CODE recorded with the audit entry. Lets a regulator\nanswer \"why did counsel X unmask Y on date Z?\" — Art. 22 GDPR ask.\n\nA closed set, not free text (S3/F4): ``audit_log`` is immutable and survives a\ndossier purge, so anything typed here would stay forever. Omitted or ``null``\nis allowed (older clients); any other value is a 422."},"RevealResponse":{"properties":{"placeholder":{"type":"string","title":"Placeholder"},"entity_type":{"type":"string","title":"Entity Type"},"original_value":{"type":"string","title":"Original Value"},"revealed_at":{"type":"string","title":"Revealed At"},"audit_id":{"type":"integer","title":"Audit Id"}},"type":"object","required":["placeholder","entity_type","original_value","revealed_at","audit_id"],"title":"RevealResponse"},"RuleResponse":{"properties":{"id":{"type":"string","title":"Id"},"matter_id":{"type":"string","title":"Matter Id"},"priority":{"type":"integer","title":"Priority"},"match_type":{"type":"string","title":"Match Type"},"pattern":{"type":"string","title":"Pattern"},"action":{"type":"string","title":"Action"},"replacement":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Replacement"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","matter_id","priority","match_type","pattern","action","replacement","created_at"],"title":"RuleResponse"},"TextProtectRequest":{"properties":{"text":{"type":"string","maxLength":50000,"minLength":1,"title":"Text"},"source":{"type":"string","enum":["companion","web"],"title":"Source","default":"companion"}},"type":"object","required":["text"],"title":"TextProtectRequest"},"TextProtectResponse":{"properties":{"file":{"$ref":"#/components/schemas/FileSummary"},"text":{"type":"string","title":"Text"},"counts_by_type":{"additionalProperties":{"type":"integer"},"type":"object","title":"Counts By Type"}},"type":"object","required":["file","text","counts_by_type"],"title":"TextProtectResponse"},"TextQueued":{"properties":{"file":{"$ref":"#/components/schemas/FileSummary"}},"type":"object","required":["file"],"title":"TextQueued"},"UpdateMatterRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Description"},"is_archived":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Archived"}},"type":"object","title":"UpdateMatterRequest","description":"Payload for PATCH /matters/{id}. All fields optional — partial update."},"UsageStats":{"properties":{"period":{"type":"string","title":"Period"},"total_requests":{"type":"integer","title":"Total Requests"},"total_chars":{"type":"integer","title":"Total Chars"},"total_entities":{"type":"integer","title":"Total Entities"},"daily":{"items":{"$ref":"#/components/schemas/DailyUsage"},"type":"array","title":"Daily"}},"type":"object","required":["period","total_requests","total_chars","total_entities","daily"],"title":"UsageStats"},"UserProfile":{"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"tier":{"type":"string","title":"Tier"},"created_at":{"type":"string","title":"Created At"},"key_count":{"type":"integer","title":"Key Count"}},"type":"object","required":["id","email","tier","created_at","key_count"],"title":"UserProfile"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WordResponse":{"properties":{"id":{"type":"string","title":"Id"},"priority":{"type":"integer","title":"Priority"},"match_type":{"type":"string","title":"Match Type"},"pattern":{"type":"string","title":"Pattern"},"action":{"type":"string","title":"Action"},"replacement":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Replacement"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","priority","match_type","pattern","action","replacement","created_at"],"title":"WordResponse"}},"securitySchemes":{"GatewayHMAC":{"type":"apiKey","in":"header","name":"X-Gateway-Signature","description":"Gateway-signed request per chut/platform APP-MANIFEST §1.1 + §1.2.\nThe HMAC signature header is the primary discriminator, but every\nsigned request MUST also carry:\n  - X-Gateway-Timestamp (unix-seconds)\n  - X-Tenant-Id          (tenant uuid)\n  - X-Platform-User-Id   (user uuid)\n  - X-User-Email, X-User-Name, X-User-Roles (json array),\n    X-User-KYC-Level (none|pending|verified|review|denied),\n    X-Source-Origin (user|ai|platform),\n    X-Source-App-Id (callee app slug or empty),\n    X-Request-Id (uuid)\nBackend MUST NOT trust any of these headers from any source other\nthan the gateway. Verify the HMAC; everything else is derived."}}},"security":[{"GatewayHMAC":[]}]}