{
  "openapi": "3.0.0",
  "paths": {
    "/api/auth-extended/set-password": {
      "post": {
        "description": "Sets a password for the current user (e.g. for OIDC-only accounts that have never had one).",
        "operationId": "AuthExtendedController_setPassword",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "newPassword": {
                    "type": "string",
                    "minLength": 8,
                    "description": "New password, at least 8 characters"
                  }
                },
                "required": [
                  "newPassword"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Password set successfully"
          },
          "401": {
            "description": "Failed to set password"
          },
          "403": {
            "description": "This instance is single-sign-on only (OIDC_ONLY)"
          }
        },
        "summary": "Set a password",
        "tags": [
          "auth-extended"
        ]
      }
    },
    "/api/auth-extended/preferences": {
      "patch": {
        "description": "Currently just `locale` — the language this user reads the app in, and will eventually receive mail in (`User.locale`; see `resolve-user-language.ts`). No `@ActiveCompany()`: this is a per-USER setting, not scoped to whichever company happens to be active. `null` clears it (falls back to the active company's own language, then English); any other value must be one of the languages this product actually renders documents/mail in.",
        "operationId": "AuthExtendedController_updatePreferences",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "locale": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "en",
                      "fr",
                      "it",
                      "pl",
                      "de",
                      "pt"
                    ],
                    "description": "One of SUPPORTED_RENDER_LANGUAGES, or null to clear the preference."
                  }
                },
                "required": [
                  "locale"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preferences updated"
          },
          "400": {
            "description": "locale is missing, or not one of SUPPORTED_RENDER_LANGUAGES"
          }
        },
        "summary": "Update account preferences",
        "tags": [
          "auth-extended"
        ]
      }
    },
    "/api/api-keys/options": {
      "get": {
        "operationId": "ApiKeysController_options",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Scopes retrieved"
          }
        },
        "summary": "List available API key scopes",
        "tags": [
          "api-keys"
        ]
      }
    },
    "/api/api-keys": {
      "post": {
        "description": "The plaintext key is only ever returned in this response — it cannot be retrieved again afterwards.",
        "operationId": "ApiKeysController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "API key created"
          }
        },
        "summary": "Create a new API key",
        "tags": [
          "api-keys"
        ]
      },
      "get": {
        "operationId": "ApiKeysController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": "List of API keys (without the plaintext key)"
          }
        },
        "summary": "List API keys for the active company",
        "tags": [
          "api-keys"
        ]
      }
    },
    "/api/api-keys/{id}": {
      "delete": {
        "operationId": "ApiKeysController_revoke",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "API key ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "API key revoked"
          }
        },
        "summary": "Revoke an API key",
        "tags": [
          "api-keys"
        ]
      }
    },
    "/api/articles": {
      "get": {
        "description": "Returns all active catalog articles for the company.",
        "operationId": "ArticlesController_findAll",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Articles retrieved"
          }
        },
        "summary": "List articles",
        "tags": [
          "articles"
        ]
      },
      "post": {
        "description": "Adds a new reusable catalog article (product or service).",
        "operationId": "ArticlesController_create",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Article created"
          }
        },
        "summary": "Create an article",
        "tags": [
          "articles"
        ]
      }
    },
    "/api/articles/low-stock": {
      "get": {
        "description": "Returns the active, stock-tracked catalog articles currently at or under their own alert threshold.",
        "operationId": "ArticlesController_findLowStock",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Low-stock articles retrieved"
          }
        },
        "summary": "List low-stock articles",
        "tags": [
          "articles"
        ]
      }
    },
    "/api/articles/{id}": {
      "get": {
        "description": "Returns a single catalog article by ID.",
        "operationId": "ArticlesController_findOne",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Article ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Article retrieved"
          },
          "404": {
            "description": "Article not found"
          }
        },
        "summary": "Get an article",
        "tags": [
          "articles"
        ]
      },
      "patch": {
        "description": "Updates an existing catalog article by ID.",
        "operationId": "ArticlesController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Article ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Article updated"
          }
        },
        "summary": "Update an article",
        "tags": [
          "articles"
        ]
      },
      "delete": {
        "description": "Soft-deletes a catalog article by ID.",
        "operationId": "ArticlesController_remove",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Article ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Article deleted"
          }
        },
        "summary": "Delete an article",
        "tags": [
          "articles"
        ]
      }
    },
    "/api/companies": {
      "post": {
        "description": "Creates an additional company and makes the caller its owner. Open to any authenticated user, regardless of existing memberships.",
        "operationId": "CompaniesController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EditCompanyDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Company created"
          }
        },
        "summary": "Create a new company",
        "tags": [
          "companies"
        ]
      }
    },
    "/api/companies/switch": {
      "post": {
        "description": "Switches the current session's active company to one the caller belongs to.",
        "operationId": "CompaniesController_switch",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "companyId": {
                    "type": "string"
                  }
                },
                "required": [
                  "companyId"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Active company switched"
          }
        },
        "summary": "Switch active company",
        "tags": [
          "companies"
        ]
      }
    },
    "/api/companies/leave": {
      "delete": {
        "description": "Removes the caller's own membership from the active company. Refused for the company's last remaining owner — ownership must be transferred to another member first.",
        "operationId": "CompaniesController_leave",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Left the company"
          }
        },
        "summary": "Leave the active company",
        "tags": [
          "companies"
        ]
      }
    },
    "/api/companies/members": {
      "get": {
        "operationId": "CompaniesController_listMembers",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Members retrieved"
          }
        },
        "summary": "List the active company members",
        "tags": [
          "companies"
        ]
      }
    },
    "/api/companies/members/{userId}": {
      "patch": {
        "description": "Owner-only: promoting/demoting owners is ownership-sensitive.",
        "operationId": "CompaniesController_changeMemberRole",
        "parameters": [
          {
            "name": "userId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "OWNER",
                      "ADMIN",
                      "MEMBER"
                    ]
                  }
                },
                "required": [
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Member role updated"
          }
        },
        "summary": "Change a member's role",
        "tags": [
          "companies"
        ]
      },
      "delete": {
        "operationId": "CompaniesController_removeMember",
        "parameters": [
          {
            "name": "userId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Member removed"
          }
        },
        "summary": "Remove a member from the active company",
        "tags": [
          "companies"
        ]
      }
    },
    "/api/companies/export": {
      "post": {
        "description": "Self-service full data export (every document the company holds, its stored fields plus a rendered PDF where one can be produced) — the same archive the hosted-billing lifecycle sweep already mails an OWNER automatically, available on demand instead of waiting for that or writing to support. OWNER/ADMIN only, rate-limited per company (see the 429 response below). A small export streams back directly; a large one is emailed to the caller instead.",
        "operationId": "CompaniesController_exportData",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Zip streamed directly",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "202": {
            "description": "Export built and emailed to the caller instead"
          },
          "429": {
            "description": "Rate-limited — retry after the window named in the response"
          }
        },
        "summary": "Export everything the active company holds",
        "tags": [
          "companies"
        ]
      }
    },
    "/api/company/info": {
      "get": {
        "description": "Returns the company name, address, contact details, and numbering configuration.",
        "operationId": "CompanyController_getCompanyInfo",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Company info retrieved"
          }
        },
        "summary": "Get company info",
        "tags": [
          "company"
        ]
      },
      "post": {
        "description": "Saves the company profile including name, address, contact details, currency, numbering formats, and PDF config.",
        "operationId": "CompanyController_postCompanyInfo",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EditCompanyDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Company info saved"
          }
        },
        "summary": "Update company info",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/number-format": {
      "put": {
        "description": "Merges `{ [typeId]: pattern }` into Company.numberFormats — e.g. `{ \"typeId\": \"invoice\", \"pattern\": \"FT {year}/{number:4}\" }`. Rejects a pattern with no \"{number}\" token.",
        "operationId": "CompanyController_updateNumberFormat",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "typeId": {
                    "type": "string",
                    "example": "invoice"
                  },
                  "pattern": {
                    "type": "string",
                    "example": "FT {year}/{number:4}"
                  }
                },
                "required": [
                  "typeId",
                  "pattern"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Number format updated"
          }
        },
        "summary": "Set one document type's own number-format pattern",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/email-templates": {
      "get": {
        "description": "The two emails that are not about a document — the signature request and the verification code — each resolved to what actually applies (this company's own override, else the copy shipped in code), with the `variables` that family offers mapped to sample values. A DOCUMENT's email lives elsewhere, per document type: GET /api/documents/email-templates.",
        "operationId": "CompanyController_getEmailTemplates",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Email templates retrieved"
          }
        },
        "summary": "Get the system email templates",
        "tags": [
          "company"
        ]
      },
      "put": {
        "description": "Saves this company's override of one system email. The template is identified by `id` (the family: SIGNATURE_REQUEST or VERIFICATION_CODE) or, for a company that already has a stored row, by `dbId`. `body` is html and is sanitized server-side before storage; the text/plain alternative is derived from it at send time. An unknown `{placeholder}` comes back in `warnings` rather than being rejected — a typo must never be what stops a verification code.",
        "operationId": "CompanyController_updateEmailTemplate",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Template family (SIGNATURE_REQUEST | VERIFICATION_CODE)"
                  },
                  "dbId": {
                    "type": "string",
                    "description": "Database ID of an already-stored override"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string",
                    "description": "Html body"
                  }
                },
                "required": [
                  "subject",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Email template updated, with any placeholder warnings"
          },
          "400": {
            "description": "Unidentifiable family, blank subject, or empty body"
          }
        },
        "summary": "Update a system email template",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/mail-settings": {
      "get": {
        "description": "Status only (configured + kind + fromAddress) — never a secret. Falls back to the instance-level provider when unconfigured.",
        "operationId": "CompanyController_getMailSettings",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Mail settings status retrieved"
          }
        },
        "summary": "Get this company's own mail server status",
        "tags": [
          "company"
        ]
      },
      "put": {
        "description": "Body is discriminated by \"kind\": \"smtp\" (host, port, secure, username, password, fromAddress) or \"resend\" (apiKey, fromAddress). Replaces any existing configuration.",
        "operationId": "CompanyController_setMailSettings",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "smtp"
                        ]
                      },
                      "host": {
                        "type": "string"
                      },
                      "port": {
                        "type": "number"
                      },
                      "secure": {
                        "type": "boolean"
                      },
                      "username": {
                        "type": "string"
                      },
                      "password": {
                        "type": "string"
                      },
                      "fromAddress": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "kind",
                      "host",
                      "port",
                      "username",
                      "password",
                      "fromAddress"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "resend"
                        ]
                      },
                      "apiKey": {
                        "type": "string"
                      },
                      "fromAddress": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "kind",
                      "apiKey",
                      "fromAddress"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mail settings saved"
          },
          "400": {
            "description": "Missing required field for the given \"kind\""
          },
          "503": {
            "description": "CREDENTIALS_ENCRYPTION_KEY is not configured"
          }
        },
        "summary": "Set this company's own mail server",
        "tags": [
          "company"
        ]
      },
      "delete": {
        "description": "Falls back to the instance-level provider (or a named refusal if none is set either).",
        "operationId": "CompanyController_deleteMailSettings",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Mail settings cleared"
          }
        },
        "summary": "Clear this company's own mail server",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/mail-settings/reply-to": {
      "put": {
        "description": "Applied to every outgoing message sent for this company, winning over the instance-level MAIL_REPLY_TO. Validated as an e-mail address here, at write time. null/omitted clears it.",
        "operationId": "CompanyController_setMailReplyTo",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "replyTo": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reply-To saved"
          },
          "400": {
            "description": "replyTo is not a valid e-mail address"
          }
        },
        "summary": "Set this company's own Reply-To address",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/mail-settings/test": {
      "post": {
        "description": "Exercises this company's real société → instance → refus-nommé mail cascade and reports the failure reason (credentials rejected, address not allowed, nothing configured at all, ...). Every network-level outcome reports identically — see the mail endpoint guard.",
        "operationId": "CompanyController_testMailSettings",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Test email sent"
          },
          "400": {
            "description": "The real send failure — see the message"
          }
        },
        "summary": "Send a test email to yourself",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/currency-rates": {
      "get": {
        "description": "Returns every manually-entered exchange rate for the current company, newest first.",
        "operationId": "CurrencyRatesController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Currency rates retrieved"
          }
        },
        "summary": "List currency rates",
        "tags": [
          "company"
        ]
      },
      "post": {
        "description": "Records a manually-entered exchange rate (source: \"manual\"). Rejects rate <= 0 and from === to.",
        "operationId": "CurrencyRatesController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "string",
                    "description": "Currency converted FROM, e.g. \"USD\""
                  },
                  "to": {
                    "type": "string",
                    "description": "Currency converted TO, e.g. \"EUR\""
                  },
                  "rate": {
                    "type": "number",
                    "description": "Units of \"to\" per one unit of \"from\""
                  },
                  "asOf": {
                    "type": "string",
                    "description": "ISO date-time this rate became true; defaults to now"
                  }
                },
                "required": [
                  "from",
                  "to",
                  "rate"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Currency rate created"
          }
        },
        "summary": "Add a currency rate",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/currency-rates/gaps": {
      "get": {
        "description": "Every (from, to) pair this company has entered that neither the ECB feed nor the open.er-api.com fallback has ever been able to refresh automatically — see CurrencyRateSweepRunner for how both are tried before a pair counts as one of these.",
        "operationId": "CurrencyRatesController_gaps",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Currency pairs with no automatic rate"
          }
        },
        "summary": "List currency pairs with no automatic rate",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/channels": {
      "get": {
        "description": "Returns this company's connected channels (status only — never a credential value), this company's own country channel policy (suggested and/or mandated), and this company's own country DECLARATIVE-REPORTING obligations (e.g. pt-at — never a transport hint).",
        "operationId": "ChannelsController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Channel status retrieved"
          }
        },
        "summary": "List channel connections",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/channels/{providerId}": {
      "put": {
        "description": "Creates or updates this company's configuration for one provider (e.g. \"pdp\"). The config blob is encrypted at rest and never logged.",
        "operationId": "ChannelsController_upsert",
        "parameters": [
          {
            "name": "providerId",
            "required": true,
            "in": "path",
            "schema": {
              "example": "pdp",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "environment": {
                    "type": "string",
                    "enum": [
                      "TEST",
                      "PROD"
                    ],
                    "default": "TEST"
                  },
                  "config": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "isActive": {
                    "type": "boolean",
                    "default": true
                  }
                },
                "required": [
                  "config"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Channel connected"
          }
        },
        "summary": "Connect/update a channel",
        "tags": [
          "company"
        ]
      },
      "delete": {
        "description": "Removes this company's configuration for one provider, every environment included.",
        "operationId": "ChannelsController_remove",
        "parameters": [
          {
            "name": "providerId",
            "required": true,
            "in": "path",
            "schema": {
              "example": "pdp",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Channel disconnected"
          }
        },
        "summary": "Disconnect a channel",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/branding": {
      "get": {
        "description": "Current accent color / font / preset, whether a logo is uploaded, and the full catalog of presets and fonts the settings screen can offer — the catalogs are the single source of truth, never duplicated on the frontend.",
        "operationId": "BrandingController_get",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Branding status retrieved"
          }
        },
        "summary": "Get this company's document branding",
        "tags": [
          "company"
        ]
      },
      "put": {
        "description": "A named \"preset\" sets accentColor/font to that preset's own values — UNLESS this same call already carries an explicit accentColor/font, which then wins. Every field is independently nullable: null clears it back to the pre-branding default; a key simply absent from the body leaves the existing stored value untouched.",
        "operationId": "BrandingController_set",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preset": {
                    "type": "string",
                    "nullable": true,
                    "example": "modern"
                  },
                  "accentColor": {
                    "type": "string",
                    "nullable": true,
                    "example": "#1d4ed8"
                  },
                  "font": {
                    "type": "string",
                    "nullable": true,
                    "example": "dmSans"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Branding updated"
          },
          "400": {
            "description": "Unknown preset/font, or accentColor is not a hex color"
          }
        },
        "summary": "Set the accent color, font and/or preset",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/branding/logo": {
      "post": {
        "description": "Same validation as documents/attachments: image/jpeg, image/png or image/webp only, up to 10 MB. Replaces any previously uploaded logo.",
        "operationId": "BrandingController_uploadLogo",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Logo uploaded"
          },
          "400": {
            "description": "Missing file, an empty file, or a disallowed mime"
          },
          "413": {
            "description": "The file is over the size limit"
          }
        },
        "summary": "Upload the company logo",
        "tags": [
          "company"
        ]
      },
      "delete": {
        "operationId": "BrandingController_removeLogo",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Logo removed"
          }
        },
        "summary": "Remove the company logo",
        "tags": [
          "company"
        ]
      },
      "get": {
        "operationId": "BrandingController_getLogo",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Logo bytes, verbatim",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No logo uploaded for this company"
          }
        },
        "summary": "The company logo's raw stored bytes",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/branding/preview": {
      "get": {
        "description": "A fixed sample invoice — never a real document — rendered through the exact same HTML pipeline a real PDF uses, with this company's CURRENT accent color/font/logo applied. Returns { html }; the frontend sanitizes it (DOMPurify) before display.",
        "operationId": "BrandingController_preview",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Sample HTML"
          }
        },
        "summary": "A sample document rendered with this branding applied",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/signing-certificates": {
      "get": {
        "operationId": "SigningCertificatesController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Certificate list (no PFX / no password)"
          }
        },
        "summary": "List signing certificates for the active company (metadata only)",
        "tags": [
          "company"
        ]
      },
      "post": {
        "operationId": "SigningCertificatesController_upload",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "example": "FR production cert 2025"
                  },
                  "applicability": {
                    "type": "string",
                    "example": "*",
                    "description": "\"*\" for all algorithms, or \"XAdES\" / \"CAdES\" / \"PAdES\""
                  },
                  "environment": {
                    "type": "string",
                    "enum": [
                      "TEST",
                      "PROD"
                    ],
                    "default": "TEST"
                  },
                  "pfxBase64": {
                    "type": "string",
                    "description": "Base64-encoded PKCS#12 (.pfx) file — write-only, never returned."
                  },
                  "pfxPassword": {
                    "type": "string",
                    "description": "Password for the PKCS#12 bundle — write-only, never returned."
                  }
                },
                "required": [
                  "label",
                  "pfxBase64",
                  "pfxPassword"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Certificate stored (metadata returned, no secrets)"
          }
        },
        "summary": "Upload a signing certificate (PFX + password, write-only)",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/signing-certificates/{id}": {
      "delete": {
        "operationId": "SigningCertificatesController_deactivate",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Certificate record ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Certificate deactivated"
          }
        },
        "summary": "Deactivate a signing certificate",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/atcud-series": {
      "get": {
        "operationId": "AtcudSeriesController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": "ATCUD series list"
          }
        },
        "summary": "List this company's registered ATCUD series validation codes",
        "tags": [
          "company"
        ]
      },
      "put": {
        "operationId": "AtcudSeriesController_upsert",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "typeId": {
                    "type": "string",
                    "example": "invoice"
                  },
                  "seriesId": {
                    "type": "string",
                    "example": "FT 2026"
                  },
                  "validationCode": {
                    "type": "string",
                    "example": "JCVPTS0J"
                  }
                },
                "required": [
                  "typeId",
                  "seriesId",
                  "validationCode"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Series validation code stored"
          }
        },
        "summary": "Register (or update) an ATCUD series validation code",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/atcud-series/{id}": {
      "delete": {
        "operationId": "AtcudSeriesController_remove",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "ATCUD series record ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Series removed"
          }
        },
        "summary": "Remove a registered ATCUD series validation code",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/sso": {
      "get": {
        "description": "Returns this company's SSO provider status (never a credential value) and the redirect URI to register at the identity provider. Null when SSO has never been configured.",
        "operationId": "SsoController_get",
        "parameters": [],
        "responses": {
          "200": {
            "description": "SSO status retrieved"
          }
        },
        "summary": "Get the SSO configuration",
        "tags": [
          "company"
        ]
      },
      "put": {
        "description": "Creates or updates this company's OIDC provider. Credentials are encrypted at rest and never logged or returned. Takes effect immediately.",
        "operationId": "SsoController_upsert",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "example": "Acme SSO"
                  },
                  "discoveryUrl": {
                    "type": "string",
                    "example": "https://idp.acme.com/.well-known/openid-configuration"
                  },
                  "authorizationUrl": {
                    "type": "string"
                  },
                  "tokenUrl": {
                    "type": "string"
                  },
                  "userInfoUrl": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "openid",
                      "profile",
                      "email"
                    ]
                  },
                  "clientId": {
                    "type": "string"
                  },
                  "clientSecret": {
                    "type": "string"
                  },
                  "isActive": {
                    "type": "boolean",
                    "default": true
                  }
                },
                "required": [
                  "clientId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SSO configured"
          },
          "400": {
            "description": "Missing clientId, or neither a discovery URL nor endpoints"
          },
          "503": {
            "description": "CREDENTIALS_ENCRYPTION_KEY is not configured"
          }
        },
        "summary": "Configure SSO",
        "tags": [
          "company"
        ]
      },
      "delete": {
        "description": "Deletes this company's SSO configuration and stops accepting sign-ins through it.",
        "operationId": "SsoController_remove",
        "parameters": [],
        "responses": {
          "200": {
            "description": "SSO removed"
          }
        },
        "summary": "Remove SSO",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/sso/domains": {
      "get": {
        "operationId": "SsoController_listDomains",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Domain claims"
          }
        },
        "summary": "List claimed SSO domains and their verification status",
        "tags": [
          "company"
        ]
      },
      "post": {
        "description": "Mints (or re-mints, for an existing unverified claim) a DNS TXT verification token and returns the record name/value to publish.",
        "operationId": "SsoController_addDomain",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "example": "acme.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Domain claimed"
          },
          "400": {
            "description": "Not a valid bare domain"
          },
          "404": {
            "description": "No SSO provider configured yet for this company"
          }
        },
        "summary": "Claim an email domain for SSO",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/sso/domains/{id}/verify": {
      "post": {
        "operationId": "SsoController_verifyDomain",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Domain claim ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Domain verified"
          },
          "400": {
            "description": "TXT record missing, wrong, or a DNS failure"
          },
          "404": {
            "description": "No such domain claim for this company"
          },
          "409": {
            "description": "Already verified for a different account"
          }
        },
        "summary": "Verify a claimed domain via its DNS TXT record",
        "tags": [
          "company"
        ]
      }
    },
    "/api/company/sso/domains/{id}": {
      "delete": {
        "operationId": "SsoController_removeDomain",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Domain claim ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Domain removed"
          }
        },
        "summary": "Remove a claimed SSO domain",
        "tags": [
          "company"
        ]
      }
    },
    "/api/sso/lookup": {
      "get": {
        "description": "Public, rate-limited. Returns { providerId, label } when the address belongs to a company whose SSO is active AND whose email domains have been verified; 204 otherwise. Never reveals anything else about the company.",
        "operationId": "SsoLookupController_lookup",
        "parameters": [
          {
            "name": "email",
            "required": true,
            "in": "query",
            "schema": {
              "example": "alice@acme.com",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The provider to sign in with"
          },
          "204": {
            "description": "No SSO provider for this address"
          }
        },
        "summary": "Find the SSO provider for an email address",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/webhooks/options": {
      "get": {
        "description": "Returns the available webhook types and event types for configuring a webhook.",
        "operationId": "WebhooksController_options",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Webhook types and events retrieved"
          }
        },
        "summary": "List webhook types and events",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/webhooks/{id}": {
      "get": {
        "description": "Returns a single webhook configuration (without the secret).",
        "operationId": "WebhooksController_findOne",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Webhook ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook retrieved"
          },
          "404": {
            "description": "Webhook not found"
          }
        },
        "summary": "Get a webhook by ID",
        "tags": [
          "webhooks"
        ]
      },
      "patch": {
        "description": "Updates the URL, type, events, or secret of an existing webhook configuration.",
        "operationId": "WebhooksController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Webhook ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "secret": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook updated"
          },
          "404": {
            "description": "Webhook not found"
          }
        },
        "summary": "Update a webhook",
        "tags": [
          "webhooks"
        ]
      },
      "delete": {
        "description": "Permanently removes a webhook configuration.",
        "operationId": "WebhooksController_remove",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Webhook ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook deleted"
          },
          "404": {
            "description": "Webhook not found"
          }
        },
        "summary": "Delete a webhook",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/webhooks": {
      "get": {
        "description": "Returns all webhook configurations for the current company (secrets are excluded).",
        "operationId": "WebhooksController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Webhooks retrieved"
          }
        },
        "summary": "List all webhooks",
        "tags": [
          "webhooks"
        ]
      },
      "post": {
        "description": "Creates a new webhook configuration. The secret is returned only in this response.",
        "operationId": "WebhooksController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "description": "Webhook type, e.g. GENERIC"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "List of event types to subscribe to"
                  },
                  "secret": {
                    "type": "string",
                    "description": "Optional pre-set secret; generated if omitted"
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created"
          }
        },
        "summary": "Create a webhook",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/clients": {
      "get": {
        "description": "Returns a paginated list of clients.",
        "operationId": "ClientsController_getClients",
        "parameters": [
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "Page number (1-indexed) of the paginated client list. Defaults to 1.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Clients retrieved"
          }
        },
        "summary": "List clients",
        "tags": [
          "clients"
        ]
      },
      "post": {
        "description": "Creates a new client with the provided information.",
        "operationId": "ClientsController_postClientsInfo",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EditClientsDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Client created"
          }
        },
        "summary": "Create a client",
        "tags": [
          "clients"
        ]
      }
    },
    "/api/clients/search": {
      "get": {
        "description": "Searches clients by query string (name, email, etc.).",
        "operationId": "ClientsController_searchClients",
        "parameters": [
          {
            "name": "query",
            "required": true,
            "in": "query",
            "description": "Free-text search term matched against client name, email, etc.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results retrieved"
          }
        },
        "summary": "Search clients",
        "tags": [
          "clients"
        ]
      }
    },
    "/api/clients/duplicates": {
      "get": {
        "description": "Non-blocking duplicate detection for the client wizard: matches an existing, active client by contact email (case-insensitive), or by name + country together (also case-insensitive), scoped to the active company. Returns an empty array when neither criterion is usable — this never refuses anything, it only informs. Pass excludeId when editing an existing client so it never flags itself as its own duplicate.",
        "operationId": "ClientsController_findDuplicates",
        "parameters": [
          {
            "name": "email",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "excludeId",
            "required": false,
            "in": "query",
            "description": "Client id to exclude (editing)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Potential duplicates found (possibly empty)"
          }
        },
        "summary": "Find potential duplicate clients",
        "tags": [
          "clients"
        ]
      }
    },
    "/api/clients/{id}/statement": {
      "get": {
        "description": "Every \"sent\" invoice for this client, the credit notes correcting each one, the resulting balance (settlement/compute-settlement.ts — payments and credits already netted in), and an aged balance per currency (current / 0-30 / 31-60 / 60+ days overdue, by the balance's own due date). See settlement/client-statement.ts.",
        "operationId": "ClientsController_getStatement",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Client ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Statement computed"
          },
          "404": {
            "description": "Not found for this company"
          }
        },
        "summary": "A client's account statement",
        "tags": [
          "clients"
        ]
      }
    },
    "/api/clients/{id}": {
      "get": {
        "description": "Returns a single client by id, scoped to the active company — e.g. the duplicate-detection wizard's own \"view existing client\" link, opening a record that may not be on the caller's currently loaded page of the paginated list.",
        "operationId": "ClientsController_getClient",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Client ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Client found"
          },
          "404": {
            "description": "Not found for this company"
          }
        },
        "summary": "Get a client",
        "tags": [
          "clients"
        ]
      },
      "patch": {
        "description": "Updates an existing client by ID.",
        "operationId": "ClientsController_editClientsInfo",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Client ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EditClientsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Client updated"
          }
        },
        "summary": "Update a client",
        "tags": [
          "clients"
        ]
      },
      "delete": {
        "description": "Permanently removes a client by ID.",
        "operationId": "ClientsController_deleteClient",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Client ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Client deleted"
          }
        },
        "summary": "Delete a client",
        "tags": [
          "clients"
        ]
      }
    },
    "/api/country-readiness/fully-supported": {
      "get": {
        "description": "Every country code present in all six core mechanisms (country-policy, vat-rates, tax-systems, correction-routes, country-identifiers, channel-policy) — i.e. every code `GET /country-readiness/:countryCode` would answer `complete: true` for. Optional convenience so the frontend can highlight fully-supported countries; not itself required by any flow. Not scoped by `@ActiveCompany()` — see the sibling endpoint for why.",
        "operationId": "CountryReadinessController_listFullySupported",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Fully-supported country codes retrieved"
          }
        },
        "summary": "Country codes fully supported by the compliance core",
        "tags": [
          "country-readiness"
        ]
      }
    },
    "/api/country-readiness/mention-window-alerts": {
      "get": {
        "description": "One entry per (country, placeholder) whose `documents/mentions/data/*.json` value table has every window bounded (no open-ended entry) and its latest `validTo` within 90 days — or already past. Once that date is reached, `mentions/invoice-notes.ts` refuses to build the mention at all (a named 400, never a printed \"{token}\") for every invoice in that country — this is the advance warning meant to prevent that from ever being a surprise. Not scoped by @ActiveCompany(): this is an operational fact about the catalog itself, not about any one company's data.",
        "operationId": "CountryReadinessController_getMentionWindowAlerts",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Mention window alerts computed"
          }
        },
        "summary": "Legal mentions whose interpolated value table is about to stop covering new invoices",
        "tags": [
          "country-readiness"
        ]
      }
    },
    "/api/country-readiness/{countryCode}": {
      "get": {
        "description": "A country is \"complete\" when it has a `data/xx.json` file in all six CŒUR mechanisms: country-policy, vat-rates, tax-systems, correction-routes, country-identifiers, channel-policy (`mentions`, `content-requirements`, `b2g-routing`, `country-fields`, `archive/retention` and `reporting` are deliberately excluded, each for its own reason — see country-readiness.service.ts). Used at company creation (onboarding and settings) to warn, never block, when the chosen country is not yet fully supported. Deliberately NOT scoped by @ActiveCompany(): company creation happens BEFORE a company — and therefore an active company — exists, so this only requires the caller to be authenticated (mirrors documents.controller.ts's own 'required-identifiers' endpoint for the same reason).",
        "operationId": "CountryReadinessController_getReadiness",
        "parameters": [
          {
            "name": "countryCode",
            "required": true,
            "in": "path",
            "description": "ISO 3166-1 alpha-2, case-insensitive",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Readiness computed"
          }
        },
        "summary": "Whether a country has every core compliance mechanism wired",
        "tags": [
          "country-readiness"
        ]
      }
    },
    "/api/sirene/siret/{siret}": {
      "get": {
        "description": "Looks up company information by SIRET via the recherche-entreprises.api.gouv.fr public directory.",
        "operationId": "SireneController_getCompanyBySiret",
        "parameters": [
          {
            "name": "siret",
            "required": true,
            "in": "path",
            "description": "14-digit SIRET number",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Company found or not found"
          }
        },
        "summary": "Look up a company by SIRET",
        "tags": [
          "sirene"
        ]
      }
    },
    "/api/company-lookup/capabilities": {
      "get": {
        "description": "Returns, for every country the compliance profiles know about, the registry providers that can autofill a company form, whether they are configured on this instance, and what identifier the user must type.",
        "operationId": "CompanyLookupController_getCapabilities",
        "parameters": [],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CountryLookupCapabilityDto"
                  }
                }
              }
            }
          }
        },
        "summary": "List which registry lookup is available per country",
        "tags": [
          "company-lookup"
        ]
      }
    },
    "/api/company-lookup/capabilities/{countryCode}": {
      "get": {
        "operationId": "CompanyLookupController_getCapability",
        "parameters": [
          {
            "name": "countryCode",
            "required": true,
            "in": "path",
            "description": "ISO 3166-1 alpha-2 country code",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CountryLookupCapabilityDto"
                }
              }
            }
          }
        },
        "summary": "Registry lookup capability for one country",
        "tags": [
          "company-lookup"
        ]
      }
    },
    "/api/company-lookup": {
      "get": {
        "description": "Resolves a national registration number (or VAT number) against the country’s official register — SIRENE for France, ARES for Czechia, ANAF for Romania, Companies House for the UK, VIES for the rest of the EU, and so on.",
        "operationId": "CompanyLookupController_lookup",
        "parameters": [
          {
            "name": "country",
            "required": true,
            "in": "query",
            "description": "ISO 3166-1 alpha-2 country code",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "value",
            "required": true,
            "in": "query",
            "description": "The identifier as typed by the user",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scheme",
            "required": false,
            "in": "query",
            "description": "Omit to try the registration number first, then the VAT number",
            "schema": {
              "enum": [
                "LEGAL_ID",
                "VAT"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyLookupResultDto"
                }
              }
            }
          }
        },
        "summary": "Look up a company in its national business register",
        "tags": [
          "company-lookup"
        ]
      }
    },
    "/api/danger/otp": {
      "post": {
        "description": "Sends a one-time passcode to the user email to authorize destructive operations.",
        "operationId": "DangerController_requestOtp",
        "parameters": [],
        "responses": {
          "201": {
            "description": "OTP sent"
          },
          "429": {
            "description": "Locked out after too many failed codes — retry after the moment named in the body"
          }
        },
        "summary": "Request OTP for dangerous actions",
        "tags": [
          "danger"
        ]
      }
    },
    "/api/danger/reset/company-data/preflight": {
      "get": {
        "description": "Reports what \"Reset company data\" would delete, and whether it is currently refused because a document is still under legal retention. Read-only — never touches an OTP.",
        "operationId": "DangerController_getCompanyDataResetPreflight",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Preflight result"
          }
        },
        "summary": "Preview a company-data reset",
        "tags": [
          "danger"
        ]
      }
    },
    "/api/danger/reset/company-data": {
      "post": {
        "description": "Deletes every document, client, article, project, time entry, bank statement/reconciliation, archived file and attachment for the active company, while keeping the company itself, its members, its subscription, its channels and its e-mail templates.",
        "operationId": "DangerController_resetCompanyData",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "otp"
                ],
                "properties": {
                  "otp": {
                    "type": "string",
                    "description": "One-time passcode sent via POST /danger/otp"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Company data reset"
          },
          "409": {
            "description": "Refused — a document is still under legal retention"
          }
        },
        "summary": "Reset company data",
        "tags": [
          "danger"
        ]
      }
    },
    "/api/danger/delete-company": {
      "post": {
        "description": "Permanently deletes the active company and everything scoped to it — members, documents, channels, subscription, every setting. A full data export is mailed to the requesting OWNER first. Irreversible.",
        "operationId": "DangerController_deleteCompany",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "otp",
                  "companyName"
                ],
                "properties": {
                  "otp": {
                    "type": "string",
                    "description": "One-time passcode sent via POST /danger/otp"
                  },
                  "companyName": {
                    "type": "string",
                    "description": "The company's own exact, current name"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Company deleted"
          }
        },
        "summary": "Delete company",
        "tags": [
          "danger"
        ]
      }
    },
    "/api/instance/danger/preflight": {
      "get": {
        "description": "How many companies, users and documents an instance-wide reset would delete. Also the sole signal the frontend has for whether this feature exists on this deployment at all — a non-200 response (404 in SaaS, 403 for anyone not an instance operator) hides it entirely.",
        "operationId": "InstanceController_getPreflight",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Preflight counts"
          }
        },
        "summary": "Instance-reset preflight counts",
        "tags": [
          "instance"
        ]
      }
    },
    "/api/instance/danger/otp": {
      "post": {
        "description": "Sends a one-time passcode to the requesting operator's own e-mail address.",
        "operationId": "InstanceController_requestOtp",
        "parameters": [],
        "responses": {
          "201": {
            "description": "OTP sent"
          }
        },
        "summary": "Request an OTP for an instance-wide reset",
        "tags": [
          "instance"
        ]
      }
    },
    "/api/instance/danger/reset": {
      "post": {
        "description": "Deletes every company, user, document and stored file on this deployment, then signs everyone out. Irreversible. Requires a fresh OTP (POST .../danger/otp) AND typing \"RESET INSTANCE\" exactly — both in the request BODY, never a query string (see `danger.controller.ts`'s own comment on this for why).",
        "operationId": "InstanceController_reset",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "otp",
                  "confirmationWord"
                ],
                "properties": {
                  "otp": {
                    "type": "string",
                    "description": "One-time passcode sent via POST /instance/danger/otp"
                  },
                  "confirmationWord": {
                    "type": "string",
                    "description": "Must be exactly \"RESET INSTANCE\""
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Instance reset"
          }
        },
        "summary": "Reset the ENTIRE instance",
        "tags": [
          "instance"
        ]
      }
    },
    "/api/documents/expense-categories": {
      "get": {
        "description": "Includes archived rows unless `includeArchived=false` is passed — the settings screen shows them (greyed out) so a category disappearing from the expense FORM is never a silent deletion from this screen's point of view. Also seeds this company's default category set the first time anything reads this table at all — see `persistence.ts#ensureDefaultExpenseCategoriesSeeded`.",
        "operationId": "ExpenseCategoriesController_list",
        "parameters": [
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Expense categories retrieved"
          }
        },
        "summary": "Every expense category for this company (settings screen's own list)",
        "tags": [
          "documents"
        ]
      },
      "post": {
        "description": "`key` is never accepted here — it is derived from `label` and frozen forever (see schema.prisma's own `ExpenseCategory.key` header).",
        "operationId": "ExpenseCategoriesController_create",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Expense category created"
          }
        },
        "summary": "Create a new expense category",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/expense-categories/{id}": {
      "put": {
        "description": "`key` is immutable after creation — this only ever changes `label`.",
        "operationId": "ExpenseCategoriesController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Expense category updated"
          }
        },
        "summary": "Rename an expense category",
        "tags": [
          "documents"
        ]
      },
      "delete": {
        "description": "Never a hard delete — an expense that already carries this category keeps reading it forever (see persistence.ts's own header). Disappears from the create/edit surface only.",
        "operationId": "ExpenseCategoriesController_archive",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Expense category archived"
          }
        },
        "summary": "Archive (soft-delete) an expense category",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/schedules": {
      "get": {
        "description": "Every DocumentSchedule for the active company — optionally narrowed to one document type.",
        "operationId": "DocumentsController_listSchedules",
        "parameters": [
          {
            "name": "typeId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Schedules retrieved"
          }
        },
        "summary": "List recurrences",
        "tags": [
          "documents"
        ]
      },
      "post": {
        "description": "Replays `actionId` on `sourceDocumentId` at the given cadence, starting at `firstOccurrenceAt` (may be in the past — it becomes due at the very next sweep pass).",
        "operationId": "DocumentsController_createSchedule",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Schedule created"
          },
          "400": {
            "description": "Unknown cadence, or an unparseable firstOccurrenceAt"
          },
          "404": {
            "description": "Unknown type/action, or the source document does not exist"
          }
        },
        "summary": "Create a recurrence",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/schedules/{id}": {
      "patch": {
        "description": "The only write the screen offers on an EXISTING schedule — cadence/source/action are fixed at creation.",
        "operationId": "DocumentsController_updateSchedule",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Schedule updated"
          },
          "404": {
            "description": "Not found for this company"
          }
        },
        "summary": "Enable or disable a recurrence",
        "tags": [
          "documents"
        ]
      },
      "delete": {
        "operationId": "DocumentsController_deleteSchedule",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Schedule deleted"
          },
          "404": {
            "description": "Not found for this company"
          }
        },
        "summary": "Delete a recurrence",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/types": {
      "get": {
        "description": "Every registered document type descriptor, id and label only.",
        "operationId": "DocumentsController_listTypes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Document types retrieved"
          }
        },
        "summary": "List document types",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/email-templates": {
      "get": {
        "description": "Each registered type's CURRENTLY APPLYING email template (the company's own override, else the type's descriptor default, else the generic fallback — `source` says which), plus the `variables` that type actually offers, mapped to sample values: the keys are the available- placeholder list, the values make a preview. Derived per type, so `recipientName` is absent for a type with no client reference and `totalGross` for a type with no money at all.",
        "operationId": "DocumentsController_listEmailTemplates",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Email templates retrieved"
          }
        },
        "summary": "List every document type email template",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/types/{typeId}/email-template": {
      "get": {
        "description": "The same resolved template and derived vocabulary as the list route, for one type.",
        "operationId": "DocumentsController_getEmailTemplate",
        "parameters": [
          {
            "name": "typeId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Email template retrieved"
          },
          "404": {
            "description": "Unknown document type"
          }
        },
        "summary": "One document type's email template",
        "tags": [
          "documents"
        ]
      },
      "put": {
        "description": "Stores this company's own template for the type (`Company.documentEmailTemplates`). The html part is sanitized server-side before storage. An unknown `{placeholder}` is REPORTED in `warnings`, never rejected — the same contract the send path holds, so a typo can never be what blocks a document from reaching a customer. A blank subject, or neither body nor html, IS refused: there would be no message to send.",
        "operationId": "DocumentsController_updateEmailTemplate",
        "parameters": [
          {
            "name": "typeId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string",
                    "description": "Plain-text part"
                  },
                  "html": {
                    "type": "string",
                    "description": "Optional html part, sent alongside the text one"
                  }
                },
                "required": [
                  "subject"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template saved, with any placeholder warnings"
          },
          "400": {
            "description": "Blank subject, or neither a text body nor an html one"
          },
          "404": {
            "description": "Unknown document type"
          }
        },
        "summary": "Save one document type's email template",
        "tags": [
          "documents"
        ]
      },
      "delete": {
        "description": "Drops this company's own override for the type and returns what now applies. Reverting a template that was never overridden is a no-op, never an error.",
        "operationId": "DocumentsController_resetEmailTemplate",
        "parameters": [
          {
            "name": "typeId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Override removed; the applying template is returned"
          },
          "404": {
            "description": "Unknown document type"
          }
        },
        "summary": "Revert one document type's email template to the shipped default",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/types/{typeId}": {
      "get": {
        "description": "The full descriptor (fields, actions) a frontend renders a form from. Each action carries a policyBlockedReason when the active company's country document-action policy refuses it — absent when the action is allowed. Optional `clientId`: when it names a GOVERNMENT client (Client.kind) whose own country declares a B2G routing rule, that rule's own `requiredDocumentFields` are folded into `fields` too (e.g. a French company invoicing a German public body sees the Leitweg-ID input even though this company's own country has no field overlay for it — see documents.service.ts#describeTypeForCompany's own header).",
        "operationId": "DocumentsController_getType",
        "parameters": [
          {
            "name": "typeId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "clientId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Descriptor retrieved"
          },
          "404": {
            "description": "Unknown document type"
          }
        },
        "summary": "Get a document type descriptor",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/types/{typeId}/fields/{fieldKey}/rows": {
      "get": {
        "description": "The rows a 'rowSelection' field may currently offer, given the live value of its sourceField sibling (?sourceId=...) — an empty list, never an error, when that source isn't resolvable yet; the actual block on an invalid selection happens at save time (runAction).",
        "operationId": "DocumentsController_listSelectableRows",
        "parameters": [
          {
            "name": "typeId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fieldKey",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sourceId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Selectable rows retrieved (possibly empty)"
          },
          "400": {
            "description": "The field exists but isn't a valid 'rowSelection' field"
          },
          "404": {
            "description": "Unknown type, or no such field on it"
          }
        },
        "summary": "A 'rowSelection' field's currently selectable rows",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/available-types": {
      "get": {
        "description": "Which document types the Documents sidebar group should show, id and label only — see country-policy/country-policy.ts's resolveAvailableDocumentTypes. `reason` is present, and `types` empty, when the country cannot be resolved or has no document-type policy declared at all — never a silently empty list.",
        "operationId": "DocumentsController_listAvailableTypes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Available types retrieved (possibly empty, with a reason)"
          }
        },
        "summary": "List document types available for the active company's country",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/required-identifiers": {
      "get": {
        "description": "Which identifier schemes (e.g. \"LEGAL_ID\", \"VAT\") a party of the given type must supply for the given country — see country-identifiers/country-identifiers.ts's resolveRequiredIdentifiers. `reason` is present, and `requirements` empty, only when the country has NO identifier-requirements file declared at all; `requirements` can also be legitimately empty WITHOUT a reason when the file exists but declares nothing for this specific party type — never a silently empty form either way. Not scoped by @ActiveCompany(): the country in question is the CALLER's own country picker (a client, the company itself, or a not-yet-created company during onboarding), never the active company's.",
        "operationId": "DocumentsController_listRequiredIdentifiers",
        "parameters": [
          {
            "name": "countryCode",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "partyType",
            "required": true,
            "in": "query",
            "schema": {
              "enum": [
                "COMPANY",
                "INDIVIDUAL"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Requirements retrieved (possibly empty, with a reason)"
          }
        },
        "summary": "Legal identifier requirements for a country and party type",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/b2g-routing": {
      "get": {
        "description": "What sending an invoice to a GOVERNMENT client of the given country requires — the imposed channel/format, any required client identifiers, and any required invoice fields (see b2g-routing/b2g-routing.ts's resolveB2gRoutingRule). `null` means no B2G rule is declared for this country YET — the client edit screen shows this as help, never a block: a client can still be marked GOVERNMENT and saved, the actual refusal only happens when an invoice to it is sent (see actions/invoice-actions.ts's own B2G precedence). Not scoped by @ActiveCompany() — same reasoning as 'required-identifiers' above: this is the CLIENT's own country, unrelated to the active company's.",
        "operationId": "DocumentsController_getB2gRoutingRule",
        "parameters": [
          {
            "name": "countryCode",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The rule, or null when none is declared for this country"
          }
        },
        "summary": "The B2G routing rule declared for a country, if any",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/declarations": {
      "get": {
        "description": "Every DECLARATION journaled onto `DocumentAuthorityEvent` for the active company — see reporting/report-on-send.ts's own header: a country's own tax-authority declaration obligation (e.g. Portugal’s \"pt-at\"), never an ordinary post-deposit conformity poll event (pdp/ksef/chorus-pro — those stay on `GET :id/authority-events` only). Paginated, most recent first, optionally narrowed to an exact `status` code. `hasObligation` is `false` for a country with no `reporting/data/*.json` fact at all (and `undefined` only when the company’s own country cannot even be resolved) — what lets the screen say so plainly instead of showing a permanently empty list with no explanation.",
        "operationId": "DocumentsController_listDeclarations",
        "parameters": [
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "Page number (1-indexed). Defaults to 1.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "description": "Exact statusCode filter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Declarations retrieved (possibly empty)"
          }
        },
        "summary": "List the active company's declarative-reporting events",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/dashboard": {
      "get": {
        "description": "Every widget every document type contributes to the dashboard — see contributions/. A type that declares a dashboard contribution but has none implemented shows up as an explicit \"unimplemented\" widget, never a silent gap.",
        "operationId": "DocumentsController_listDashboardWidgets",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Widgets retrieved"
          }
        },
        "summary": "Dashboard widgets",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/statistics": {
      "get": {
        "description": "Same mechanism as GET documents/dashboard, for the Statistics screen.",
        "operationId": "DocumentsController_listStatisticsWidgets",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Widgets retrieved"
          }
        },
        "summary": "Statistics widgets",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/transports": {
      "get": {
        "description": "Every registered document transport, id and label only — what a company chooses from for Company.invoiceTransportId. Never scoped by country: the choice is a company setting.",
        "operationId": "DocumentsController_listTransports",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Transports retrieved"
          }
        },
        "summary": "List document transports",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/events": {
      "get": {
        "description": "One message per persisted transition (sending/sent/send_failed) or newly-journaled authority-event batch, scoped to the active company — {documentId, typeId, kind} only, never the resulting state. See queue/document-events-publisher.ts for the worker→API Redis pub/sub bridge this is fed from.",
        "operationId": "DocumentsController_streamEvents",
        "parameters": [],
        "responses": {
          "200": {
            "description": "text/event-stream — JSON document events plus periodic \"heartbeat\"-typed keep-alives"
          }
        },
        "summary": "Live document status/conformity events (SSE)",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/references/{entity}/search": {
      "get": {
        "description": "Generic search behind a \"reference\" field, regardless of which entity it targets.",
        "operationId": "DocumentsController_searchReferences",
        "parameters": [
          {
            "name": "entity",
            "required": true,
            "in": "path",
            "description": "e.g. \"client\"",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching options retrieved"
          },
          "404": {
            "description": "Unknown reference entity"
          }
        },
        "summary": "Search a reference entity",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/references/{entity}/{refId}": {
      "get": {
        "description": "The {id, label} for one entity id — used to display an already-set reference field.",
        "operationId": "DocumentsController_resolveReference",
        "parameters": [
          {
            "name": "entity",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "refId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Option retrieved (or null if the id does not resolve)"
          },
          "404": {
            "description": "Unknown reference entity"
          }
        },
        "summary": "Resolve a reference value",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/references/{entity}/{refId}/fields": {
      "get": {
        "description": "The raw field values behind a field declaring `prefillFrom` (e.g. an article's name/unitPrice/vatRate) — null when the id does not resolve, or when this entity has no prefill data to offer at all (most reference entities do not; see EntityReferenceProvider's optional `getFields`).",
        "operationId": "DocumentsController_getReferenceFields",
        "parameters": [
          {
            "name": "entity",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "refId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fields retrieved (or null)"
          },
          "404": {
            "description": "Unknown reference entity"
          }
        },
        "summary": "A reference entity's own raw fields, for prefilling a row",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/attachments/upload": {
      "post": {
        "description": "Stores the file content-addressed for the active company. Refused, named, for a disallowed mime type or a file over the size limit — see AttachmentsService for both.",
        "operationId": "DocumentsController_uploadAttachment",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File stored — { fileRef, fileName, mime }"
          },
          "400": {
            "description": "Missing file, an empty file, or a disallowed mime"
          },
          "413": {
            "description": "The file is over the size limit"
          }
        },
        "summary": "Upload an attachment (a photo or PDF of a receipt, today)",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/attachments/{fileRef}": {
      "get": {
        "operationId": "DocumentsController_downloadAttachment",
        "parameters": [
          {
            "name": "fileRef",
            "required": true,
            "in": "path",
            "description": "The attachment's own SHA-256, from the upload response",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mime",
            "required": true,
            "in": "query",
            "description": "The mime the upload response carried",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File bytes, verbatim",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Not found for this company, or the file is no longer on disk"
          }
        },
        "summary": "An attachment's original uploaded bytes",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/types/{typeId}/actions/{actionId}": {
      "post": {
        "description": "Runs one action declared on a document type (e.g. \"save-draft\"), native or attached by a third party. 501 if the action is declared but has no registered implementation.",
        "operationId": "DocumentsController_runAction",
        "parameters": [
          {
            "name": "typeId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Action ran, a result envelope (document/changed/message)"
          },
          "400": {
            "description": "Document data, or the action's own params, are invalid"
          },
          "403": {
            "description": "A MEMBER ran \"send\" on a document whose gross total exceeds the company's configured approval threshold — an ADMIN or OWNER must send it instead\n\nThe active company's country document-action policy forbids this action"
          },
          "404": {
            "description": "Unknown type, or action not declared on it"
          },
          "409": {
            "description": "Action not available for the record's current status"
          },
          "501": {
            "description": "Action declared but not implemented"
          }
        },
        "summary": "Run a document action",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/types/{typeId}/actions/{actionId}/params/defaults": {
      "post": {
        "description": "Optional pre-fill for the action params form (e.g. \"send\" pre-filling the recipient from the document's client) — {} when the action declares no defaults resolver, never an error.",
        "operationId": "DocumentsController_resolveActionParamsDefaults",
        "parameters": [
          {
            "name": "typeId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Default param values retrieved (possibly empty)"
          },
          "404": {
            "description": "Unknown type, or action not declared on it"
          }
        },
        "summary": "Get default values for an action's own parameters",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents": {
      "get": {
        "description": "One PAGE of instances for the active company, filtered and sorted server-side — `{ items, total, page, pageSize }`, never a bare array. `clientId`/`dateFrom`/`dateTo`/`q` each read the named type's own descriptor (which field is its client reference, which its issuance date, which of its `listItem.titleFields` are free text) and so all four REQUIRE `typeId` — a 400 otherwise, and a 400 too for a filter naming a field this specific type has none of (never a silent, indistinguishable-from-\"nothing matched\" empty page).",
        "operationId": "DocumentsController_listDocuments",
        "parameters": [
          {
            "name": "order",
            "required": false,
            "in": "query",
            "schema": {
              "enum": [
                "asc",
                "desc"
              ],
              "type": "string"
            }
          },
          {
            "name": "sort",
            "required": false,
            "in": "query",
            "schema": {
              "enum": [
                "updatedAt",
                "createdAt",
                "number",
                "status"
              ],
              "type": "string"
            }
          },
          {
            "name": "q",
            "required": false,
            "in": "query",
            "description": "Case-insensitive: document number, title field(s), client name. Requires typeId.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateTo",
            "required": false,
            "in": "query",
            "description": "YYYY-MM-DD, requires typeId.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateFrom",
            "required": false,
            "in": "query",
            "description": "YYYY-MM-DD, requires typeId.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "clientId",
            "required": false,
            "in": "query",
            "description": "Requires typeId.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "description": "Repeatable, or one comma-separated value. OR-ed together.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "pageSize",
            "required": false,
            "in": "query",
            "description": "Default 25, clamped to 100.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "1-indexed. Default 1.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "typeId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of instances retrieved"
          },
          "400": {
            "description": "A malformed page/pageSize/date, an unknown sort field, or a descriptor filter with no typeId"
          }
        },
        "summary": "List document instances",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/totals": {
      "get": {
        "description": "Computes net, VAT, and gross totals (in minor units) for a document instance.",
        "operationId": "DocumentsController_computeTotals",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Totals computed"
          },
          "404": {
            "description": "Not found for this company/type"
          }
        },
        "summary": "Compute document totals",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/settlement": {
      "get": {
        "description": "Totals, the payments recorded against this document, and the resulting balance (paid / outstanding / overpaid — see settlement/compute-settlement.ts). Same mould as GET .../totals.",
        "operationId": "DocumentsController_getSettlement",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Settlement computed"
          },
          "404": {
            "description": "Not found for this company/type"
          }
        },
        "summary": "Compute a document instance's payment settlement",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/tax-warnings": {
      "get": {
        "description": "The warnings this invoice's own cross-border tax resolution records — a buyer VAT number that could not be confirmed (so the sale was taxed as a consumer sale), a destination whose reduced rates are not modelled (so the line may be over-taxed), a line with no declared supply type. Never a refusal: a send is blocked by its own named 400, and a block is reported here as an empty list, not as an error. Recomputed on every read (see DocumentsService.getTaxWarnings); any type but \"invoice\" answers an empty list.",
        "operationId": "DocumentsController_getTaxWarnings",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tax warnings computed (possibly an empty list)"
          },
          "404": {
            "description": "Not found for this company/type"
          }
        },
        "summary": "A document's non-fatal tax caveats",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/correction-routes": {
      "get": {
        "description": "Every correction route (CREDIT_NOTE, INTERNAL_CREDIT_NOTE, …) the active company's own country declares for this document, each with its status (required/allowed/forbidden/unverified), its legal label VERBATIM (never a summary), and whether this repo actually implements it today — only INTERNAL_CREDIT_NOTE does. `limitation` always names the seller-only scope of this read (see correction-routes/correction-routes.ts's own header on the unwritten seller×buyer composition). V1 only answers for typeId=\"invoice\".",
        "operationId": "DocumentsController_getCorrectionRoutes",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Correction routes retrieved"
          },
          "404": {
            "description": "Unknown document type, document not found, or no correction-routes rule for this country"
          },
          "409": {
            "description": "The document is still a \"draft\" — nothing issued to correct yet"
          },
          "501": {
            "description": "typeId is not \"invoice\" — not supported by this endpoint yet"
          }
        },
        "summary": "A document's correction routes, for its SELLER country",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/pdf": {
      "get": {
        "description": "Serves the document as a PDF — straight from its own send-time archive when one exists (documents.service.ts#renderInstancePdf), never re-rendering (no Chromium launched) in that case; a document with nothing archived yet (a draft, or one delivered through a channel with no plain-PDF artifact) renders fresh.",
        "operationId": "DocumentsController_renderPdf",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PDF generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Not found for this company/type"
          },
          "500": {
            "description": "PDF rendering failed"
          }
        },
        "summary": "Get a document instance as PDF",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/formats/{syntax}": {
      "get": {
        "description": "Builds and validates a normalized XML export (CII or UBL — see the \"download-xml\" action's own `syntax` param) on demand, same mould as GET .../pdf. Never serves an artifact that failed EN 16931 validation.",
        "operationId": "DocumentsController_downloadFormat",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "syntax",
            "required": true,
            "in": "path",
            "description": "e.g. \"cii\" or \"ubl\"",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "XML generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "The generated document failed EN 16931 validation, or could not be built"
          },
          "403": {
            "description": "The active company's country document-action policy forbids it"
          },
          "404": {
            "description": "Not found for this company/type"
          },
          "409": {
            "description": "Not available for the record's current status (e.g. still a draft)"
          },
          "501": {
            "description": "Unknown/unimplemented format"
          }
        },
        "summary": "Get a normalized EN 16931 export of a document instance",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/archives": {
      "get": {
        "description": "Every legal archive written for this document, most recent first: DELIVERY rows (one per successful send that produced at least one artifact) and, since 2026-09-06, VERDICT rows (the authority's own terminal verdict on a deposit — see `DocumentArchive`’s own schema comment). Distinguish them via `kind`.",
        "operationId": "DocumentsController_listDocumentArchives",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archives retrieved (possibly empty)"
          },
          "404": {
            "description": "Not found for this company/type"
          }
        },
        "summary": "List the legal archives of a document instance",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/authority-events": {
      "get": {
        "description": "Every event the ISSUING PLATFORM itself reported (e.g. PDP's fr:200/201/202/213), most recent first, append-only. Empty for a document sent by a channel with no conformity poller (e.g. \"email\", or \"sdi\" — push-only notifiche).",
        "operationId": "DocumentsController_listAuthorityEvents",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Events retrieved (possibly empty)"
          },
          "404": {
            "description": "Not found for this company/type"
          }
        },
        "summary": "List the post-deposit conformity events of a document instance",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/archives/{archiveId}/verify": {
      "post": {
        "description": "RE-HASHES the bytes actually stored on disk and compares them against the hash recorded at archive time — never a bare re-read of the stored hash. Never mutates the archive row, even when it reports a corruption.",
        "operationId": "DocumentsController_verifyDocumentArchive",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "archiveId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "\"intact\", or \"corrupted\" with the mismatching artifact(s) named"
          },
          "404": {
            "description": "Not found for this company/type, or unknown archiveId"
          }
        },
        "summary": "Verify one legal archive’s integrity",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/share-link": {
      "post": {
        "description": "Mints a new, high-entropy token (see share-links/share-link-token.ts) and returns the PUBLIC url ONCE — the raw token is never stored (only its hash) and this response is the only time this API ever hands it back; GET .../share-links afterwards shows only metadata (createdAt/expiresAt/revokedAt), never the token itself. Same four-gate story as \"download-xml\" (documents.service.ts#downloadDocumentFormat) — only country policy (403) and status (409) ever fire for this action: a draft document has no number and no legal existence to share yet.",
        "operationId": "DocumentsController_createShareLink",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Link created — the token is shown here, and only here"
          },
          "403": {
            "description": "The active company's country document-action policy forbids it"
          },
          "404": {
            "description": "Not found for this company/type, or type has no such action"
          },
          "409": {
            "description": "Not available for the record's current status (e.g. a draft)"
          }
        },
        "summary": "Create a public share link",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/share-links": {
      "get": {
        "description": "Metadata only (id/createdAt/expiresAt/revokedAt/active) — never the token or its hash. See POST .../share-link for the one-time creation response that DOES carry the token.",
        "operationId": "DocumentsController_listShareLinks",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Share links retrieved (possibly empty)"
          },
          "404": {
            "description": "Not found for this company/type"
          }
        },
        "summary": "List the public share links of a document instance",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}/share-link/{tokenId}": {
      "delete": {
        "description": "A SOFT delete — sets `revokedAt`, never removes the row (who shared what, and when it was pulled back, is information worth keeping). The public url stops resolving immediately: GET /api/public/documents/:token/pdf answers the exact same 404 a revoked token gets as an expired or an unknown one.",
        "operationId": "DocumentsController_revokeShareLink",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tokenId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Link revoked"
          },
          "404": {
            "description": "Not found for this company/type, or unknown share link"
          }
        },
        "summary": "Revoke a public share link",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/{id}": {
      "get": {
        "description": "One saved document instance by id.",
        "operationId": "DocumentsController_getDocument",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Instance retrieved"
          },
          "404": {
            "description": "Not found for this company/type"
          }
        },
        "summary": "Get a document instance",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/accounting-export": {
      "get": {
        "description": "The GENERIC accounting CSV export — one row per real invoice/credit-note/payment whose own date falls in [from, to], amounts/dates reused verbatim from totals/compute-totals.ts and settlement/compute-settlement.ts, never recomputed. Country-specific ledger FORMATS (FR FEC, DE DATEV) are out of scope for this endpoint.",
        "operationId": "AccountingExportController_exportCsv",
        "parameters": [
          {
            "name": "from",
            "required": true,
            "in": "query",
            "description": "YYYY-MM-DD, inclusive",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "required": true,
            "in": "query",
            "description": "YYYY-MM-DD, inclusive",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "\"from\"/\"to\" missing, malformed, or \"from\" is after \"to\""
          }
        },
        "summary": "Export the company ledger (invoices, credit notes, payments) as CSV over a date range",
        "tags": [
          "accounting-export"
        ]
      }
    },
    "/api/public/documents/{token}/pdf": {
      "get": {
        "description": "Resolves `token` by its hash (never logged, never stored in the clear — see share-links/share-link-token.ts) and, if it is still valid, serves EXACTLY the same PDF the authenticated GET /api/documents/:id/pdf would — documents.service.ts#renderInstancePdf, never a second implementation, and never a second Chromium launch on THIS route either: an already-sent document is served straight from its own send-time archive; only a document with nothing archived yet renders fresh (still the same rendering + PAdES signing pipeline). An unknown token, an EXPIRED one, and a REVOKED one all answer the exact same 404, with the exact same body: this endpoint never lets a caller distinguish \"this link once existed\" from \"this link was never real\". No company data beyond the PDF itself is ever exposed here. `Cache-Control: private, no-store`, the same header the signature route (public-signatures.controller.ts) sets for the same reason: a real invoice or quote must not be written to any cache, least of all a shared machine's own browser disk cache, where it would reopen without the token.",
        "operationId": "PublicDocumentsController_getSharedPdf",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PDF retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Unknown, expired, or revoked token — indistinguishable"
          }
        },
        "summary": "Download a shared document PDF — no session required",
        "tags": [
          "public-documents"
        ]
      }
    },
    "/api/public/signatures/{token}": {
      "get": {
        "description": "Returns the minimal facts the /signature/:token page needs to render (which document type, its display number) — nothing about the client or the document CONTENT. An unknown, locked, or already-signed token answers the exact same 400 as every other route on this controller.",
        "operationId": "PublicSignaturesController_resolve",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Signature request resolved"
          },
          "400": {
            "description": "Unknown, locked, or already-used token"
          }
        },
        "summary": "Resolve a signature request by its raw token — no session required",
        "tags": [
          "public-signatures"
        ]
      }
    },
    "/api/public/signatures/{token}/document": {
      "get": {
        "description": "Serves the SAME artifact `sign` seals: rendered once, on whichever call reaches it first, and served byte-for-byte identical on every later call (SignaturesService.getPublicDocument's own header) — never a fresh render per request, which a company with an active signing certificate could not guarantee to stay byte-identical to what an earlier viewer saw. Deliberately the SAME 400, with the SAME body, for an unknown, locked, signed, or expired token as every other route on this controller — see this controller's own header. `Cache-Control: private, no-store` because this is a specific, unauthenticated party's own document, never something a shared cache (or the browser's own disk cache on a shared machine) should retain.",
        "operationId": "PublicSignaturesController_getDocument",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PDF retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Unknown, locked, signed, or expired token — indistinguishable"
          }
        },
        "summary": "The exact PDF this signature will seal — no session required",
        "tags": [
          "public-signatures"
        ]
      }
    },
    "/api/public/signatures/{token}/otp": {
      "post": {
        "description": "Capped at 3 mints per signature request, EVER (signatures/otp.ts#MAX_OTP_MINTS) — never a widening of the separate, lifetime `otpFailedAttempts` budget `sign` below is bound by.",
        "operationId": "PublicSignaturesController_requestOtp",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A verification code was emailed"
          },
          "400": {
            "description": "Unknown/locked/used token, or the resend cap was reached"
          }
        },
        "summary": "Mint and email a fresh OTP for this signature request — no session required",
        "tags": [
          "public-signatures"
        ]
      }
    },
    "/api/public/signatures/{token}/sign": {
      "post": {
        "description": "An unknown token, a locked one, an already-signed one, an expired-or-never-minted OTP, and a wrong OTP code all answer the EXACT SAME 400, with the exact same body — see the header on SignaturesService.verifyAndSign for why that indistinguishability is the point. A wrong code counts against this signature request LIFETIME attempt budget (signatures/otp.ts#MAX_FAILED_ATTEMPTS) regardless of this route own per-IP throttle.",
        "operationId": "PublicSignaturesController_sign",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Document signed"
          },
          "400": {
            "description": "Invalid, expired, locked, or already-used — indistinguishable"
          }
        },
        "summary": "Verify the submitted OTP and, on success, sign the document — no session required",
        "tags": [
          "public-signatures"
        ]
      }
    },
    "/api/documents/received-invoices/reconciliation-settings": {
      "get": {
        "operationId": "ReceivedInvoicesController_getReconciliationSettings",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The configured tolerance, or the default if never set"
          }
        },
        "summary": "This company's own 3-way-match tolerance (percent)",
        "tags": [
          "documents"
        ]
      },
      "put": {
        "operationId": "ReceivedInvoicesController_setReconciliationSettings",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tolerancePercent": {
                    "type": "number",
                    "example": 2
                  }
                },
                "required": [
                  "tolerancePercent"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The newly saved tolerance"
          },
          "400": {
            "description": "tolerancePercent is missing, negative, or not a number"
          }
        },
        "summary": "Set this company's own 3-way-match tolerance (percent)",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/received-invoices/upload/{fileRef}/ocr": {
      "get": {
        "operationId": "ReceivedInvoicesController_getUploadOcrStatus",
        "parameters": [
          {
            "name": "fileRef",
            "required": true,
            "in": "path",
            "description": "The 64-character lowercase-hex SHA-256 from the upload response",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ status: \"pending\" } while running, or { status: \"done\", extraction, supplierMatch, ocr }"
          },
          "400": {
            "description": "fileRef is not a 64-character lowercase hex SHA-256"
          },
          "404": {
            "description": "No OCR job exists for this company/fileRef — expired or never enqueued"
          }
        },
        "summary": "Poll the asynchronous OCR result for a pending upload",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/received-invoices/{id}/reconciliation": {
      "get": {
        "operationId": "ReceivedInvoicesController_getReconciliation",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reconciliation result (or hasPurchaseOrder: false)"
          },
          "404": {
            "description": "Not found for this company, or its own purchase order is dangling"
          }
        },
        "summary": "The 3-way-match reconciliation for this received invoice",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/received-invoices/{id}/accept-variance": {
      "post": {
        "operationId": "ReceivedInvoicesController_acceptVariance",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The freshly re-resolved reconciliation, now accepted"
          },
          "404": {
            "description": "Not found for this company"
          }
        },
        "summary": "Accept the reconciliation variance for this received invoice",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/received-invoices/upload": {
      "post": {
        "description": "Stores the file content-addressed and attempts structural field extraction — never refused for an unrecognized STRUCTURE (a plain scanned PDF still stores and returns empty fields), only for a disallowed mime, a magic-byte mismatch, an oversized file, or an exact repeat of an already-received file (same SHA-256) — see ReceivedInvoicesService/upload-validation.ts.",
        "operationId": "ReceivedInvoicesController_upload",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File stored, extraction preview returned"
          },
          "400": {
            "description": "Missing file, an empty file, a disallowed mime, or a magic-byte mismatch against it"
          },
          "409": {
            "description": "This exact file was already received (named, by hash)"
          },
          "413": {
            "description": "The file is over the size limit"
          }
        },
        "summary": "Upload an inbound invoice file (PDF, or XML CII/UBL, or Factur-X)",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/documents/received-invoices/{id}/file": {
      "get": {
        "operationId": "ReceivedInvoicesController_downloadFile",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File bytes, verbatim",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Not found for this company, or the file is no longer on disk"
          }
        },
        "summary": "A received invoice's original uploaded file",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/clients/{clientId}/portal-access": {
      "post": {
        "description": "Mints a long-lived (30 days), revocable link and, when the client has a `contactEmail` on file, emails it — the response also carries the raw URL, shown and copyable exactly once, the same \"shown once\" contract a document share link already holds.",
        "operationId": "PortalAccessController_create",
        "parameters": [
          {
            "name": "clientId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Portal access created"
          },
          "404": {
            "description": "Client not found for this company"
          }
        },
        "summary": "Invites a client to the client portal",
        "tags": [
          "client-portal"
        ]
      },
      "get": {
        "operationId": "PortalAccessController_list",
        "parameters": [
          {
            "name": "clientId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Portal access list retrieved"
          }
        },
        "summary": "A client's own portal-access history",
        "tags": [
          "client-portal"
        ]
      },
      "delete": {
        "description": "A single \"cut off portal access entirely\" call — e.g. a leaked link — rather than revoking each active invite one by one.",
        "operationId": "PortalAccessController_revokeAll",
        "parameters": [
          {
            "name": "clientId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          },
          "404": {
            "description": "Client not found for this company"
          }
        },
        "summary": "Revokes EVERY active portal-access link for this client",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/clients/{clientId}/portal-access/{tokenId}": {
      "delete": {
        "operationId": "PortalAccessController_revoke",
        "parameters": [
          {
            "name": "clientId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tokenId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          },
          "404": {
            "description": "Not found for this client"
          }
        },
        "summary": "Revokes one portal-access link",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/portal/me": {
      "get": {
        "operationId": "PortalController_getProfile",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Profile resolved"
          }
        },
        "summary": "The signed-in client and company display names",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/portal/statement": {
      "get": {
        "description": "Exactly `GET /clients/:id/statement` (settlement/client-statement.ts), scoped to the session's own clientId — never a recomputation of the balance.",
        "operationId": "PortalController_getStatement",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Statement computed"
          }
        },
        "summary": "This client's own account statement",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/portal/quotes": {
      "get": {
        "operationId": "PortalController_listQuotes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Quotes listed"
          }
        },
        "summary": "Every quote awaiting, or already answered by, this client",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/portal/quotes/{id}/request-signature": {
      "post": {
        "description": "Never signs anything itself — it mints the same signature request `SignaturesService.requestSignature` already sends for a company-triggered request, emailing the client the `/signature/:token` link they still have to open and complete.",
        "operationId": "PortalController_requestQuoteSignature",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Quote ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Signature request sent"
          },
          "404": {
            "description": "Not this client’s quote"
          },
          "409": {
            "description": "Quote no longer awaiting a decision"
          }
        },
        "summary": "Starts the EXISTING, OTP-hardened signature request for this quote",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/portal/quotes/{id}/refuse": {
      "post": {
        "operationId": "PortalController_refuseQuote",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Quote ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Quote refused"
          },
          "404": {
            "description": "Not this client’s quote"
          },
          "409": {
            "description": "Quote no longer awaiting a decision"
          }
        },
        "summary": "Declines a quote — a reversible preference, not a signature",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/portal/documents/invoice/{id}/checkout-session": {
      "post": {
        "description": "The Pay link (online payment). Delegates to `PaymentSessionsService` — never writes a DocumentPayment or touches a provider secret itself. See that service's own header for the amount guard (always the fresh outstanding balance), the provider connectivity check (501), and the status check (409) this route can surface.",
        "operationId": "PortalController_createCheckoutSession",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Invoice ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Checkout session opened"
          },
          "404": {
            "description": "Not this client’s invoice"
          },
          "409": {
            "description": "Invoice not \"sent\", or already fully settled"
          },
          "501": {
            "description": "No payment provider connected for this company"
          }
        },
        "summary": "Opens a payment checkout session for this client's own invoice",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/portal/documents/{typeId}/{id}/pdf": {
      "get": {
        "description": "Byte-for-byte the same rendering pipeline the staff-facing download and the public share link both already use. 404s — never 403 — for a document belonging to another client of this same company, or parked in a status this type never exposes to a portal at all.",
        "operationId": "PortalController_getDocumentPdf",
        "parameters": [
          {
            "name": "typeId",
            "required": true,
            "in": "path",
            "description": "invoice | quote | credit-note",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Document ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PDF retrieved"
          },
          "404": {
            "description": "Not this client’s document"
          }
        },
        "summary": "One of this client's own documents, as a PDF",
        "tags": [
          "client-portal"
        ]
      }
    },
    "/api/projects": {
      "get": {
        "description": "Returns this company's projects, active ones only unless includeArchived=true.",
        "operationId": "ProjectsController_findAll",
        "parameters": [
          {
            "name": "clientId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Projects retrieved"
          }
        },
        "summary": "List projects",
        "tags": [
          "time-tracking"
        ]
      },
      "post": {
        "description": "A billing bucket between a client and its logged time entries — see Project's own schema comment.",
        "operationId": "ProjectsController_create",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Project created"
          },
          "404": {
            "description": "Client not found"
          }
        },
        "summary": "Create a project",
        "tags": [
          "time-tracking"
        ]
      }
    },
    "/api/projects/{id}": {
      "get": {
        "operationId": "ProjectsController_findOne",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Project retrieved"
          }
        },
        "summary": "Get a project",
        "tags": [
          "time-tracking"
        ]
      },
      "patch": {
        "description": "Also how a project is archived — set isArchived: true (there is no hard delete: a project keeps every time entry it ever billed).",
        "operationId": "ProjectsController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Project updated"
          },
          "404": {
            "description": "Project not found"
          }
        },
        "summary": "Update a project",
        "tags": [
          "time-tracking"
        ]
      }
    },
    "/api/time-entries": {
      "get": {
        "description": "Filter by project, by client (across every one of its projects), and/or to only the unbilled+billable ones (what the \"generate invoice\" picker offers).",
        "operationId": "TimeEntriesController_findAll",
        "parameters": [
          {
            "name": "projectId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "clientId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "unbilledOnly",
            "required": false,
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Time entries retrieved"
          }
        },
        "summary": "List time entries",
        "tags": [
          "time-tracking"
        ]
      },
      "post": {
        "operationId": "TimeEntriesController_create",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Time entry logged"
          },
          "404": {
            "description": "Project not found"
          }
        },
        "summary": "Log a time entry",
        "tags": [
          "time-tracking"
        ]
      }
    },
    "/api/time-entries/{id}": {
      "patch": {
        "operationId": "TimeEntriesController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Time entry updated"
          },
          "404": {
            "description": "Time entry not found"
          },
          "409": {
            "description": "The entry has already been billed and is now read-only"
          }
        },
        "summary": "Edit a time entry",
        "tags": [
          "time-tracking"
        ]
      },
      "delete": {
        "operationId": "TimeEntriesController_remove",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Time entry deleted"
          },
          "404": {
            "description": "Time entry not found"
          },
          "409": {
            "description": "The entry has already been billed and can no longer be deleted"
          }
        },
        "summary": "Delete a time entry",
        "tags": [
          "time-tracking"
        ]
      }
    },
    "/api/time-entries/generate-invoice": {
      "post": {
        "description": "Atomically creates a new draft invoice with one line per selected entry and marks every one of them billed — see TimeEntriesService.billToInvoice for the double-billing guard.",
        "operationId": "TimeEntriesController_generateInvoice",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Draft invoice created, entries marked billed"
          },
          "400": {
            "description": "No entries selected, or one cannot be billed yet (rate, client, …)"
          },
          "404": {
            "description": "Client not found"
          },
          "409": {
            "description": "One or more entries were already billed"
          }
        },
        "summary": "Bill selected time entries to a new draft invoice",
        "tags": [
          "time-tracking"
        ]
      }
    },
    "/api/bank-reconciliation/statements": {
      "post": {
        "description": "Parses the file and stores one row per transaction line — never a persisted match: a line only ever produces a real payment once a human confirms one via POST .../lines/:id/reconcile. A CSV file additionally requires `mapping` (see CsvColumnMapping) — OFX is self-describing.",
        "operationId": "BankReconciliationController_importStatement",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "fileName": {
                    "type": "string",
                    "example": "releve-2026-08.csv"
                  },
                  "base64": {
                    "type": "string",
                    "description": "Base64-encoded raw file bytes."
                  },
                  "currency": {
                    "type": "string",
                    "example": "EUR"
                  },
                  "mapping": {
                    "type": "object",
                    "description": "Required for a CSV file, ignored for OFX."
                  }
                },
                "required": [
                  "fileName",
                  "base64",
                  "currency"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Statement stored, one row per readable transaction line"
          },
          "400": {
            "description": "Missing fields, an invalid mapping, or an unparseable file"
          }
        },
        "summary": "Import a bank statement (CSV or OFX)",
        "tags": [
          "bank-reconciliation"
        ]
      },
      "get": {
        "operationId": "BankReconciliationController_listStatements",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Statements, most recently imported first"
          }
        },
        "summary": "List every imported bank statement for the active company",
        "tags": [
          "bank-reconciliation"
        ]
      }
    },
    "/api/bank-reconciliation/statements/{id}/lines": {
      "get": {
        "description": "Suggestions are computed fresh on every read, never stored — see BankReconciliationService.",
        "operationId": "BankReconciliationController_getStatementLines",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The statement, its outstanding-invoice candidate pool, and its lines"
          },
          "404": {
            "description": "Statement not found for this company"
          }
        },
        "summary": "One statement's own lines, each with its live suggested matches",
        "tags": [
          "bank-reconciliation"
        ]
      }
    },
    "/api/bank-reconciliation/lines/{id}/reconcile": {
      "post": {
        "description": "Runs through the exact same \"record-payment\" action a hand-entered payment uses — see BankReconciliationService.reconcileLine for why. Refuses (409) a line already reconciled.",
        "operationId": "BankReconciliationController_reconcileLine",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "documentId": {
                    "type": "string"
                  }
                },
                "required": [
                  "documentId"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Reconciled — a DocumentPayment now exists for this line"
          },
          "400": {
            "description": "documentId missing, or the line is a debit (money-out) row"
          },
          "404": {
            "description": "Line, or the named invoice, not found for this company"
          },
          "409": {
            "description": "This line has already been reconciled"
          }
        },
        "summary": "Confirm a match: creates a real invoice payment from a bank statement line",
        "tags": [
          "bank-reconciliation"
        ]
      }
    },
    "/api/payments/{documentId}/sessions": {
      "get": {
        "description": "Every session ever opened for this invoice, most recently opened first — status only, never a provider credential. Company-scoped: another company's documentId resolves to an empty list.",
        "operationId": "PaymentsController_listSessions",
        "parameters": [
          {
            "name": "documentId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sessions retrieved"
          }
        },
        "summary": "One invoice's own checkout sessions",
        "tags": [
          "payments"
        ]
      }
    },
    "/api/payment-methods": {
      "get": {
        "description": "Always the full registered list (BUILT_IN_PAYMENT_METHODS), configured or not — never filtered down to only the enabled ones: the screen needs to offer every method, not just the ones already on. Each entry's own `configured` says whether its current `config` already satisfies every field the method requires — the settings screen uses it to decide whether flipping a method on can go straight through, or needs its config dialog first.",
        "operationId": "PaymentMethodsController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Payment methods retrieved"
          }
        },
        "summary": "Every registered payment method, and this company's own configuration for each",
        "tags": [
          "payment-methods"
        ]
      }
    },
    "/api/payment-methods/{methodId}": {
      "patch": {
        "description": "`enabled` and `config` are each independently optional — a plain enable/disable toggle sends only `enabled`; the config dialog sends both. `config`, when sent, REPLACES the stored value wholesale. A method left `enabled: true` must carry every field it declares required, or this is refused (400) — see persistence.ts#updateCompanyPaymentMethodConfig.",
        "operationId": "PaymentMethodsController_update",
        "parameters": [
          {
            "name": "methodId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payment method updated"
          },
          "403": {
            "description": "Not an OWNER/ADMIN, or an API key without company:write"
          }
        },
        "summary": "Update this company's own configuration for one payment method",
        "tags": [
          "payment-methods"
        ]
      }
    },
    "/api/custom-fields": {
      "get": {
        "description": "Includes archived rows unless `includeArchived=false` is passed — the settings screen shows them (greyed out, with a \"restore\" action) so a definition disappearing is never a silent deletion from this screen's point of view.",
        "operationId": "CompanyCustomFieldsController_list",
        "parameters": [
          {
            "name": "target",
            "required": false,
            "in": "query",
            "schema": {
              "enum": [
                "CLIENT",
                "DOCUMENT"
              ],
              "type": "string"
            }
          },
          {
            "name": "documentTypeId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Custom field definitions retrieved"
          }
        },
        "summary": "Every custom field definition for this company (settings screen's own list)",
        "tags": [
          "custom-fields"
        ]
      },
      "post": {
        "description": "`key` is never accepted here — it is derived from `label` and frozen forever (see schema.prisma's own `CompanyCustomField.key` header).",
        "operationId": "CompanyCustomFieldsController_create",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Custom field definition created"
          },
          "403": {
            "description": "Not an OWNER/ADMIN, or an API key without company:write"
          }
        },
        "summary": "Create a new custom field definition",
        "tags": [
          "custom-fields"
        ]
      }
    },
    "/api/custom-fields/resolved": {
      "get": {
        "description": "What the document form (`target=DOCUMENT&typeId=invoice`), the document LIST, and the client form (`target=CLIENT`) all fetch and render through the exact same generic field-renderer registry every native field already uses. `includeArchived=false` (the default — what the CREATE/EDIT form asks for) excludes archived rows; the LIST (document-list.tsx) asks with `includeArchived=true` instead, so an archived definition's already-recorded value keeps showing (see persistence.ts's own header).",
        "operationId": "CompanyCustomFieldsController_resolved",
        "parameters": [
          {
            "name": "target",
            "required": true,
            "in": "query",
            "schema": {
              "enum": [
                "CLIENT",
                "DOCUMENT"
              ],
              "type": "string"
            }
          },
          {
            "name": "typeId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved field descriptors"
          }
        },
        "summary": "Definitions for one target, as ready-to-render DocumentFieldDescriptor[]",
        "tags": [
          "custom-fields"
        ]
      }
    },
    "/api/custom-fields/{id}": {
      "patch": {
        "description": "`key`/`kind`/`target`/`documentTypeId` are immutable after creation — not part of this request body at all (see UpdateCompanyCustomFieldInput's own header).",
        "operationId": "CompanyCustomFieldsController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Custom field definition updated"
          },
          "403": {
            "description": "Not an OWNER/ADMIN, or an API key without company:write"
          }
        },
        "summary": "Update a custom field definition — label/options/required/order only",
        "tags": [
          "custom-fields"
        ]
      },
      "delete": {
        "description": "Never a hard delete — an already-recorded value for this field, on any document or client, keeps rendering forever (see persistence.ts's own header). Disappears from the create/edit surface only.",
        "operationId": "CompanyCustomFieldsController_archive",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Custom field definition archived"
          },
          "403": {
            "description": "Not an OWNER/ADMIN, or an API key without company:write"
          }
        },
        "summary": "Archive (soft-delete) a custom field definition",
        "tags": [
          "custom-fields"
        ]
      }
    },
    "/api/custom-fields/{id}/restore": {
      "post": {
        "operationId": "CompanyCustomFieldsController_restore",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Custom field definition restored"
          },
          "403": {
            "description": "Not an OWNER/ADMIN, or an API key without company:write"
          }
        },
        "summary": "Un-archive a previously archived custom field definition",
        "tags": [
          "custom-fields"
        ]
      }
    },
    "/api/mcp": {
      "post": {
        "operationId": "McpController_handleMcp",
        "parameters": [],
        "responses": {
          "201": {
            "description": ""
          }
        },
        "tags": [
          "Mcp"
        ]
      }
    },
    "/api/invitations/can-register": {
      "get": {
        "description": "Checks whether a given invitation code is valid and allows registration (public).",
        "operationId": "InvitationsController_canRegister",
        "parameters": [
          {
            "name": "code",
            "required": false,
            "in": "query",
            "description": "Invitation code to validate",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Registration status retrieved"
          }
        },
        "summary": "Check if registration is allowed",
        "tags": [
          "invitations"
        ]
      }
    },
    "/api/invitations/is-first-user": {
      "get": {
        "description": "Returns whether any user has been registered yet (public).",
        "operationId": "InvitationsController_isFirstUser",
        "parameters": [],
        "responses": {
          "200": {
            "description": "First user status retrieved"
          }
        },
        "summary": "Check if this is the first user",
        "tags": [
          "invitations"
        ]
      }
    },
    "/api/invitations/validate": {
      "post": {
        "description": "Validates an invitation code and email pair, storing the code for the registration flow (public).",
        "operationId": "InvitationsController_validateInvitation",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Invitation code validated"
          }
        },
        "summary": "Validate an invitation code",
        "tags": [
          "invitations"
        ]
      }
    },
    "/api/invitations": {
      "post": {
        "description": "Generates a new invitation link/code for inviting a user to the active company with a given role.",
        "operationId": "InvitationsController_createInvitation",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Invitation created"
          }
        },
        "summary": "Create an invitation",
        "tags": [
          "invitations"
        ]
      },
      "get": {
        "description": "Returns all pending invitations for the active company.",
        "operationId": "InvitationsController_listInvitations",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Invitations retrieved"
          }
        },
        "summary": "List invitations",
        "tags": [
          "invitations"
        ]
      }
    },
    "/api/invitations/{id}": {
      "delete": {
        "description": "Revokes a pending invitation by its ID.",
        "operationId": "InvitationsController_deleteInvitation",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Invitation ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invitation deleted"
          }
        },
        "summary": "Delete an invitation",
        "tags": [
          "invitations"
        ]
      }
    },
    "/api/companies/transfer": {
      "post": {
        "description": "OWNER-only. Requires an OTP from `POST /danger/otp` (the same challenge the danger zone uses). Always returns the same generic message whether or not `email` has an account — see this endpoint's own anti-enumeration discipline in `transfer.service.ts`.",
        "operationId": "TransferController_initiate",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "otp"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "The destination account email"
                  },
                  "otp": {
                    "type": "string",
                    "description": "One-time passcode sent via POST /danger/otp"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Request processed (generic message)"
          }
        },
        "summary": "Initiate an ownership transfer",
        "tags": [
          "company-transfer"
        ]
      },
      "get": {
        "description": "OWNER-only. `null` when none is pending.",
        "operationId": "TransferController_current",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Current transfer (or null) retrieved"
          }
        },
        "summary": "The active company's current pending transfer",
        "tags": [
          "company-transfer"
        ]
      }
    },
    "/api/companies/transfer/{id}": {
      "delete": {
        "description": "OWNER-only. No OTP required — a lower-stakes, reversible action.",
        "operationId": "TransferController_cancel",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transfer canceled"
          }
        },
        "summary": "Cancel a pending ownership transfer",
        "tags": [
          "company-transfer"
        ]
      }
    },
    "/api/account/transfers": {
      "get": {
        "description": "Every status, newest first — the pending one(s) are what the acceptance screen acts on.",
        "operationId": "AccountTransfersController_mine",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Transfers retrieved"
          }
        },
        "summary": "List ownership transfers addressed to my account",
        "tags": [
          "company-transfer"
        ]
      }
    },
    "/api/account/transfers/{id}/accept": {
      "post": {
        "description": "Must be signed in as the transfer's own recipient. Makes the caller OWNER of the company and the initiating OWNER an admin.",
        "operationId": "AccountTransfersController_accept",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Transfer accepted"
          },
          "404": {
            "description": "No such transfer addressed to this account"
          },
          "409": {
            "description": "No longer pending"
          },
          "410": {
            "description": "Expired"
          }
        },
        "summary": "Accept an ownership transfer",
        "tags": [
          "company-transfer"
        ]
      }
    },
    "/api/legal/documents": {
      "get": {
        "description": "Public, always reachable (even on a self-hosted instance — this route never 404s the way BillingModule does). In SaaS mode: Terms of Service, Privacy Policy, Data Processing Agreement, Refund and Cancellation Policy, Legal Notice, Cookies & Acceptable Use, and International Access Transparency, each with its current version, effective date, and raw markdown content resolved into a language (`legal-request-language.ts`): the `lang` query param when given and supported, else the caller's own account locale when signed in, else this request's own `Accept-Language` header, else English — resolved separately per document, since not every document ships every language. Outside SaaS mode `documents` is always the empty array: every one of the seven describes the hosted offering by name (the author's own identity, a subscription, a processor relationship) and none of it is true of a self-hosted operator's instance — only the licence in the repository governs that install, and this route has never served it. `saasMode` is the one field the frontend actually branches on: whether the sign-up screen must show the acceptance checkbox, and whether there is anything here worth linking to at all.",
        "operationId": "LegalController_documents",
        "parameters": [
          {
            "name": "lang",
            "required": false,
            "in": "query",
            "description": "Explicit language override (e.g. the frontend's own per-document language selector) — outranks the account locale and `Accept-Language`. Silently ignored if not one of the languages this catalog carries at all; a document with no translation into it still falls back to English.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Documents retrieved"
          }
        },
        "summary": "List the legal documents",
        "tags": [
          "legal"
        ]
      }
    },
    "/api/legal/status": {
      "get": {
        "description": "Authenticated. Always `{ requiresAcceptance: false, pending: [] }` outside SaaS mode. Drives the sign-in re-acceptance interstitial (`pages/legal/accept.tsx`) whenever a required document has a newer version than what this user last accepted.",
        "operationId": "LegalController_status",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Status retrieved"
          }
        },
        "summary": "The current user's legal-acceptance status",
        "tags": [
          "legal"
        ]
      }
    },
    "/api/legal/accept": {
      "post": {
        "description": "Authenticated. Body `{ slugs?: string[] }` — omit it to accept whatever is currently pending (the re-acceptance interstitial's own call); an explicit list is filtered down to the documents that actually require acceptance (terms-of-service, privacy-policy). A no-op (`{ accepted: [] }`) outside SaaS mode.",
        "operationId": "LegalController_accept",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Acceptance recorded"
          },
          "400": {
            "description": "slugs was present but not an array of strings"
          }
        },
        "summary": "Accept one or more legal documents",
        "tags": [
          "legal"
        ]
      }
    },
    "/api/health": {
      "get": {
        "description": "Returns 200 when the database is reachable, 503 otherwise (public).",
        "operationId": "HealthController_check",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Service healthy"
          },
          "503": {
            "description": "Database unreachable"
          }
        },
        "summary": "Health check",
        "tags": [
          "health"
        ]
      }
    },
    "/api/version": {
      "get": {
        "description": "The version this instance was built from, and whether a newer one has been published on GitHub. Cached server-side (see VersionService) — never a live GitHub call on every request, and never an error response: an unreachable GitHub simply reports no update available.",
        "operationId": "VersionController_get",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Version info"
          }
        },
        "summary": "Installed version and update availability",
        "tags": [
          "version"
        ]
      }
    },
    "/api/logs": {
      "get": {
        "description": "Server-sent event stream that pushes application logs in real-time, optionally filtered by category, level, or user ID.",
        "operationId": "LoggerController_streamLogs",
        "parameters": [
          {
            "name": "category",
            "required": false,
            "in": "query",
            "description": "Filter logs by category",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "level",
            "required": false,
            "in": "query",
            "description": "Filter logs by level (e.g. info, error)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "required": false,
            "in": "query",
            "description": "Filter logs by the user ID that triggered them",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "intervalMs",
            "required": false,
            "in": "query",
            "description": "Polling interval in milliseconds for the log stream. Defaults to 1000.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "summary": "Stream application logs",
        "tags": [
          "logs"
        ]
      }
    }
  },
  "info": {
    "title": "Invoicerr API",
    "description": "Authenticate with an API key (Settings > API Keys) via the Authorization: Bearer header or the X-Api-Key header.",
    "version": "0.0.1",
    "contact": {}
  },
  "tags": [],
  "servers": [],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "scheme": "bearer",
        "bearerFormat": "API key",
        "type": "http"
      }
    },
    "schemas": {
      "CreateApiKeyDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Human-readable label for the key, e.g. \"CLI on my laptop\""
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "quotes:write",
                "invoices:write",
                "clients:write",
                "articles:write",
                "articles:read",
                "quotes:read",
                "invoices:read",
                "clients:read",
                "credit-notes:write",
                "credit-notes:read",
                "expenses:write",
                "expenses:read",
                "received-invoices:write",
                "received-invoices:read",
                "company:read",
                "company:write",
                "api-keys:read",
                "api-keys:write",
                "webhooks:read",
                "webhooks:write",
                "billing:read",
                "billing:write",
                "time-tracking:read",
                "time-tracking:write"
              ]
            },
            "description": "Permission scopes granted to this key. Omit or leave empty for a key with no document-creation access."
          }
        },
        "required": [
          "name"
        ]
      },
      "EditCompanyDto": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "foundedAt": {
            "format": "date-time",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "currency": {
            "enum": [
              "JPY",
              "KRW",
              "KWD",
              "BHD",
              "OMR",
              "TND",
              "AED",
              "AFN",
              "ALL",
              "AMD",
              "ANG",
              "AOA",
              "ARS",
              "AUD",
              "AWG",
              "AZN",
              "BAM",
              "BBD",
              "BDT",
              "BGN",
              "BIF",
              "BMD",
              "BND",
              "BOB",
              "BRL",
              "BSD",
              "BTC",
              "BTN",
              "BWP",
              "BYR",
              "BZD",
              "CAD",
              "CDF",
              "CHF",
              "CLF",
              "CLP",
              "CNY",
              "COP",
              "CRC",
              "CUC",
              "CUP",
              "CVE",
              "CZK",
              "DJF",
              "DKK",
              "DOP",
              "DZD",
              "EEK",
              "EGP",
              "ERN",
              "ETB",
              "EUR",
              "FJD",
              "FKP",
              "GBP",
              "GEL",
              "GGP",
              "GHS",
              "GIP",
              "GMD",
              "GNF",
              "GTQ",
              "GYD",
              "HKD",
              "HNL",
              "HRK",
              "HTG",
              "HUF",
              "IDR",
              "ILS",
              "IMP",
              "INR",
              "IQD",
              "IRR",
              "ISK",
              "JEP",
              "JMD",
              "JOD",
              "KES",
              "KGS",
              "KHR",
              "KMF",
              "KPW",
              "KYD",
              "KZT",
              "LAK",
              "LBP",
              "LKR",
              "LRD",
              "LSL",
              "LTL",
              "LVL",
              "LYD",
              "MAD",
              "MDL",
              "MGA",
              "MKD",
              "MMK",
              "MNT",
              "MOP",
              "MRO",
              "MTL",
              "MUR",
              "MVR",
              "MWK",
              "MXN",
              "MYR",
              "MZN",
              "NAD",
              "NGN",
              "NIO",
              "NOK",
              "NPR",
              "NZD",
              "PAB",
              "PEN",
              "PGK",
              "PHP",
              "PKR",
              "PLN",
              "PYG",
              "QAR",
              "RON",
              "RSD",
              "RUB",
              "RWF",
              "SAR",
              "SBD",
              "SCR",
              "SDG",
              "SEK",
              "SGD",
              "SHP",
              "SLL",
              "SOS",
              "SRD",
              "STD",
              "SVC",
              "SYP",
              "SZL",
              "THB",
              "TJS",
              "TMT",
              "TOP",
              "TRY",
              "TTD",
              "TWD",
              "TZS",
              "UAH",
              "UGX",
              "USD",
              "UYU",
              "UZS",
              "VEF",
              "VND",
              "VUV",
              "WST",
              "XAF",
              "XAG",
              "XAU",
              "XCD",
              "XDR",
              "XOF",
              "XPD",
              "XPF",
              "XPT",
              "YER",
              "ZAR",
              "ZMK",
              "ZMW",
              "ZWL"
            ],
            "type": "string"
          },
          "exemptVat": {
            "type": "boolean"
          },
          "distanceSalesRegime": {
            "type": "string",
            "nullable": true
          },
          "address": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "language": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "iban": {
            "type": "string",
            "nullable": true
          },
          "identifiers": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "invoiceTransportId": {
            "type": "string",
            "nullable": true
          },
          "paymentProviderId": {
            "type": "string",
            "nullable": true
          },
          "referenceCurrency": {
            "type": "string",
            "nullable": true
          },
          "approvalThresholdMinor": {
            "type": "number",
            "nullable": true
          },
          "remindersEnabled": {
            "type": "boolean"
          }
        },
        "required": [
          "name",
          "currency",
          "country"
        ]
      },
      "EditClientsDto": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "foundedAt": {
            "format": "date-time",
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "contactFirstname": {
            "type": "string"
          },
          "contactLastname": {
            "type": "string"
          },
          "contactEmail": {
            "type": "string"
          },
          "contactPhone": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "language": {
            "type": "string",
            "nullable": true
          },
          "currency": {
            "enum": [
              "JPY",
              "KRW",
              "KWD",
              "BHD",
              "OMR",
              "TND",
              "AED",
              "AFN",
              "ALL",
              "AMD",
              "ANG",
              "AOA",
              "ARS",
              "AUD",
              "AWG",
              "AZN",
              "BAM",
              "BBD",
              "BDT",
              "BGN",
              "BIF",
              "BMD",
              "BND",
              "BOB",
              "BRL",
              "BSD",
              "BTC",
              "BTN",
              "BWP",
              "BYR",
              "BZD",
              "CAD",
              "CDF",
              "CHF",
              "CLF",
              "CLP",
              "CNY",
              "COP",
              "CRC",
              "CUC",
              "CUP",
              "CVE",
              "CZK",
              "DJF",
              "DKK",
              "DOP",
              "DZD",
              "EEK",
              "EGP",
              "ERN",
              "ETB",
              "EUR",
              "FJD",
              "FKP",
              "GBP",
              "GEL",
              "GGP",
              "GHS",
              "GIP",
              "GMD",
              "GNF",
              "GTQ",
              "GYD",
              "HKD",
              "HNL",
              "HRK",
              "HTG",
              "HUF",
              "IDR",
              "ILS",
              "IMP",
              "INR",
              "IQD",
              "IRR",
              "ISK",
              "JEP",
              "JMD",
              "JOD",
              "KES",
              "KGS",
              "KHR",
              "KMF",
              "KPW",
              "KYD",
              "KZT",
              "LAK",
              "LBP",
              "LKR",
              "LRD",
              "LSL",
              "LTL",
              "LVL",
              "LYD",
              "MAD",
              "MDL",
              "MGA",
              "MKD",
              "MMK",
              "MNT",
              "MOP",
              "MRO",
              "MTL",
              "MUR",
              "MVR",
              "MWK",
              "MXN",
              "MYR",
              "MZN",
              "NAD",
              "NGN",
              "NIO",
              "NOK",
              "NPR",
              "NZD",
              "PAB",
              "PEN",
              "PGK",
              "PHP",
              "PKR",
              "PLN",
              "PYG",
              "QAR",
              "RON",
              "RSD",
              "RUB",
              "RWF",
              "SAR",
              "SBD",
              "SCR",
              "SDG",
              "SEK",
              "SGD",
              "SHP",
              "SLL",
              "SOS",
              "SRD",
              "STD",
              "SVC",
              "SYP",
              "SZL",
              "THB",
              "TJS",
              "TMT",
              "TOP",
              "TRY",
              "TTD",
              "TWD",
              "TZS",
              "UAH",
              "UGX",
              "USD",
              "UYU",
              "UZS",
              "VEF",
              "VND",
              "VUV",
              "WST",
              "XAF",
              "XAG",
              "XAU",
              "XCD",
              "XDR",
              "XOF",
              "XPD",
              "XPF",
              "XPT",
              "YER",
              "ZAR",
              "ZMK",
              "ZMW",
              "ZWL"
            ],
            "type": "string"
          },
          "type": {
            "enum": [
              "INDIVIDUAL",
              "COMPANY"
            ],
            "type": "string"
          },
          "kind": {
            "enum": [
              "BUSINESS",
              "GOVERNMENT"
            ],
            "type": "string"
          },
          "isSupplier": {
            "type": "boolean"
          },
          "isActive": {
            "type": "boolean"
          },
          "identifiers": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "customFields": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "id",
          "name",
          "address",
          "postalCode",
          "city",
          "country",
          "currency",
          "isActive"
        ]
      },
      "ProviderCapabilityDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "coverage": {
            "enum": [
              "REGISTER",
              "PARTIAL"
            ],
            "type": "string",
            "description": "REGISTER = the official register · PARTIAL = a worldwide directory (LEI, Peppol)"
          },
          "schemes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "LEGAL_ID",
                "VAT"
              ]
            }
          },
          "identifierLabel": {
            "type": "string"
          },
          "docsUrl": {
            "type": "string"
          },
          "requiresCredentials": {
            "type": "boolean"
          },
          "credentialEnvVars": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "configured": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "label",
          "coverage",
          "schemes",
          "identifierLabel",
          "requiresCredentials",
          "configured"
        ]
      },
      "CountryLookupCapabilityDto": {
        "type": "object",
        "properties": {
          "countryCode": {
            "type": "string"
          },
          "status": {
            "enum": [
              "AVAILABLE",
              "NEEDS_CREDENTIALS",
              "UNAVAILABLE"
            ],
            "type": "string"
          },
          "coverage": {
            "enum": [
              "REGISTER",
              "PARTIAL"
            ],
            "type": "string"
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProviderCapabilityDto"
            }
          },
          "schemes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "LEGAL_ID",
                "VAT"
              ]
            }
          },
          "identifierLabel": {
            "type": "string"
          },
          "note": {
            "type": "string"
          }
        },
        "required": [
          "countryCode",
          "status",
          "coverage",
          "providers",
          "schemes"
        ]
      },
      "CompanyLookupCompanyDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "legalName": {
            "type": "string"
          },
          "legalId": {
            "type": "string",
            "description": "National registration number (SIRET, IČO, NIP, CVR, CNPJ…)"
          },
          "legalIdScheme": {
            "type": "string",
            "description": "The national scheme legalId belongs to, e.g. 'SIRET'"
          },
          "VAT": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "foundedAt": {
            "format": "date-time",
            "type": "string"
          },
          "status": {
            "enum": [
              "ACTIVE",
              "INACTIVE",
              "UNKNOWN"
            ],
            "type": "string"
          },
          "vatRegistered": {
            "type": "boolean",
            "nullable": true
          }
        },
        "required": [
          "name"
        ]
      },
      "CompanyLookupResultDto": {
        "type": "object",
        "properties": {
          "found": {
            "type": "boolean"
          },
          "company": {
            "nullable": true,
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/CompanyLookupCompanyDto"
              }
            ]
          },
          "source": {
            "type": "string",
            "description": "Provider id that answered, e.g. fr-recherche-entreprises"
          },
          "sourceLabel": {
            "type": "string",
            "description": "Contributing providers, joined — e.g. 'VIES + GLEIF'"
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "error": {
            "type": "string",
            "enum": [
              "UNSUPPORTED_COUNTRY",
              "NOT_CONFIGURED",
              "INVALID_IDENTIFIER",
              "PROVIDER_ERROR"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "found",
          "company"
        ]
      }
    }
  }
}
