openapi: 3.1.0
info:
  title: Melso API
  version: 1.0.0-preview
  summary: Spanish tax and labour compliance for the companies on your platform.
  description: |
    The Melso API lets an HR, payroll, banking or ERP platform run the
    Spanish tax and labour obligations of the companies it serves: invoices,
    books, tax returns, Social Security and employment notifications.

    Melso calculates and prepares. A person at the company approves the exact
    version. Melso's professionals file under the company's representation, and
    the filing is proven only by the administration's reference and receipt.

    Every operation states its availability in `x-melso-status`:

    - `live`: Melso runs this capability in production today. Partner keys
      reach it once a partner account is enabled.
    - `preview`: a contract proposed to design partners. Not callable yet.

    `x-melso-actor: person` marks a decision a signed-in person makes in
    Melso; a partner sends them the approval link instead.
  contact:
    name: Melso partnerships
    email: hello@melso.ai
    url: https://docs.melso.ai/api/overview
servers:
  - url: https://api.melso.ai/v1
    description: API. Keys and access are issued per partner.
security:
  - partnerKey: []
tags:
  - name: Companies
    description: The companies you operate for, their fiscal profile and hosted onboarding.
  - name: Mandates
    description: The company's representation of Melso before the AEAT, the TGSS and the SEPE.
  - name: Webhook endpoints
    description: Where Melso sends signed events about your companies.
  - name: Invoicing setup
    description: Registration of the company as a VERI*FACTU or TicketBAI invoice issuer.
  - name: Customers
    description: The company's invoice customers.
  - name: Invoices
    description: Drafts, approval, legal registration, delivery and payment of the company's invoices.
  - name: Expenses
    description: Purchase invoices and receipts, with their AEAT category and deductibility.
  - name: Income
    description: Income invoiced outside Melso.
  - name: Documents
    description: Files the company gives Melso, and Melso's structured reading of an invoice or receipt.
  - name: Banking
    description: Bank accounts, movements, statements and reconciliation with invoices and expenses.
  - name: Accounting
    description: Journal, imported books and the company's financial statements.
  - name: Tax calendar
    description: The forms the company must file, with their periods and deadlines.
  - name: Tax returns
    description: The returns Melso calculates, their approval and their filing.
  - name: Corporate tax
    description: The year's corporate tax (modelo 200) and its instalments (modelo 202).
  - name: Tax estimates
    description: What the company should set aside for taxes, as of today.
  - name: Cases
    description: Gestiones, the procedures Melso's professionals carry out for the company.
  - name: Administration notifications
    description: Electronic notifications from the administration to the company.
  - name: Certificates
    description: Official certificates such as being up to date with the AEAT or the Social Security.
  - name: Employees
    description: The people the company employs.
  - name: Payroll runs
    description: The payroll results your platform calculated, for Melso to settle and declare.
  - name: Social Security
    description: Affiliation (altas, bajas and variaciones) and monthly contribution settlements.
  - name: Employment notifications
    description: Contrat@ contract notifications and Certific@2 company certificates to the SEPE.
x-tagGroups:
  - name: Companies and access
    tags: [Companies, Mandates, Webhook endpoints]
  - name: Invoicing
    tags: [Invoicing setup, Customers, Invoices]
  - name: Bookkeeping
    tags: [Expenses, Income, Documents, Banking, Accounting]
  - name: Taxes
    tags: [Tax calendar, Tax returns, Corporate tax, Tax estimates, Cases, Administration notifications, Certificates]
  - name: Payroll and Social Security
    tags: [Employees, Payroll runs, Social Security, Employment notifications]

paths:
  # ───────────────────────────── Companies ─────────────────────────────
  /companies:
    get:
      operationId: listCompanies
      summary: List companies
      description: Lists the companies on your partner account, newest first.
      tags: [Companies]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: external_id
          in: query
          description: Only the company with this ID of yours.
          schema: {type: string}
      responses:
        '200':
          description: Companies.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Company'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
    post:
      operationId: createCompany
      summary: Create a company
      description: |
        Adds an autónomo (`freelancer`) or an empresa (`company`) to your partner
        account, with its owner and as much of its fiscal profile as you know.
        The owner is the person who approves invoices and returns.

        Send `setup_request` when the company is not registered yet, with
        `setup_details` as the matching case kind describes them: Melso opens
        the setup case, `alta` to register the autónomo (036 and RETA) or
        `constitucion_sl` to incorporate an S.L., and returns its ID in
        `setup.case_id`. The case is sent to Melso's professionals once its
        details are complete and paid for; a hosted onboarding link with the
        `setup` step collects what is missing from the owner. You receive
        `company.registered` when the company has its tax ID. See
        [Register an autónomo](/api/guides/register-autonomo) and
        [Incorporate an S.L.](/api/guides/incorporate-sl).
      tags: [Companies]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CompanyCreate'}
            examples:
              existing:
                summary: A registered empresa
                value:
                  kind: company
                  name: Talleres Ebro
                  external_id: cus_48213
                  owner:
                    name: Lucía Martín Pérez
                    email: lucia@example.com
                    locale: es
                  fiscal_profile:
                    tax_id: B12345674
                    legal_name: Talleres Ebro S.L.
                    address: Calle Mayor 1
                    postal_code: '50001'
                    city: Zaragoza
                    activity_description: Reparación de vehículos
                    vat_regime: general
                  services: [invoicing, tax_returns, payroll]
              register_autonomo:
                summary: An autónomo to register
                value:
                  kind: freelancer
                  name: Marta Gil Romero
                  external_id: cus_51907
                  owner:
                    name: Marta Gil Romero
                    email: marta@example.com
                    phone: '+34600000000'
                    locale: es
                  fiscal_profile:
                    tax_id: 12345678Z
                    legal_name: Marta Gil Romero
                    address: Calle de la Paz 4, 2.º B
                    postal_code: '46003'
                    city: València
                    activity_type: profesional
                    activity_description: Diseño gráfico para empresas
                  setup_request: register_autonomo
                  setup_details:
                    start_date: '2026-12-01'
                    activity: Diseño gráfico para empresas
                    birth_date: '1994-05-17'
                    nuss: '281234567840'
                    clients: Empresas en España y algún cliente de la UE
                    tarifa_plana: si
                    iban: ES9121000418450200051332
                  services: [invoicing, tax_returns]
              incorporate_sl:
                summary: An S.L. to incorporate
                value:
                  kind: company
                  name: Estudio Norte
                  external_id: cus_52011
                  owner:
                    name: Pablo Ruiz Ortega
                    email: pablo@example.com
                    phone: '+34600000001'
                    locale: es
                  setup_request: incorporate_sl
                  setup_details:
                    name_ending: sl
                    names: [Estudio Norte, Norte Diseño, Estudio Norte Creativo]
                    activity: Diseño gráfico y comunicación visual para empresas
                    capital: 3000
                    share_value: 1
                    partners:
                      - {type: person, name: Pablo Ruiz Ortega, tax_id: 12345678Z, share_percent: 60, administrator: true, has_certificate: true}
                      - {type: person, name: Ana Vidal Soler, tax_id: 87654321X, share_percent: 40, administrator: false, has_certificate: false}
                    registered_office:
                      street_type: Calle
                      street: Alcalá
                      number: '120'
                      postal_code: '28009'
                      municipality: Madrid
                      province: Madrid
                  services: [invoicing, bookkeeping, tax_returns]
      responses:
        '201':
          description: The company. `missing` lists what its fiscal profile still needs.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Company'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: getCompany
      summary: Get a company
      description: Returns the company, its kind and whether its setup is finished.
      tags: [Companies]
      x-melso-status: live
      responses:
        '200':
          description: The company.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Company'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/fiscal-profile:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: getFiscalProfile
      summary: Get the fiscal profile
      description: |
        Returns the company's tax profile, and what is still missing to invoice
        (`invoice_missing`) and to calculate its returns (`tax_missing`). The
        profile decides which forms appear in the tax calendar.
      tags: [Companies]
      x-melso-status: live
      responses:
        '200':
          description: The profile.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/FiscalProfileResult'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      operationId: updateFiscalProfile
      summary: Update the fiscal profile
      description: |
        Saves the fields you send. A field that fails validation is reported in
        `errors` and the valid fields are still saved, so the response is `200`
        either way: read `errors`. A postal code fills in the province and the
        tax territory (`deduced`).
      tags: [Companies]
      x-melso-status: live
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/FiscalProfileInput'}
            example:
              tax_id: 12345678Z
              legal_name: Lucía Martín Pérez
              address: Calle Mayor 1
              postal_code: '50001'
              activity_type: profesional
              iae_code: '844'
              registration_status: alta
              registered_on: 2024-03
              irpf_method: directa_simplificada
              vat_regime: general
              client_types: [es_business]
      responses:
        '200':
          description: The whole profile, with `errors` for any field not saved.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/FiscalProfileResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/onboarding-links:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: createOnboardingLink
      summary: Create an onboarding link
      description: |
        Returns a single-use link to Melso's hosted onboarding, with your brand,
        where the company's owner completes the fiscal profile, signs the
        mandates and authorizes invoice registration. For a company created
        with `setup_request`, the `setup` step collects the registration or
        incorporation details still missing and sends the setup case. Send the
        owner there instead of collecting tax details yourself. The link expires
        after 24 hours.
      tags: [Companies]
      x-melso-status: preview
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [return_url]
              additionalProperties: false
              properties:
                steps:
                  type: array
                  description: What to ask for. Defaults to everything the company still needs.
                  items:
                    type: string
                    enum: [setup, fiscal_profile, mandates, invoicing_authorization]
                return_url: {type: string, format: uri}
                locale:
                  type: string
                  enum: [es, ca, en]
                  default: es
      responses:
        '201':
          description: The link.
          content:
            application/json:
              schema:
                type: object
                required: [url, expires_at]
                properties:
                  url: {type: string, format: uri}
                  expires_at: {type: string, format: date-time}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

  # ───────────────────────────── Mandates ──────────────────────────────
  /companies/{company_id}/mandates:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listMandates
      summary: List mandates
      description: Lists the company's mandates to Melso, newest first.
      tags: [Mandates]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The company's mandates.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MandateList'
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: createMandate
      summary: Request a mandate
      description: |
        Asks the company to grant Melso representation before one
        administration for the procedures listed. Returns a hosted `signing_url`
        where the company's legal representative reviews and signs the mandate.
        A mandate is `active` once the administration has registered it; until
        then Melso does not act before that administration.
      tags: [Mandates]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MandateCreate'
            example:
              administration: aeat
              procedures: [tax_returns, notifications, certificates]
              signer:
                name: Lucía Martín Pérez
                tax_id: 12345678Z
                email: lucia@example.com
              return_url: https://partner.example.com/melso/mandate-done
      responses:
        '201':
          description: The mandate, waiting for signature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Mandate'
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/mandates/{mandate_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: mandate_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getMandate
      summary: Get a mandate
      description: Returns one mandate with its signature and registration state.
      tags: [Mandates]
      x-melso-status: preview
      responses:
        '200':
          description: The mandate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Mandate'
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/mandates/{mandate_id}/revoke:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: mandate_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: revokeMandate
      summary: Revoke a mandate
      description: |
        Ends Melso's representation for this mandate. Work already approved
        before the revocation is completed or handed back, as the case says;
        nothing new is filed under it.
      tags: [Mandates]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The revoked mandate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Mandate'
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Webhook endpoints ─────────────────────
  /webhook-endpoints:
    get:
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      description: Lists your webhook endpoints. Endpoints belong to your partner account and receive events for every company you operate for.
      tags: [Webhook endpoints]
      x-melso-status: preview
      responses:
        '200':
          description: Your endpoints.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/WebhookEndpoint'}
        '401': {$ref: '#/components/responses/Unauthorized'}
    post:
      operationId: createWebhookEndpoint
      summary: Create a webhook endpoint
      description: |
        Registers an HTTPS URL for the event types you choose. The response to
        the first endpoint you create carries your `signing_secret`; it is shown
        once. See [Webhooks](/api/webhooks) for signature verification.
      tags: [Webhook endpoints]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointInput'
            example:
              url: https://partner.example.com/webhooks/melso
              description: Production listener
              event_types: [tax_return.calculated, tax_return.filed, invoice.registered]
      responses:
        '201':
          description: The endpoint, and the signing secret when this is your first endpoint.
          content:
            application/json:
              schema:
                type: object
                required: [endpoint]
                properties:
                  endpoint: {$ref: '#/components/schemas/WebhookEndpoint'}
                  signing_secret:
                    type: string
                    description: '`whsec_` followed by 64 hex characters. Returned once.'
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
  /webhook-endpoints/{endpoint_id}:
    parameters:
      - name: endpoint_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    put:
      operationId: updateWebhookEndpoint
      summary: Replace a webhook endpoint
      description: Replaces the endpoint's URL, description, event types and enabled flag.
      tags: [Webhook endpoints]
      x-melso-status: preview
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointInput'
      responses:
        '200':
          description: The updated endpoint.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/WebhookEndpoint'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    delete:
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint
      description: Deletes the endpoint. Pending deliveries to it stop.
      tags: [Webhook endpoints]
      x-melso-status: preview
      responses:
        '204':
          description: Deleted.
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /webhook-endpoints/signing-secret/rotate:
    post:
      operationId: rotateWebhookSigningSecret
      summary: Rotate the signing secret
      description: Replaces your signing secret immediately. Deliveries from then on are signed with the new secret.
      tags: [Webhook endpoints]
      x-melso-status: preview
      responses:
        '200':
          description: The new secret. Returned once.
          content:
            application/json:
              schema:
                type: object
                required: [signing_secret]
                properties:
                  signing_secret: {type: string}
        '401': {$ref: '#/components/responses/Unauthorized'}
  /webhook-deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: List webhook deliveries
      description: Lists your most recent deliveries, newest first, with their status, attempts and the last response.
      tags: [Webhook endpoints]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          schema: {$ref: '#/components/schemas/WebhookDeliveryStatus'}
      responses:
        '200':
          description: Deliveries.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/WebhookDelivery'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
  /webhook-deliveries/{delivery_id}/resend:
    parameters:
      - name: delivery_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: resendWebhookDelivery
      summary: Resend a delivery
      description: Queues the delivery again and restarts its 24-hour retry window.
      tags: [Webhook endpoints]
      x-melso-status: preview
      responses:
        '202':
          description: Queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {type: string, const: pending}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Invoicing setup ───────────────────────
  /companies/{company_id}/invoicing:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: getInvoicingStatus
      summary: Get the invoicing status
      description: |
        Whether the company can issue invoices: its registration as an issuer
        with VERI*FACTU or TicketBAI, and what its fiscal profile still lacks.
        While the owner's authorization is pending, `authorization_url` is the
        page where they give it.
      tags: [Invoicing setup]
      x-melso-status: live
      responses:
        '200':
          description: The status.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/InvoicingStatus'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/invoicing/registration:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: registerInvoicingIssuer
      summary: Register the company as an issuer
      description: |
        Registers the company as an invoice issuer on its network: VERI*FACTU in
        the common territory and the Canary Islands, TicketBAI in Álava, Bizkaia
        and Gipuzkoa. Retrying resumes the same registration. The result is
        `active`, or `pending` with the `authorization_url` where the owner
        authorizes the representation for invoice registration.
      tags: [Invoicing setup]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                ticketbai_activity_code:
                  type: string
                  description: Bizkaia only, the activity code of its LROE.
      responses:
        '200':
          description: The status after registering.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/InvoicingStatus'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
        '502': {$ref: '#/components/responses/ProviderPending'}

  # ───────────────────────────── Customers ─────────────────────────────
  /companies/{company_id}/customers:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listCustomers
      summary: List customers
      description: Lists the company's invoice customers, newest first.
      tags: [Customers]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Customers.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Customer'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: upsertCustomer
      summary: Create or update a customer
      description: |
        Creates a customer, or updates the one whose `id` you send. Updating a
        customer never changes an invoice already issued to it: an invoice keeps
        the customer's name and tax ID as they were when it was drafted.
      tags: [Customers]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CustomerInput'}
            example:
              name: Talleres Ebro S.L.
              kind: company
              tax_id: B12345674
              country: ES
              address: Calle Mayor 1, 50001 Zaragoza
              email: facturas@talleresebro.example
      responses:
        '200':
          description: The updated customer.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Customer'}
        '201':
          description: The created customer.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Customer'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Invoices ──────────────────────────────
  /companies/{company_id}/invoices:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listInvoices
      summary: List invoices
      description: Lists the company's invoices in every status, drafts included, newest issue date first.
      tags: [Invoices]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          schema: {$ref: '#/components/schemas/InvoiceStatus'}
      responses:
        '200':
          description: Invoices.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Invoice'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: createInvoice
      summary: Create an invoice draft
      description: |
        Saves an unnumbered draft and calculates its totals. A draft has no
        series or number and is not registered anywhere until a person issues
        it. Use [Create an invoice approval link](/api/reference/invoices/create-invoice-approval-link)
        to get the page where the owner issues it.
      tags: [Invoices]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/InvoiceDraftInput'}
            example:
              customer_id: 3f6c2a1e-8b7d-4c5e-9f10-2a3b4c5d6e7f
              issue_date: '2026-10-15'
              due_date: '2026-11-14'
              irpf_rate: '15'
              lines:
                - description: Diseño de identidad visual
                  quantity: '1'
                  unit_price: '1200.00'
                  vat_rate: '21'
                  product_kind: services
      responses:
        '201':
          description: The draft, with `status` `draft` and its calculated totals.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Invoice'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/invoices/{invoice_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/InvoiceId'
    get:
      operationId: getInvoice
      summary: Get an invoice
      description: Returns one invoice with its lines, totals, registration and delivery state.
      tags: [Invoices]
      x-melso-status: live
      responses:
        '200':
          description: The invoice.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Invoice'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      operationId: replaceInvoiceDraft
      summary: Replace an invoice draft
      description: |
        Replaces a draft with the body you send: send every field again, as on
        create. An issued invoice cannot change; correct it instead. Each change
        gives the draft a new `version`, so an approval of an earlier version no
        longer applies.
      tags: [Invoices]
      x-melso-status: live
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/InvoiceDraftInput'}
      responses:
        '200':
          description: The replaced draft.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Invoice'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/invoices/{invoice_id}/pdf:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/InvoiceId'
    get:
      operationId: getInvoicePdf
      summary: Download the invoice PDF
      description: Returns the legal PDF of a registered invoice. It exists once `pdf_ready` is `true`.
      tags: [Invoices]
      x-melso-status: live
      responses:
        '200':
          description: The PDF.
          content:
            application/pdf:
              schema: {type: string, format: binary}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/invoices/{invoice_id}/approval-link:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/InvoiceId'
    post:
      operationId: createInvoiceApprovalLink
      summary: Create an invoice approval link
      description: |
        Returns the page where the company's owner reviews the exact saved draft
        and issues it. Issuing assigns the series and number and registers the
        invoice with VERI*FACTU or TicketBAI. The link does not issue anything by
        itself.
      tags: [Invoices]
      x-melso-status: live
      responses:
        '200':
          description: The approval page for the draft's current version.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ApprovalLink'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/invoices/{invoice_id}/issue:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/InvoiceId'
    post:
      operationId: issueInvoice
      summary: Issue an invoice
      description: |
        Issues the draft at exactly `expected_version`: assigns the series and the
        next number, registers the invoice with VERI*FACTU (AEAT) or TicketBAI
        (Álava, Bizkaia, Gipuzkoa), stores the legal PDF and emails it to the
        customer. Retrying with the same `expected_version` reuses the same
        number. An issued invoice never changes; correct it instead.
      tags: [Invoices]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expected_version]
              additionalProperties: false
              properties:
                expected_version:
                  type: string
                  minLength: 64
                  maxLength: 64
                  description: The `version` of the draft the person reviewed.
      responses:
        '200':
          description: The issued invoice.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Invoice'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '402': {$ref: '#/components/responses/PaymentRequired'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
        '502': {$ref: '#/components/responses/ProviderPending'}
  /companies/{company_id}/invoices/{invoice_id}/send:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/InvoiceId'
    post:
      operationId: sendInvoice
      summary: Send an invoice
      description: |
        Emails the registered invoice's PDF to the customer's address, when the
        automatic delivery at issue did not go out. An invoice already sent is
        not sent again.
      tags: [Invoices]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                acknowledge_duplicate_risk:
                  type: boolean
                  default: false
                  description: Required as `true` when `email_delivery_status` is `unknown`, because the earlier attempt may have reached the customer.
      responses:
        '200':
          description: The invoice with its delivery state.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Invoice'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/invoices/{invoice_id}/correct:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/InvoiceId'
    post:
      operationId: correctInvoice
      summary: Draft a corrective invoice
      description: |
        Saves a corrective (rectificativa) draft that references a registered
        invoice. `notes` must give the reason for the correction, and lines may
        carry negative prices. The original stays as issued; the corrective is
        issued like any draft, in the `R-` series.
      tags: [Invoices]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/InvoiceCorrectionInput'}
      responses:
        '201':
          description: The corrective draft, with `type` `corrective` and `corrects_invoice_id`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Invoice'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/invoices/{invoice_id}/mark-paid:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/InvoiceId'
    post:
      operationId: markInvoicePaid
      summary: Mark an invoice as paid
      description: |
        Records that a registered invoice was paid in full and deactivates its
        payment link. It records money already received; it never collects or
        moves money. Calling it again keeps the first `paid_at`.
      tags: [Invoices]
      x-melso-status: live
      responses:
        '200':
          description: The invoice with `status` `paid`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Invoice'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Bookkeeping ───────────────────────────
  /companies/{company_id}/expenses:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listExpenses
      summary: List expenses
      description: Lists the company's expenses, newest first.
      tags: [Expenses]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: Expenses.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Expense'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: createExpense
      summary: Record an expense
      description: |
        Records a purchase invoice or receipt from its details. `subtotal` plus
        `vat_amount` must equal `total` exactly; IRPF withheld is not part of
        `total`. Melso proposes the AEAT category and estimates deductibility
        when you leave them out. To record from a file instead, upload it to
        Documents and use [Read a fiscal document](/api/reference/documents/extract-fiscal-document).
      tags: [Expenses]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ExpenseInput'}
            example:
              supplier_name: Coworking Delicias S.L.
              supplier_tax_id: B50123456
              document_number: F-2026-0912
              expense_date: '2026-10-01'
              subtotal: '200.00'
              vat_rate: '21'
              vat_amount: '42.00'
              total: '242.00'
              category: G03
              deductible: 'yes'
      responses:
        '201':
          description: The expense.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Expense'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/expenses/{expense_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: expense_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getExpense
      summary: Get an expense
      description: Returns one expense with its VAT lines, category, deductibility estimate and `version`.
      tags: [Expenses]
      x-melso-status: live
      responses:
        '200':
          description: The expense.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Expense'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      operationId: updateExpense
      summary: Update an expense
      description: |
        Changes the fields you send, at exactly `expected_version`. `null` clears
        `category`, `account`, `file_id` and `description`. The tax ID, number
        and date of an expense read from its document cannot change. An expense
        in a return that was approved or filed is not changed by its return:
        the filed return shows the difference in `recalculated`.
      tags: [Expenses]
      x-melso-status: live
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ExpensePatch'}
            example:
              expected_version: 3
              category: G05
              deductible: 'yes'
      responses:
        '200':
          description: The updated expense, with a new `version`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Expense'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
    delete:
      operationId: deleteExpense
      summary: Delete an expense
      description: Deletes an expense recorded by mistake. Its file stays in Documents.
      tags: [Expenses]
      x-melso-status: live
      x-melso-actor: person
      responses:
        '204':
          description: Deleted.
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

  /companies/{company_id}/income:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listIncome
      summary: List income
      description: Lists the income invoiced outside Melso, newest first. Invoices issued with Melso are in Invoices.
      tags: [Income]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: Income.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Income'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: createIncome
      summary: Record an income
      description: |
        Records a sales invoice the company issued with another program, such as
        your ERP or a sales platform, so it counts in its VAT and income tax.
        Melso calculates `subtotal`, `vat_amount` and `total` from `vat_lines`.
        `document_number` is unique in the company.
      tags: [Income]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/IncomeInput'}
            example:
              customer_name: Talleres Ebro S.L.
              customer_tax_id: B12345674
              document_number: A-2026-0187
              issue_date: '2026-10-05'
              description: Mantenimiento web octubre
              vat_lines:
                - rate: '21'
                  base: '800.00'
                  vat: '168.00'
              irpf_rate: '15'
              irpf_amount: '120.00'
      responses:
        '201':
          description: The income.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Income'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/income/{income_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: income_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getIncome
      summary: Get an income
      description: Returns one income with its VAT lines, withholding, review codes and `version`.
      tags: [Income]
      x-melso-status: live
      responses:
        '200':
          description: The income.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Income'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    put:
      operationId: replaceIncome
      summary: Replace an income
      description: Replaces the income with the body you send, at exactly `expected_version`. Send every field again; a field left out is cleared.
      tags: [Income]
      x-melso-status: live
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/IncomeInput'
                - type: object
                  required: [expected_version]
                  properties:
                    expected_version: {type: integer, minimum: 1}
      responses:
        '200':
          description: The replaced income, with a new `version`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Income'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
    delete:
      operationId: deleteIncome
      summary: Delete an income
      description: Deletes an income recorded by mistake. Its file stays in Documents.
      tags: [Income]
      x-melso-status: live
      x-melso-actor: person
      responses:
        '204':
          description: Deleted.
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

  /companies/{company_id}/documents:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listDocuments
      summary: List documents
      description: Lists the files the company keeps in Melso's Documents, newest first.
      tags: [Documents]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Documents.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/StoredDocument'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: uploadDocument
      summary: Upload a document
      description: Stores a file of up to 100 MiB in the company's Documents. Its `file_id` links it to an expense, an income or a case as evidence.
      tags: [Documents]
      x-melso-status: live
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: {type: string, format: binary}
                category:
                  type: string
                  enum: [expenses, contracts, receipts]
      responses:
        '201':
          description: The stored document.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/StoredDocument'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /companies/{company_id}/documents/extract:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: extractFiscalDocument
      summary: Read a fiscal document
      description: |
        Reads an invoice or a receipt (a PDF with text, or a JPEG, PNG, WebP or
        GIF photo, up to 15 MB), stores it in Documents and records it as an
        expense or an income, depending on whether the company received or
        issued it. `review` lists what to check. A document already recorded
        answers `200` with `duplicate` `true` and the existing record.
      tags: [Documents]
      x-melso-status: live
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: {type: string, format: binary}
                direction:
                  type: string
                  enum: [income, expense]
                  description: Leave it out to let Melso decide from the parties on the document.
                counterparty_country:
                  type: string
                  description: ISO 3166-1 alpha-2, when the document does not show it.
      responses:
        '200':
          description: The document was already recorded.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ExtractionResult'}
        '201':
          description: The new record.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ExtractionResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '402': {$ref: '#/components/responses/PaymentRequired'}
        '409': {$ref: '#/components/responses/Conflict'}
        '413':
          description: The file is larger than 15 MB.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        '415':
          description: The file type is not supported.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        '422':
          description: |
            The file is not a fiscal document, cannot be read, or its direction,
            VAT treatment or country cannot be decided. `code` says which:
            `not_fiscal`, `unreadable`, `pdf_without_text`, `direction_unknown`,
            `vat_treatment_unknown`, `country_unknown` or `exchange_rate`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}

  /companies/{company_id}/bank-accounts:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listBankAccounts
      summary: List bank accounts
      description: Lists the company's bank accounts. Banking and accounting are for empresas.
      tags: [Banking]
      x-melso-status: live
      responses:
        '200':
          description: Bank accounts.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/BankAccount'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
    post:
      operationId: createBankAccount
      summary: Connect a bank account
      description: |
        Registers an account your platform holds for the company, so you can
        push its movements. Use `external_id` for your own account ID.
      tags: [Banking]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, iban]
              additionalProperties: false
              properties:
                name: {type: string, minLength: 1, maxLength: 120}
                iban: {type: string}
                currency: {type: string, default: EUR}
                external_id: {type: string}
      responses:
        '201':
          description: The account.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/BankAccount'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/bank-accounts/{bank_account_id}/movements:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: bank_account_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: pushBankMovements
      summary: Push bank movements
      description: |
        Sends up to 1,000 booked movements of an account you hold, as they
        happen. Movements are matched by `external_id`, so sending one again is
        harmless. Melso proposes a reconciliation for each new movement and
        emits `bank_movement.reconciliation_proposed`; it reconciles nothing on
        its own.
      tags: [Banking]
      x-melso-status: preview
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [movements]
              additionalProperties: false
              properties:
                movements:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  items: {$ref: '#/components/schemas/BankMovementInput'}
            example:
              movements:
                - external_id: tx_9f1c22
                  booked_on: '2026-10-14'
                  value_on: '2026-10-14'
                  amount: '-242.00'
                  currency: EUR
                  concept: RECIBO COWORKING DELICIAS OCT
                  counterparty_name: Coworking Delicias S.L.
                  counterparty_iban: ES9121000418450200051332
                  balance_after: '18234.51'
      responses:
        '200':
          description: How many were saved, and which were already known.
          content:
            application/json:
              schema:
                type: object
                properties:
                  saved: {type: integer}
                  duplicates: {type: integer}
                  rejected:
                    type: array
                    items:
                      type: object
                      properties:
                        external_id: {type: string}
                        error: {type: string}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/bank-movements:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listBankMovements
      summary: List bank movements
      description: Lists movements, newest booking date first, each with its reconciliation or Melso's proposal.
      tags: [Banking]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - name: bank_account_id
          in: query
          schema: {type: string, format: uuid}
        - name: status
          in: query
          description: '`to_review` returns only the movements not reconciled.'
          schema:
            type: string
            enum: [all, to_review]
            default: all
      responses:
        '200':
          description: Movements.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/BankMovement'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /companies/{company_id}/bank-movements/{bank_movement_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/BankMovementId'
    get:
      operationId: getBankMovement
      summary: Get a bank movement
      description: Returns one movement with its reconciliation or Melso's proposal.
      tags: [Banking]
      x-melso-status: live
      responses:
        '200':
          description: The movement.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/BankMovement'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/bank-movements/{bank_movement_id}/reconciliation:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/BankMovementId'
    post:
      operationId: reconcileBankMovement
      summary: Reconcile a bank movement
      description: |
        Settles the movement against one or more open expenses, income or
        invoices. Money in settles income and invoices; money out settles
        expenses. A document without `amount` is settled for what is still
        outstanding. Whatever the movement does not settle stays on account.
      tags: [Banking]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [documents]
              additionalProperties: false
              properties:
                documents:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [type, id]
                    properties:
                      type:
                        type: string
                        enum: [expense, income, invoice]
                      id: {type: string, format: uuid}
                      amount: {$ref: '#/components/schemas/Amount'}
      responses:
        '201':
          description: The movement with its reconciliation.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/BankMovement'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
        '422':
          description: A document is not open, or an amount exceeds what the document or the movement has left.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
    delete:
      operationId: undoBankReconciliation
      summary: Undo a reconciliation
      description: Removes the movement's reconciliation. The documents are open again.
      tags: [Banking]
      x-melso-status: live
      x-melso-actor: person
      responses:
        '204':
          description: Undone.
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/bank-statements:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listBankStatements
      summary: List bank statements
      description: Lists the statements saved for the company, latest period first.
      tags: [Banking]
      x-melso-status: live
      responses:
        '200':
          description: Statements.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/BankStatement'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
    post:
      operationId: saveBankStatement
      summary: Save a bank statement
      description: |
        Saves a statement file's new movements, skipping those already saved.
        The opening and closing balances must reconcile with the movements.
        Read the file first with [Read a bank statement](/api/reference/banking/read-bank-statement)
        to show the person what it holds.
      tags: [Banking]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: {$ref: '#/components/schemas/BankStatementUpload'}
      responses:
        '201':
          description: The saved statement and how many movements were new.
          content:
            application/json:
              schema:
                type: object
                properties:
                  bank_statement: {$ref: '#/components/schemas/BankStatement'}
                  bank_account: {$ref: '#/components/schemas/BankAccount'}
                  saved: {type: integer}
                  duplicates: {type: integer}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
        '422':
          description: The file cannot be read, holds several accounts, or its balances do not reconcile.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
  /companies/{company_id}/bank-statements/read:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: readBankStatement
      summary: Read a bank statement
      description: |
        Reads a statement file without saving it: Norma 43 (Cuaderno 43), CSV,
        XLSX or PDF, up to 5 MB and 5,000 movements of one account. The format
        is detected from the content. Returns the movements, the balances,
        which movements are already saved and whether it can be saved as is.
      tags: [Banking]
      x-melso-status: live
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: {$ref: '#/components/schemas/BankStatementUpload'}
      responses:
        '200':
          description: What the file holds.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/BankStatementPreview'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '402': {$ref: '#/components/responses/PaymentRequired'}
        '422':
          description: The file cannot be read, is empty, holds several accounts or is of another account.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}

  /companies/{company_id}/journal-entries:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listJournalEntries
      summary: List journal entries
      description: |
        The company's journal (libro diario) in date order, as Melso keeps it
        from its expenses, income, invoices, reconciliations and depreciation,
        plus the entries imported from earlier books. With `account`, only the
        entries that move accounts starting with those digits: that account's
        ledger (libro mayor).
      tags: [Accounting]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - name: account
          in: query
          description: An account or its first digits, such as `572`.
          schema: {type: string, pattern: '^[0-9]{1,12}$'}
        - name: limit
          in: query
          schema: {type: integer, minimum: 1, maximum: 500, default: 100}
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Entries.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/JournalEntry'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /companies/{company_id}/journal-imports:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listJournalImports
      summary: List journal imports
      description: Lists the journals imported from earlier books.
      tags: [Accounting]
      x-melso-status: live
      responses:
        '200':
          description: Imports.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/JournalImport'}
        '401': {$ref: '#/components/responses/Unauthorized'}
    post:
      operationId: saveJournalImport
      summary: Import a journal
      description: |
        Imports a journal kept in another program (CSV or XLSX, up to 10 MB and
        100,000 lines), so Melso's books continue from it. Every entry must
        balance and use accounts of the Plan General de Contabilidad. Read the
        file first with [Read a journal file](/api/reference/accounting/read-journal-import).
      tags: [Accounting]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: {type: string, format: binary}
      responses:
        '201':
          description: The import.
          content:
            application/json:
              schema:
                type: object
                properties:
                  journal_import: {$ref: '#/components/schemas/JournalImport'}
                  entries: {type: integer}
                  lines: {type: integer}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
        '422':
          description: A row is invalid (`invalid_date`, `invalid_amount`, `invalid_account`, `unsupported_group`, `unbalanced_entry`) or has a future date.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
  /companies/{company_id}/journal-imports/read:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: readJournalImport
      summary: Read a journal file
      description: Reads a journal file without saving it, and says what it holds, what overlaps the books and whether it can be saved.
      tags: [Accounting]
      x-melso-status: live
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: {type: string, format: binary}
      responses:
        '200':
          description: What the file holds.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ImportPreview'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '422':
          description: The file cannot be read.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
  /companies/{company_id}/journal-imports/{import_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ImportId'
    delete:
      operationId: deleteJournalImport
      summary: Delete a journal import
      description: Removes an imported journal and its entries from the books.
      tags: [Accounting]
      x-melso-status: live
      x-melso-actor: person
      responses:
        '204':
          description: Deleted.
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/vat-book-imports:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listVatBookImports
      summary: List VAT book imports
      description: Lists the VAT books (libros registro de IVA) imported from other programs.
      tags: [Accounting]
      x-melso-status: live
      responses:
        '200':
          description: Imports.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/VatBookImport'}
        '401': {$ref: '#/components/responses/Unauthorized'}
    post:
      operationId: saveVatBookImport
      summary: Import a VAT book
      description: |
        Imports an issued or received VAT book (CSV with headers, ContaPlus CSV
        or XLSX, up to 10 MB and 50,000 rows) as the company's income or
        expenses, skipping invoices already recorded. The fastest way to bring
        a company's history from an ERP.
      tags: [Accounting]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: {$ref: '#/components/schemas/VatBookUpload'}
      responses:
        '201':
          description: The import and how many invoices it recorded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  vat_book_import: {$ref: '#/components/schemas/VatBookImport'}
                  invoices: {type: integer}
                  duplicates: {type: integer}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/vat-book-imports/read:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: readVatBookImport
      summary: Read a VAT book file
      description: Reads a VAT book without saving it, with its totals by VAT rate, the invoices already recorded and what to review.
      tags: [Accounting]
      x-melso-status: live
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: {$ref: '#/components/schemas/VatBookUpload'}
      responses:
        '200':
          description: What the file holds.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ImportPreview'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '422':
          description: The file cannot be read.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
  /companies/{company_id}/vat-book-imports/{import_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ImportId'
    delete:
      operationId: deleteVatBookImport
      summary: Delete a VAT book import
      description: Deletes the import and the income or expenses it recorded. Refused when a 303 already approved or filed covers its dates.
      tags: [Accounting]
      x-melso-status: live
      x-melso-actor: person
      responses:
        '204':
          description: Deleted.
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/reports/trial-balance:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: getTrialBalance
      summary: Get the trial balance
      description: The year's trial balance (balance de sumas y saldos) from 1 January to `to`, by account.
      tags: [Accounting]
      x-melso-status: live
      parameters:
        - name: year
          in: query
          required: true
          schema: {type: integer, minimum: 2000, maximum: 2100}
        - name: to
          in: query
          schema: {type: string, format: date}
        - name: digits
          in: query
          description: Group accounts by this many digits. Defaults to 4.
          schema: {type: integer, minimum: 0, maximum: 12}
        - name: prefix
          in: query
          description: Only accounts starting with these digits, in full.
          schema: {type: string}
      responses:
        '200':
          description: The trial balance.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TrialBalance'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /companies/{company_id}/reports/profit-and-loss:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: getProfitAndLoss
      summary: Get the profit and loss account
      description: The profit and loss account (cuenta de pérdidas y ganancias) between two dates, in the PGC's model, optionally beside a comparison period.
      tags: [Accounting]
      x-melso-status: live
      parameters:
        - name: from
          in: query
          required: true
          schema: {type: string, format: date}
        - name: to
          in: query
          required: true
          schema: {type: string, format: date}
        - name: compare_from
          in: query
          schema: {type: string, format: date}
        - name: compare_to
          in: query
          schema: {type: string, format: date}
      responses:
        '200':
          description: The account.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProfitAndLoss'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /companies/{company_id}/reports/balance-sheet:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: getBalanceSheet
      summary: Get the balance sheet
      description: The balance sheet at a date, in the PGC's model, optionally beside another date.
      tags: [Accounting]
      x-melso-status: live
      parameters:
        - name: date
          in: query
          description: Defaults to today.
          schema: {type: string, format: date}
        - name: compare_date
          in: query
          schema: {type: string, format: date}
      responses:
        '200':
          description: The balance sheet.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/BalanceSheet'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}

  # ───────────────────────────── Tax calendar ──────────────────────────
  /companies/{company_id}/tax-calendar:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: getTaxCalendar
      summary: Get the tax calendar
      description: |
        Returns the forms the company must file, from about six months back to
        about thirteen months ahead, ordered by deadline. Deadlines already move
        for national and regional public holidays. A form Melso calculates
        (`melso` `calculates`) links to its return once one exists; a form Melso
        only reminds of (`reminds`) links to the official instructions.
      tags: [Tax calendar]
      x-melso-status: live
      responses:
        '200':
          description: The calendar.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TaxCalendar'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/tax-calendar/mark-filed:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: markObligationFiled
      summary: Mark a reminder form as filed
      description: |
        Records that the company filed a form Melso only reminds of (for example
        a foral form or a monthly 303) by itself. It applies once the period has
        ended, and not to a form Melso calculates.
      tags: [Tax calendar]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [obligation_id, period]
              additionalProperties: false
              properties:
                obligation_id:
                  type: string
                  description: The calendar entry's `obligation_id`.
                period: {$ref: '#/components/schemas/Period'}
      responses:
        '200':
          description: The entry, now filed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  obligation_id: {type: string}
                  period: {$ref: '#/components/schemas/Period'}
                  filing_status: {type: string, const: filed}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Tax returns ───────────────────────────
  /companies/{company_id}/tax-returns:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listTaxReturns
      summary: List tax returns
      description: |
        Lists the company's returns: the ones Melso calculated (drafts,
        approved and filed) and the reminder forms marked filed. While a
        filing window is open, a draft is recalculated from the company's
        records, and each change raises its `version`. Without an active tax
        service, the list carries estimates (`is_estimate` `true`) instead of
        drafts.
      tags: [Tax returns]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: model
          in: query
          description: Only this form, such as `303`.
          schema: {type: string}
        - name: status
          in: query
          schema: {$ref: '#/components/schemas/TaxReturnStatus'}
      responses:
        '200':
          description: Returns.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/TaxReturn'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/tax-returns/{tax_return_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/TaxReturnId'
    get:
      operationId: getTaxReturn
      summary: Get a tax return
      description: Returns one return with its boxes, the records behind an informative return and what to review before approving.
      tags: [Tax returns]
      x-melso-status: live
      responses:
        '200':
          description: The return.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TaxReturn'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/tax-returns/{tax_return_id}/aeat-file:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/TaxReturnId'
    get:
      operationId: getTaxReturnAeatFile
      summary: Download the AEAT import file
      description: |
        Returns the fixed-width file the AEAT's online form imports, for a
        quarterly 303 or 130 and for a 111 or 115, encoded in ISO-8859-1. It is
        a way to load the return into the AEAT's form. It is not a submission
        and not proof of filing.
      tags: [Tax returns]
      x-melso-status: live
      responses:
        '200':
          description: The import file, named like `303_2026_3T.txt`.
          content:
            text/plain:
              schema: {type: string}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '402': {$ref: '#/components/responses/PaymentRequired'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/tax-returns/{tax_return_id}/approval-link:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/TaxReturnId'
    post:
      operationId: createTaxReturnApprovalLink
      summary: Create a tax return approval link
      description: |
        Returns the page where the company's owner reviews the exact calculated
        return and authorizes that version. Authorizing opens a case for Melso's
        professionals to review and file it. An estimate has no approval page.
      tags: [Tax returns]
      x-melso-status: live
      responses:
        '200':
          description: The approval page for the return's current version.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ApprovalLink'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/tax-returns/{tax_return_id}/approve:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/TaxReturnId'
    post:
      operationId: approveTaxReturn
      summary: Approve a tax return
      description: |
        The owner's authorization of the return at exactly `expected_version`.
        It freezes the figures and opens the case in which Melso's
        professionals review and file the return. If the draft changed, the call
        is refused and the person reviews the current version.
      tags: [Tax returns]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expected_version]
              additionalProperties: false
              properties:
                expected_version:
                  type: integer
                  minimum: 1
                  description: The `version` the person reviewed.
      responses:
        '200':
          description: The return, `approved`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TaxReturn'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '402': {$ref: '#/components/responses/PaymentRequired'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/tax-returns/{tax_return_id}/attested-approvals:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/TaxReturnId'
    post:
      operationId: attestTaxReturnApproval
      summary: Attest an approval from your interface
      description: |
        For partners that show the return inside their own product. You attest
        that the company's authorized person reviewed the boxes of exactly
        `expected_version` in your interface and approved it, and you send the
        evidence. Melso records the attestation with the evidence and treats it
        as that person's approval. Requires a signed partner agreement that
        defines who may approve and how evidence is kept.
      tags: [Tax returns]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AttestedApprovalInput'}
            example:
              expected_version: 4
              approver:
                name: Lucía Martín Pérez
                email: lucia@example.com
                role: owner
                external_user_id: usr_8f2k1
              evidence:
                approved_at: '2026-10-14T09:12:44Z'
                ip_address: 203.0.113.24
                user_agent: Mozilla/5.0
                shown_document_sha256: 9f2c4e0b7d1a3c5e8f6a2b4d0c1e3f5a7b9d2c4e6f8a0b1c3d5e7f9a2b4c6d8e
      responses:
        '200':
          description: The return, `approved`, with the attestation recorded.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TaxReturn'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/tax-returns/{tax_return_id}/mark-filed:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/TaxReturnId'
    post:
      operationId: markTaxReturnFiled
      summary: Mark a return as filed by the company
      description: |
        Records that the company filed this return by itself, at exactly
        `expected_version`, once its period has ended. Returns that Melso's
        professionals file are marked filed by Melso, with the administration's
        reference and receipt in `filing`.
      tags: [Tax returns]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expected_version]
              additionalProperties: false
              properties:
                expected_version: {type: integer, minimum: 1}
      responses:
        '200':
          description: The return, `filed`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TaxReturn'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/tax-returns/imports:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: importFiledTaxReturn
      summary: Record a return filed elsewhere
      description: |
        Records a quarterly 303 or 130 that was already filed outside Melso, for
        example by the company's previous gestoría, with the AEAT's reference,
        its boxes and the receipt PDF. Later quarters then carry forward from
        it: the 303's amounts to offset and the 130's payments to date. It does
        not file, approve or pay anything.

        Send `expected_version` `0` for a period Melso has no return for, or
        the current draft's `version` to replace it. A return under review,
        approved or filed by Melso cannot be replaced.
      tags: [Tax returns]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [request_id, model, period, tax_id, filed_at, external_reference, receipt_file_id, expected_version, boxes]
              additionalProperties: false
              properties:
                request_id:
                  type: string
                  format: uuid
                  description: Your key for this import. Reusing it with different details is refused.
                model:
                  type: string
                  enum: ['303', '130']
                period:
                  type: string
                  pattern: '^[0-9]{4}-Q[1-4]$'
                tax_id:
                  type: string
                  description: The tax ID on the receipt. Must be the company's.
                filed_at:
                  type: string
                  format: date-time
                  description: After the quarter ended and no later than today.
                external_reference:
                  type: string
                  maxLength: 200
                  description: The AEAT's submission reference on the receipt.
                receipt_file_id:
                  type: string
                  format: uuid
                  description: The receipt, a PDF already in the company's Documents.
                expected_version:
                  type: integer
                  minimum: 0
                boxes:
                  type: object
                  description: The declared boxes, by AEAT box number, as amounts with two decimals.
                  additionalProperties: {$ref: '#/components/schemas/Amount'}
            example:
              request_id: 0f1e2d3c-4b5a-4968-8776-655443322110
              model: '303'
              period: 2026-Q2
              tax_id: 12345678Z
              filed_at: '2026-07-18T10:00:00Z'
              external_reference: '3030123456789'
              receipt_file_id: 6f0c1d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f
              expected_version: 0
              boxes:
                '27': '1840.00'
                '45': '612.30'
                '71': '1227.70'
      responses:
        '200':
          description: The same import was already recorded with this `request_id`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TaxReturn'}
        '201':
          description: The return, `filed`.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TaxReturn'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Corporate tax ─────────────────────────
  /companies/{company_id}/corporate-tax/{year}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: year
        in: path
        required: true
        description: The fiscal year, such as `2025`.
        schema: {type: integer, minimum: 2000, maximum: 2100}
    get:
      operationId: getCorporateTax
      summary: Get the corporate tax
      description: |
        The year's corporate tax (Impuesto sobre Sociedades, modelo 200) as Melso
        calculates it from the books and the saved inputs: result before tax,
        adjustments, capitalization reserve, negative bases offset, taxable base,
        rate, cuota íntegra and líquida, withholdings, instalments paid and what
        is left to pay, and the deferred tax; plus the modelo 202 instalments
        that build on it, with their deadlines. For empresas.
      tags: [Corporate tax]
      x-melso-status: live
      responses:
        '200':
          description: The calculation.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CorporateTax'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
    put:
      operationId: saveCorporateTaxInputs
      summary: Save the corporate tax inputs
      description: |
        Saves what the year's corporate tax needs beyond the books, at exactly
        `expected_version`, and returns the recalculated tax. Only the fields
        you send change; an empty amount takes the value from the books. It does
        not approve or file the 200.
      tags: [Corporate tax]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CorporateTaxInput'}
            example:
              expected_version: 2
              adjustments:
                - concept: Multa de tráfico (art. 15.c LIS)
                  amount: '300.00'
                  temporary: false
              capitalization_reserve: true
              losses_offset: '4500.00'
      responses:
        '200':
          description: The recalculated tax.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CorporateTax'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Tax estimates ─────────────────────────
  /companies/{company_id}/tax-estimates:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: getTaxEstimate
      summary: Get the tax set-aside estimate
      description: |
        What the company's records so far imply for the returns of a period, and
        the total to set aside. Built for a "tax pot" in a business account:
        refresh it after each new movement, invoice or expense. An estimate is
        not a return and has no approval.
      tags: [Tax estimates]
      x-melso-status: preview
      parameters:
        - name: period
          in: query
          required: true
          description: A quarter such as `2026-Q4`, or a year such as `2026` for every quarter of it.
          schema: {$ref: '#/components/schemas/Period'}
      responses:
        '200':
          description: The estimate.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/TaxEstimate'}
              example:
                period: 2026-Q4
                as_of: '2026-11-03T08:00:00Z'
                set_aside_total: '3142.55'
                items:
                  - model: '303'
                    period: 2026-Q4
                    amount: '2210.40'
                    deadline: '2027-01-30'
                    basis: records_to_date
                  - model: '130'
                    period: 2026-Q4
                    amount: '932.15'
                    deadline: '2027-01-30'
                    basis: records_to_date
                review: [expenses_without_vat_rate]
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

  # ───────────────────────────── Cases ─────────────────────────────────
  /companies/{company_id}/case-kinds:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listCaseKinds
      summary: List case kinds
      description: |
        The kinds of case this company can open, each with what it is, the JSON
        Schema of its `details`, whether the company approves a professional's
        draft, and how it is paid: included in the plan, or a one-off service
        price the owner pays before Melso starts.
      tags: [Cases]
      x-melso-status: live
      responses:
        '200':
          description: Kinds.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/CaseKind'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/cases:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listCases
      summary: List cases
      description: Lists the company's cases, newest first, with their status, deadlines, versions and filing reference.
      tags: [Cases]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          schema: {$ref: '#/components/schemas/CaseStatus'}
      responses:
        '200':
          description: Cases.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Case'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: createCase
      summary: Open a case
      description: |
        Opens a case of a kind from [List case kinds](/api/reference/cases/list-case-kinds),
        with `details` that match the kind's schema, and sends it to Melso's
        professionals when `submit` is `true` and the plan covers it. A kind
        paid as a one-off service stays a draft until the owner pays, and the
        response carries `payment_url`; with `payer` `partner` (preview, under a
        partner agreement) Melso charges your partner account instead and the
        case is sent at once.
      tags: [Cases]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CaseCreate'}
            example:
              kind: alta
              details:
                start_date: '2026-12-01'
                activity: Diseño gráfico para empresas
              note: Empieza a facturar el 1 de diciembre.
              submit: true
      responses:
        '201':
          description: The case.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Case'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '402': {$ref: '#/components/responses/PaymentRequired'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/cases/{case_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/CaseId'
    get:
      operationId: getCase
      summary: Get a case
      description: Returns the case with its conversation, versions, open information request and documents, including the professionals' drafts and the filing receipts.
      tags: [Cases]
      x-melso-status: live
      responses:
        '200':
          description: The case.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Case'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/cases/{case_id}/submit:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/CaseId'
    post:
      operationId: submitCase
      summary: Submit a case
      description: Sends a draft case to Melso's professionals. A kind paid as a service answers `402` with the `payment_url` where the owner pays first.
      tags: [Cases]
      x-melso-status: live
      responses:
        '200':
          description: The submitted case.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Case'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '402': {$ref: '#/components/responses/PaymentRequired'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/cases/{case_id}/messages:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/CaseId'
    post:
      operationId: addCaseMessage
      summary: Add a message to a case
      description: |
        Sends a message or documents to the professionals on a submitted case.
        With `changes` `true`, it asks for changes to the version waiting for
        approval and returns the case to review.
      tags: [Cases]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                content: {type: string, maxLength: 20000}
                file_ids:
                  type: array
                  maxItems: 20
                  items: {type: string, format: uuid}
                changes: {type: boolean, default: false}
      responses:
        '201':
          description: The case with the new message.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Case'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/cases/{case_id}/answers:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/CaseId'
    post:
      operationId: answerCaseRequest
      summary: Answer an information request
      description: Answers every item of the case's open information request, a document for a document item and text for a question, and returns the case to review.
      tags: [Cases]
      x-melso-status: live
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [request_id, answers]
              additionalProperties: false
              properties:
                request_id: {type: string, format: uuid}
                answers:
                  type: array
                  minItems: 1
                  maxItems: 20
                  items:
                    type: object
                    required: [item_id]
                    properties:
                      item_id: {type: string}
                      answer: {type: string, maxLength: 2000}
                      file_id: {type: string, format: uuid}
      responses:
        '200':
          description: The case, back in review.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Case'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/cases/{case_id}/approval-link:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/CaseId'
    post:
      operationId: createCaseApprovalLink
      summary: Create a case approval link
      description: Returns the page where the owner reviews the professional's current draft and approves it.
      tags: [Cases]
      x-melso-status: live
      responses:
        '200':
          description: The approval page for the current version.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ApprovalLink'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/cases/{case_id}/approve:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/CaseId'
    post:
      operationId: approveCase
      summary: Approve a case draft
      description: The owner's approval of the professional's draft at exactly `expected_version`. Melso's professionals then file it.
      tags: [Cases]
      x-melso-status: live
      x-melso-actor: person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expected_version]
              additionalProperties: false
              properties:
                expected_version: {type: integer, minimum: 1}
      responses:
        '200':
          description: The case, approved.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Case'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/cases/{case_id}/attested-approvals:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/CaseId'
    post:
      operationId: attestCaseApproval
      summary: Attest a case approval from your interface
      description: |
        For partners that show the professional's draft inside their own
        product. You attest that the person who must approve it read exactly
        `expected_version` in your interface and approved it, and you send the
        evidence. Melso records the attestation with the evidence and treats it
        as that person's approval, the same as on Melso's approval page; you
        receive `case.approved`. A case whose version changed is refused.

        For an incorporation this approves the deed draft. It never stands in
        for signing the deed before the notary. Requires a signed partner
        agreement that defines who may approve and how evidence is kept.
      tags: [Cases]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AttestedApprovalInput'}
            example:
              expected_version: 2
              approver:
                name: Pablo Ruiz Ortega
                email: pablo@example.com
                role: owner
                tax_id: 12345678Z
                external_user_id: usr_3k9d2
              evidence:
                approved_at: '2026-11-20T16:03:12Z'
                ip_address: 203.0.113.24
                user_agent: Mozilla/5.0
                authentication: strong_customer_authentication
                shown_document_sha256: 4b1e7c9a2d5f8e0b3c6a9d2f5e8b1c4a7d0e3f6b9c2a5d8e1f4b7c0a3d6e9f2b
      responses:
        '200':
          description: The case, approved, with the attestation recorded.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Case'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/cases/{case_id}/cancel:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/CaseId'
    post:
      operationId: cancelCase
      summary: Cancel a case
      description: Withdraws a case Melso has not filed. Its conversation and documents are kept. A tax return authorized through it goes back to draft and needs a new approval.
      tags: [Cases]
      x-melso-status: live
      x-melso-actor: person
      responses:
        '200':
          description: The cancelled case.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Case'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Administration notifications ─────────
  /companies/{company_id}/notifications:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listAdministrationNotifications
      summary: List administration notifications
      description: |
        Lists the electronic notifications the AEAT, the TGSS, the DGT and other
        administrations made available to the company, as Melso reads them
        under the company's mandate. An unopened notification counts as received
        ten calendar days after it was made available, so `deemed_received_on`
        is the date that matters for any deadline it starts.
      tags: [Administration notifications]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          schema:
            type: string
            enum: [available, accessed, deemed_received]
      responses:
        '200':
          description: Notifications.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/AdministrationNotification'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/notifications/{notification_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: notification_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getAdministrationNotification
      summary: Get an administration notification
      description: Returns one notification with Melso's summary, the deadline it starts and the case Melso opened to answer it, if any.
      tags: [Administration notifications]
      x-melso-status: preview
      responses:
        '200':
          description: The notification.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AdministrationNotification'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

  # ───────────────────────────── Certificates ──────────────────────────
  /companies/{company_id}/certificates:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: requestCertificate
      summary: Request a certificate
      description: |
        Requests an official certificate for the company, such as being up to
        date with tax obligations (AEAT) or with the Social Security (TGSS),
        which lenders and public tenders ask for. Melso obtains it under the
        company's mandate; the certificate carries the administration's CSV
        (código seguro de verificación) for anyone to verify it.
      tags: [Certificates]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              additionalProperties: false
              properties:
                type: {$ref: '#/components/schemas/CertificateType'}
                purpose:
                  type: string
                  description: Why it is needed, when the administration asks for it, such as `public_procurement` or `subsidy`.
      responses:
        '201':
          description: The request, `pending` until the administration issues it.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Certificate'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/certificates/{certificate_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: certificate_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getCertificate
      summary: Get a certificate
      description: Returns the certificate request, and the certificate document once issued. A negative certificate says what the administration found pending.
      tags: [Certificates]
      x-melso-status: preview
      responses:
        '200':
          description: The certificate.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Certificate'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

  # ───────────────────────────── Employees ─────────────────────────────
  /companies/{company_id}/employees:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listEmployees
      summary: List employees
      description: Lists the company's employees, ordered by name. Employees are for empresas with Melso's payroll service.
      tags: [Employees]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: active
          in: query
          schema: {type: boolean}
      responses:
        '200':
          description: Employees.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Employee'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: createEmployee
      summary: Add an employee
      description: |
        Adds an employee to the company. `name`, `start_date` and `details` are
        live today. The structured identity and employment fields are preview:
        they are what affiliation, payroll runs and SEPE notifications use.
      tags: [Employees]
      x-melso-status: live
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/EmployeeInput'}
            example:
              name: Jorge Ruiz Gómez
              start_date: '2026-11-02'
              external_id: emp_10442
              tax_id: 12345678Z
              social_security_number: '281234567840'
              birth_date: '1991-04-18'
              employment:
                contract_type: '100'
                contribution_group: 5
                occupation_code: '3820'
                working_time_coefficient: 1000
                contribution_account: '28123456789'
      responses:
        '201':
          description: The employee.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Employee'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /companies/{company_id}/employees/{employee_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/EmployeeId'
    patch:
      operationId: updateEmployee
      summary: Update an employee
      description: |
        Changes the fields you send. `details` replaces the whole object. Every
        change is kept in the employee's history with who made it.
      tags: [Employees]
      x-melso-status: live
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/EmployeePatch'}
      responses:
        '200':
          description: The updated employee.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Employee'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/employees/{employee_id}/changes:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/EmployeeId'
    get:
      operationId: listEmployeeChanges
      summary: List an employee's changes
      description: The employee's change history, newest first, each with the full record before and after.
      tags: [Employees]
      x-melso-status: live
      responses:
        '200':
          description: Changes.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/EmployeeChange'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/employees/{employee_id}/withholding-certificates/{year}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/EmployeeId'
      - name: year
        in: path
        required: true
        schema: {type: integer, minimum: 2000, maximum: 2100}
    get:
      operationId: getWithholdingCertificate
      summary: Get an employee's withholding certificate
      description: |
        The year's certificate of income and IRPF withholdings (certificado de
        retenciones) the company must give the employee, built from the payroll
        runs and consistent with the company's modelo 190.
      tags: [Employees]
      x-melso-status: preview
      responses:
        '200':
          description: The certificate as a PDF.
          content:
            application/pdf:
              schema: {type: string, format: binary}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}

  # ───────────────────────────── Payroll runs ──────────────────────────
  /companies/{company_id}/payroll-runs:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listPayrollRuns
      summary: List payroll runs
      description: Lists the payroll runs you sent for the company, newest period first.
      tags: [Payroll runs]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: period
          in: query
          schema: {$ref: '#/components/schemas/Period'}
      responses:
        '200':
          description: Payroll runs.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/PayrollRun'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: createPayrollRun
      summary: Submit a payroll run
      description: |
        Sends the payslips your platform calculated for one month. Melso does not
        recalculate the payslips. It validates them against each employee's
        affiliation and contribution bases, then uses them for three things:

        - the month's Social Security contribution settlement (liquidación
          directa) and its CRA data,
        - the work-income boxes of the modelo 111, and the employee records
          (key A) of the year's modelo 190,
        - each employee's annual withholding certificate.

        Send a second run with `kind` `supplementary` for arrears or late
        corrections of an earlier month.
      tags: [Payroll runs]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/PayrollRunInput'}
            example:
              period: 2026-10
              kind: ordinary
              payment_date: '2026-10-31'
              external_id: run_2026_10
              payslips:
                - employee_id: 6a1f0c2e-4b3d-4e5f-8a9b-0c1d2e3f4a5b
                  days_contributed: 31
                  gross_pay: '2450.00'
                  in_kind_pay: '0.00'
                  irpf:
                    base: '2450.00'
                    rate: '14.20'
                    withheld: '347.90'
                  social_security:
                    base_common_contingencies: '2654.17'
                    base_professional_contingencies: '2654.17'
                    employee_contribution: '172.79'
                  concepts:
                    - code: salario_base
                      description: Salario base
                      amount: '1900.00'
                      kind: salary
                    - code: plus_transporte
                      description: Plus transporte
                      amount: '150.00'
                      kind: non_salary
      responses:
        '201':
          description: The run, `received`. Validation results arrive in `issues` and with the `payroll_run.validated` event.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PayrollRun'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/payroll-runs/{payroll_run_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: payroll_run_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getPayrollRun
      summary: Get a payroll run
      description: Returns the run with its totals, the issues found per payslip and the settlement and returns it feeds.
      tags: [Payroll runs]
      x-melso-status: preview
      responses:
        '200':
          description: The run.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PayrollRun'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

  # ───────────────────────────── Social Security ───────────────────────
  /companies/{company_id}/social-security/affiliations:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listAffiliations
      summary: List affiliation requests
      description: Lists the company's affiliation requests (altas, bajas and variaciones de datos), newest first.
      tags: [Social Security]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: employee_id
          in: query
          schema: {type: string, format: uuid}
      responses:
        '200':
          description: Affiliation requests.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Affiliation'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    post:
      operationId: createAffiliation
      summary: Request an alta, baja or variación
      description: |
        Asks Melso to communicate an employee's registration (`alta`),
        deregistration (`baja`) or data change (`variacion`) to the TGSS through
        the Sistema RED, under the company's TGSS mandate.

        Timing is the law's, not Melso's: an alta must reach the TGSS before the
        employee starts work, and a baja or a variación within three calendar
        days of the event. Send the request as soon as your platform knows.
      tags: [Social Security]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AffiliationInput'}
            example:
              employee_id: 6a1f0c2e-4b3d-4e5f-8a9b-0c1d2e3f4a5b
              action: alta
              effective_date: '2026-11-02'
      responses:
        '201':
          description: The request, `pending` until it reaches the TGSS.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Affiliation'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/social-security/affiliations/{affiliation_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: affiliation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getAffiliation
      summary: Get an affiliation request
      description: Returns the request with the TGSS's resolution, or the reason it was rejected.
      tags: [Social Security]
      x-melso-status: preview
      responses:
        '200':
          description: The request.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Affiliation'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/social-security/settlements:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      operationId: listContributionSettlements
      summary: List contribution settlements
      description: Lists the company's monthly Social Security contribution settlements, newest period first.
      tags: [Social Security]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: period
          in: query
          schema: {$ref: '#/components/schemas/Period'}
      responses:
        '200':
          description: Settlements.
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/ContributionSettlement'}
                  next_cursor: {$ref: '#/components/schemas/NextCursor'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
  /companies/{company_id}/social-security/settlements/{settlement_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: settlement_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getContributionSettlement
      summary: Get a contribution settlement
      description: |
        Returns one month's settlement: the TGSS's calculation, how it compares
        with the payroll run, its state and its documents, the RNT (relación
        nominal de trabajadores) and the RLC (recibo de liquidación de
        cotizaciones).
      tags: [Social Security]
      x-melso-status: preview
      responses:
        '200':
          description: The settlement.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ContributionSettlement'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

  # ───────────────────────────── Employment notifications ──────────────
  /companies/{company_id}/employment/contracts:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: notifyContract
      summary: Notify a contract (Contrat@)
      description: |
        Asks Melso to communicate the content of an employment contract, or its
        extension, to the SEPE through Contrat@. The law gives ten working days
        from signature.
      tags: [Employment notifications]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ContractNotificationInput'}
      responses:
        '201':
          description: The notification, `pending` until the SEPE registers it.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/EmploymentNotification'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/employment/certificates:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      operationId: sendCompanyCertificate
      summary: Send a company certificate (Certific@2)
      description: |
        Asks Melso to send the certificado de empresa to the SEPE through
        Certific@2 when an employment ends, so the employee can claim
        unemployment benefit. Melso fills the contribution bases of the last
        180 days from your payroll runs. The law gives ten days from the end of
        the employment.
      tags: [Employment notifications]
      x-melso-status: preview
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CompanyCertificateInput'}
      responses:
        '201':
          description: The notification, `pending` until the SEPE registers it.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/EmploymentNotification'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /companies/{company_id}/employment/notifications/{notification_id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - name: notification_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getEmploymentNotification
      summary: Get an employment notification
      description: Returns a Contrat@ or Certific@2 notification with the SEPE's reference and receipt once registered.
      tags: [Employment notifications]
      x-melso-status: preview
      responses:
        '200':
          description: The notification.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/EmploymentNotification'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}

webhooks:
  event:
    post:
      summary: An event about one of your companies
      description: |
        Sent to each endpoint subscribed to the event's type, signed with
        `Melso-Signature`. See [Webhooks](/api/webhooks).
      x-melso-status: preview
      parameters:
        - name: Melso-Event-ID
          in: header
          required: true
          schema: {type: string, format: uuid}
        - name: Melso-Signature
          in: header
          required: true
          description: '`t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>." + body>`'
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/Event'}
      responses:
        '200':
          description: Any 2xx within 15 seconds acknowledges the event.


components:
  securitySchemes:
    partnerKey:
      type: http
      scheme: bearer
      description: |
        Your partner secret key, `mk_live_…` for real companies or `mk_test_…`
        for test companies. Keep it on your servers. See
        [Authentication](/api/authentication).

  parameters:
    CompanyId:
      name: company_id
      in: path
      required: true
      description: The company's ID. Every company-scoped call works in exactly this company.
      schema: {type: string, format: uuid}
    InvoiceId:
      name: invoice_id
      in: path
      required: true
      schema: {type: string, format: uuid}
    TaxReturnId:
      name: tax_return_id
      in: path
      required: true
      schema: {type: string, format: uuid}
    CaseId:
      name: case_id
      in: path
      required: true
      schema: {type: string, format: uuid}
    EmployeeId:
      name: employee_id
      in: path
      required: true
      schema: {type: string, format: uuid}
    BankMovementId:
      name: bank_movement_id
      in: path
      required: true
      schema: {type: string, format: uuid}
    ImportId:
      name: import_id
      in: path
      required: true
      schema: {type: string, format: uuid}
    From:
      name: from
      in: query
      description: From this date, inclusive.
      schema: {type: string, format: date}
    To:
      name: to
      in: query
      description: Up to this date, inclusive.
      schema: {type: string, format: date}
    Limit:
      name: limit
      in: query
      description: Page size.
      schema: {type: integer, minimum: 1, maximum: 200, default: 50}
    Cursor:
      name: cursor
      in: query
      description: The `next_cursor` of the previous page.
      schema: {type: string}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: A unique key, such as a UUID, that makes a retried create safe. Reusing a key with a different body is refused.
      schema: {type: string, maxLength: 255}

  responses:
    BadRequest:
      description: The request is malformed or a field is invalid. Unknown fields are refused.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          example:
            error: 'lines[0].vat_rate must be 21, 10, 4 or 0'
            code: invalid_invoice
            request_id: req_01JAB3K9X2
    Unauthorized:
      description: The key is missing, revoked or invalid.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          example:
            error: invalid token
            code: unauthorized
            request_id: req_01JAB3K9X2
    PaymentRequired:
      description: The company's plan does not include this service, or its allowance is used up.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          example:
            error: The tax service is not active for this company
            code: tax_service_required
            request_id: req_01JAB3K9X2
    Forbidden:
      description: The key cannot do this in this company, or the step needs a person.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          example:
            error: Review and approve in the authenticated web application
            code: person_required
            request_id: req_01JAB3K9X2
    NotFound:
      description: No such object in this company, or the company is not one of yours.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          example:
            error: invoice not found
            code: not_found
            request_id: req_01JAB3K9X2
    Conflict:
      description: The object changed or is not in a state that allows this. Read it again and decide.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          example:
            error: The tax return changed or is not ready for approval
            code: version_conflict
            request_id: req_01JAB3K9X2
    ProviderPending:
      description: The registration with the tax authority's network is pending. Retry the same request; do not create a replacement.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Error'}
          example:
            error: The invoice number is reserved. Retry this same invoice to reconcile it; do not prepare a replacement.
            code: invoicing_provider_pending
            request_id: req_01JAB3K9X2

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: What went wrong, for a person to read.
        code:
          type: string
          description: A stable code to branch on, such as `invoicing_not_ready` or `version_conflict`.
        request_id:
          type: string
          description: Quote it when you contact Melso.
    NextCursor:
      type: [string, 'null']
      description: Pass it as `cursor` for the next page. `null` on the last page.
    Amount:
      type: string
      pattern: '^-?[0-9]+\.[0-9]{2}$'
      description: An amount in euros as a decimal string with two decimals, such as `"121.00"`.
      examples: ['121.00']
    Decimal:
      type: string
      pattern: '^-?[0-9]{1,10}(\.[0-9]{1,6})?$'
      description: A decimal number as a string, such as `"21"` or `"0.5"`.
    Period:
      type: string
      pattern: '^[0-9]{4}(-Q[1-4]|-(0[1-9]|1[0-2])|-[1-3]P)?$'
      description: A year (`2026`), a quarter (`2026-Q3`), a month (`2026-09`) or an instalment (`2026-1P`).
      examples: [2026-Q3]
    Administration:
      type: string
      enum: [aeat, tgss, sepe, alava, bizkaia, gipuzkoa, navarra, canarias, ceuta, melilla, registro_mercantil]
      description: The administration a form, mandate or notification belongs to. The foral treasuries of Álava, Bizkaia, Gipuzkoa and Navarra replace the AEAT where they apply.
    ApprovalLink:
      type: object
      required: [approval_url, version]
      properties:
        approval_url:
          type: string
          format: uri
          description: The signed-in Melso page where the person reviews and decides. Share it with the person; it is useless without their sign-in.
        version:
          description: The version the page shows. A later change needs a new link and a new decision.
          oneOf:
            - type: string
            - type: integer
    Document:
      type: object
      properties:
        id: {type: string, format: uuid}
        filename: {type: string}
        content_type: {type: string}
        size_bytes: {type: integer}
        url:
          type: string
          format: uri
          description: A short-lived download URL.

    # Mandates
    MandateProcedure:
      type: string
      enum: [tax_returns, notifications, certificates, census, affiliation, contributions, contracts, company_certificates]
      description: |
        What Melso may do under the mandate. AEAT: `tax_returns`, `notifications`,
        `certificates`, `census` (036). TGSS: `affiliation`,
        `contributions`, `notifications`, `certificates`. SEPE: `contracts`
        (Contrat@), `company_certificates` (Certific@2).
    MandateCreate:
      type: object
      required: [administration, procedures, signer]
      additionalProperties: false
      properties:
        administration:
          type: string
          enum: [aeat, tgss, sepe]
        procedures:
          type: array
          minItems: 1
          items: {$ref: '#/components/schemas/MandateProcedure'}
        signer:
          type: object
          required: [name, tax_id, email]
          description: The company's legal representative, or the autónomo.
          properties:
            name: {type: string}
            tax_id: {type: string, description: DNI or NIE.}
            email: {type: string, format: email}
        return_url:
          type: string
          format: uri
          description: Where the hosted signing page sends the person when they finish.
    Mandate:
      type: object
      required: [id, administration, procedures, status, created_at]
      properties:
        id: {type: string, format: uuid}
        administration:
          type: string
          enum: [aeat, tgss, sepe]
        procedures:
          type: array
          items: {$ref: '#/components/schemas/MandateProcedure'}
        status:
          type: string
          enum: [pending_signature, signed, active, rejected, revoked, expired]
          description: |
            `signed`: the company signed; Melso is registering it with the
            administration. `active`: registered, so Melso can act. `rejected`:
            the administration refused the registration; `rejection_reason` says why.
        signing_url:
          type: [string, 'null']
          format: uri
          description: The hosted page where the signer signs. Present while `pending_signature`.
        rejection_reason: {type: [string, 'null']}
        signed_at: {type: [string, 'null'], format: date-time}
        activated_at: {type: [string, 'null'], format: date-time}
        revoked_at: {type: [string, 'null'], format: date-time}
        evidence: {$ref: '#/components/schemas/Document'}
        created_at: {type: string, format: date-time}
    MandateList:
      type: object
      required: [data, next_cursor]
      properties:
        data:
          type: array
          items: {$ref: '#/components/schemas/Mandate'}
        next_cursor: {$ref: '#/components/schemas/NextCursor'}

    # Webhook endpoints
    WebhookEventType:
      type: string
      enum:
        - company.onboarding_completed
        - company.registered
        - mandate.signed
        - mandate.activated
        - mandate.rejected
        - mandate.revoked
        - invoice.issued
        - invoice.registered
        - invoice.registration_failed
        - invoice.sent
        - invoice.paid
        - document.processed
        - bank_movement.reconciliation_proposed
        - tax_obligation.due_soon
        - tax_return.calculated
        - tax_return.approved
        - tax_return.filed
        - case.updated
        - case.information_requested
        - case.approval_requested
        - case.approved
        - case.completed
        - notification.received
        - certificate.issued
        - payroll_run.validated
        - affiliation.accepted
        - affiliation.rejected
        - contribution_settlement.available
        - contribution_settlement.discrepancy
        - contribution_settlement.paid
        - employment_notification.registered
        - employment_notification.rejected
    WebhookEndpointInput:
      type: object
      required: [url, event_types]
      additionalProperties: false
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
          description: An absolute `https` URL that resolves to public addresses.
        description: {type: string, maxLength: 500}
        event_types:
          type: array
          minItems: 1
          items: {$ref: '#/components/schemas/WebhookEventType'}
        enabled: {type: boolean, default: true}
    WebhookEndpoint:
      type: object
      required: [id, url, event_types, enabled, created_at]
      properties:
        id: {type: string, format: uuid}
        url: {type: string, format: uri}
        description: {type: string}
        event_types:
          type: array
          items: {$ref: '#/components/schemas/WebhookEventType'}
        enabled: {type: boolean}
        created_at: {type: string, format: date-time}
    WebhookDeliveryStatus:
      type: string
      enum: [pending, delivering, succeeded, failed, disabled]
    WebhookDelivery:
      type: object
      properties:
        id: {type: string, format: uuid}
        event_id: {type: string, format: uuid}
        endpoint_id: {type: string, format: uuid}
        url: {type: string, format: uri}
        status: {$ref: '#/components/schemas/WebhookDeliveryStatus'}
        attempts: {type: integer}
        response_code: {type: [integer, 'null']}
        latency_ms: {type: [integer, 'null']}
        error: {type: [string, 'null']}
        next_attempt_at: {type: [string, 'null'], format: date-time}
        created_at: {type: string, format: date-time}
    Event:
      type: object
      required: [id, type, created_at, data]
      properties:
        id:
          type: string
          format: uuid
          description: Also sent as the `Melso-Event-ID` header. Use it to ignore a duplicate delivery.
        type: {$ref: '#/components/schemas/WebhookEventType'}
        created_at: {type: string, format: date-time}
        company_id: {type: [string, 'null'], format: uuid}
        data:
          type: object
          description: The object the event is about, as the matching `GET` returns it.

    # Customers
    CustomerKind:
      type: string
      enum: [company, professional, individual]
      description: '`company` for a business, `professional` for an autónomo, `individual` for a consumer.'
    CustomerInput:
      type: object
      required: [name, kind]
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
          description: Send it to update that customer instead of creating one.
        name: {type: string, minLength: 1, maxLength: 300}
        kind: {$ref: '#/components/schemas/CustomerKind'}
        tax_id:
          type: string
          description: NIF or CIF. Required unless `kind` is `individual`. One customer per tax ID and country.
        country:
          type: string
          default: ES
          description: ISO 3166-1 alpha-2. Melso invoices customers in Spain (`ES`) today.
        address: {type: string}
        email:
          type: string
          format: email
          description: Where the issued invoice is emailed.
    Customer:
      type: object
      required: [id, name, kind, country, created_at]
      properties:
        id: {type: string, format: uuid}
        name: {type: string}
        tax_id: {type: string}
        country: {type: string}
        address: {type: string}
        email: {type: string}
        kind: {$ref: '#/components/schemas/CustomerKind'}
        created_at: {type: string, format: date-time}

    # Invoices
    InvoiceStatus:
      type: string
      enum: [draft, issued, sent, paid]
      description: '`draft` until a person issues it. `issued` once numbered; `sent` once emailed; `paid` once marked paid.'
    InvoiceLineInput:
      type: object
      required: [description, quantity, unit_price, vat_rate]
      additionalProperties: false
      properties:
        description: {type: string, minLength: 1, maxLength: 1000}
        quantity:
          $ref: '#/components/schemas/Decimal'
          description: Greater than zero.
        unit_price:
          $ref: '#/components/schemas/Decimal'
          description: Negative only on a corrective invoice.
        vat_rate:
          type: string
          enum: ['21', '10', '4', '0', '7']
          description: |
            VAT rate in percent: `21`, `10`, `4` or `0`. A rate of `0` needs the
            legal basis in `notes`. A company in the Canary Islands uses IGIC `7`.
        product_kind:
          type: string
          enum: [goods, services]
    InvoiceDraftInput:
      type: object
      required: [customer_id, issue_date, lines]
      additionalProperties: false
      properties:
        type:
          type: string
          enum: [standard, proforma]
          default: standard
        customer_id: {type: string, format: uuid}
        issue_date: {type: string, format: date}
        due_date:
          type: string
          format: date
          description: On or after `issue_date`.
        lines:
          type: array
          minItems: 1
          maxItems: 200
          items: {$ref: '#/components/schemas/InvoiceLineInput'}
        irpf_rate:
          type: string
          enum: ['0', '7', '15']
          default: '0'
          description: IRPF withholding for an autónomo professional invoicing a business. A company always uses `0`.
        notes: {type: string, maxLength: 4000}
    InvoiceCorrectionInput:
      type: object
      required: [issue_date, lines, notes]
      additionalProperties: false
      properties:
        customer_id:
          type: string
          format: uuid
          description: Must be the original invoice's customer. Defaults to it.
        issue_date: {type: string, format: date}
        due_date: {type: string, format: date}
        lines:
          type: array
          minItems: 1
          maxItems: 200
          items: {$ref: '#/components/schemas/InvoiceLineInput'}
        irpf_rate:
          type: string
          enum: ['0', '7', '15']
        notes:
          type: string
          maxLength: 4000
          description: The reason for the correction.
    InvoiceLine:
      type: object
      properties:
        description: {type: string}
        quantity: {$ref: '#/components/schemas/Decimal'}
        unit_price: {$ref: '#/components/schemas/Decimal'}
        vat_rate: {$ref: '#/components/schemas/Decimal'}
        product_kind:
          type: string
          enum: [goods, services]
        amount: {$ref: '#/components/schemas/Amount'}
    Invoice:
      type: object
      required: [id, version, type, status, issue_date, customer_id, lines, subtotal, vat_amount, irpf_amount, total, currency, created_at, updated_at]
      properties:
        id: {type: string, format: uuid}
        version:
          type: string
          description: A fingerprint of the invoice's fiscal content. Issuing requires the exact `version` the person reviewed.
        type:
          type: string
          enum: [standard, proforma, corrective]
        status: {$ref: '#/components/schemas/InvoiceStatus'}
        series:
          type: [string, 'null']
          description: Set at issue. The issue year, such as `2026`; `R-2026` for a corrective.
        number:
          type: [integer, 'null']
          description: Set at issue, the next in the series.
        issue_date: {type: string, format: date}
        due_date: {type: [string, 'null'], format: date}
        customer_id: {type: string, format: uuid}
        customer_name: {type: string, description: The customer's name when the invoice was drafted.}
        customer_tax_id: {type: string}
        lines:
          type: array
          items: {$ref: '#/components/schemas/InvoiceLine'}
        subtotal: {$ref: '#/components/schemas/Amount'}
        vat_amount: {$ref: '#/components/schemas/Amount'}
        irpf_rate: {$ref: '#/components/schemas/Decimal'}
        irpf_amount: {$ref: '#/components/schemas/Amount'}
        total: {$ref: '#/components/schemas/Amount'}
        currency: {type: string, const: EUR}
        tax_label:
          type: string
          enum: [IVA, IGIC]
        notes: {type: string}
        fiscal_network:
          type: string
          enum: [verifactu, ticketbai]
          description: Where the invoice is registered, from the company's tax territory.
        verifactu_status:
          type: string
          enum: [pending, registered, error]
          description: The registration state with VERI*FACTU or TicketBAI.
        pdf_ready: {type: boolean}
        payment_link: {type: [string, 'null'], format: uri}
        payment_link_status:
          type: string
          enum: [pending, creating, active, inactive, error, unknown]
        email_delivery_status:
          type: string
          enum: [pending, sending, sent, error, unknown]
        email_delivery_error: {type: [string, 'null']}
        sent_at: {type: [string, 'null'], format: date-time}
        paid_at: {type: [string, 'null'], format: date-time}
        corrects_invoice_id: {type: [string, 'null'], format: uuid}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    # Companies
    CompanyKind:
      type: string
      enum: [freelancer, company]
      description: '`freelancer` for an autónomo, `company` for an empresa.'
    Company:
      type: object
      required: [id, name, kind, onboarded, created_at]
      properties:
        id: {type: string, format: uuid}
        name: {type: string}
        kind: {$ref: '#/components/schemas/CompanyKind'}
        onboarded:
          type: boolean
          description: Whether the company's setup is finished.
        external_id: {type: [string, 'null'], description: Preview. Your ID for the company.}
        missing:
          type: array
          description: Preview. What the fiscal profile still needs, as in `tax_missing`.
          items: {type: string}
        setup:
          oneOf:
            - {$ref: '#/components/schemas/CompanySetup'}
            - {type: 'null'}
          description: Preview. The registration or incorporation Melso does for the company, when one was asked for.
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    CompanySetup:
      type: object
      required: [request, case_id, missing]
      properties:
        request:
          type: string
          enum: [register_autonomo, incorporate_sl]
        case_id:
          type: string
          format: uuid
          description: The setup case, `alta` or `constitucion_sl`. Follow it with Get a case and the `case.*` events.
        missing:
          type: array
          description: The details the case still needs before Melso can send it.
          items: {type: string}
        registered_at:
          type: [string, 'null']
          format: date-time
          description: When the company got its tax ID and became registered. Sent with `company.registered`.
    CompanyCreate:
      type: object
      required: [kind, name, owner]
      additionalProperties: false
      properties:
        kind: {$ref: '#/components/schemas/CompanyKind'}
        name: {type: string}
        external_id: {type: string}
        owner:
          type: object
          required: [name, email]
          description: The person who approves the company's invoices and returns.
          properties:
            name: {type: string}
            email: {type: string, format: email}
            phone: {type: string}
            locale:
              type: string
              enum: [es, ca, en]
        fiscal_profile: {$ref: '#/components/schemas/FiscalProfileInput'}
        setup_request:
          type: string
          enum: [none, register_autonomo, incorporate_sl]
          default: none
          description: |
            Ask Melso's professionals to register the autónomo
            (`register_autonomo`, a `freelancer`) or incorporate an S.L.
            (`incorporate_sl`, a `company`). A company created with
            `incorporate_sl` is a company in incorporation: it has only its
            `constitucion_sl` case until the Registro grants its legal name and
            tax ID.
        setup_details:
          description: |
            The setup case's details: `CaseDetailsAlta` for
            `register_autonomo`, `CaseDetailsConstitucionSL` for
            `incorporate_sl`. Send what you have; the hosted onboarding collects
            the rest.
          anyOf:
            - {$ref: '#/components/schemas/SetupDetailsAlta'}
            - {$ref: '#/components/schemas/SetupDetailsConstitucionSL'}
        setup_payer:
          type: string
          enum: [owner, partner]
          default: owner
          description: Who pays the setup case when its kind is sold as a one-off service, as `payer` on Open a case.
        services:
          type: array
          items:
            type: string
            enum: [invoicing, bookkeeping, tax_returns, payroll]
    TaxTerritory:
      type: string
      enum: [comun, alava, bizkaia, gipuzkoa, navarra, canarias, ceuta, melilla]
    FiscalProfileFields:
      type: object
      properties:
        tax_id:
          type: string
          description: DNI or NIE for an autónomo, CIF for an empresa, with a valid check character.
        legal_name: {type: string, maxLength: 300}
        trade_name: {type: string, maxLength: 300}
        address: {type: string, maxLength: 300}
        postal_code:
          type: string
          pattern: '^[0-9]{5}$'
          description: Fills in `province` and `tax_territory`.
        city: {type: string, maxLength: 300}
        province: {type: string, maxLength: 300}
        tax_territory: {$ref: '#/components/schemas/TaxTerritory'}
        vat_regime:
          type: string
          enum: [general, recargo_equivalencia, simplificado, reagp, exento, igic_general, igic_simplificado, igic_repep, ipsi]
          description: Must fit the tax territory.
        activity_type:
          type: string
          enum: [profesional, empresarial]
          description: An empresa is always `empresarial`.
        activity_description: {type: string, maxLength: 300}
        iae_code:
          type: string
          pattern: '^[0-9]{1,4}(\.[0-9]{1,2})?$'
        registration_status:
          type: string
          enum: [alta, pendiente]
          description: '`pendiente` while the autónomo is not registered or the S.L. not incorporated yet.'
        registered_on:
          type: string
          description: '`YYYY-MM` or `YYYY-MM-DD`, not in the future.'
        irpf_method:
          type: string
          enum: [directa_simplificada, directa_normal, modulos]
          description: Autónomos only.
        irpf_retention_rate:
          type: number
          minimum: 0
          maximum: 100
          description: Autónomos only.
        eu_operator: {type: boolean, description: Registered in the ROI for intra-community operations.}
        vat_sii: {type: boolean, description: Keeps its VAT books through the SII.}
        vat_redeme: {type: boolean, description: Registered for monthly VAT refunds.}
        is_large_company: {type: boolean, description: Gran empresa, filing VAT and withholdings monthly.}
        client_types:
          type: array
          minItems: 1
          items:
            type: string
            enum: [es_business, es_individual, eu_business, non_eu_business]
        invoice_series: {type: string, maxLength: 40}
        next_invoice_number: {type: integer, minimum: 1, maximum: 1000000000}
        iban: {type: string, description: Returned masked.}
    FiscalProfileInput:
      allOf:
        - $ref: '#/components/schemas/FiscalProfileFields'
        - type: object
          properties:
            clear:
              type: array
              description: Fields to empty.
              items: {type: string}
    FiscalProfileResult:
      type: object
      required: [profile, missing]
      properties:
        profile:
          allOf:
            - $ref: '#/components/schemas/FiscalProfileFields'
            - type: object
              properties:
                tax_id_type:
                  type: string
                  enum: [dni, nie, cif]
        errors:
          type: object
          additionalProperties: {type: string}
          description: The fields not saved, each with the reason.
        deduced:
          type: object
          additionalProperties: true
          description: Fields Melso filled in from others, such as the territory from the postal code.
        missing:
          type: array
          items: {type: string}
        invoice_missing:
          type: array
          description: What is still needed to invoice.
          items: {type: string}
        tax_missing:
          type: array
          description: What is still needed to calculate returns.
          items: {type: string}

    InvoicingStatus:
      type: object
      required: [status, invoicing_ready]
      properties:
        status:
          type: string
          enum: [pending, active, error]
        invoicing_ready:
          type: boolean
          description: The company can issue invoices.
        fiscal_network:
          type: string
          enum: [verifactu, ticketbai, '']
          description: Empty while the tax territory is unknown.
        fiscal_missing:
          type: array
          description: Fiscal profile fields still missing.
          items: {type: string}
        invoicing_missing:
          type: array
          description: Invoicing fields still missing, such as `vat_regime` or `city`.
          items: {type: string}
        authorization_url:
          type: [string, 'null']
          format: uri
          description: While `pending`, where the owner authorizes registration. Valid for 24 hours.
        error: {type: [string, 'null']}

    # Expenses
    VatTreatment:
      type: string
      enum: [taxable, exempt, reverse_charge, not_subject, import]
      description: '`reverse_charge` (inversión del sujeto pasivo) and `exempt` or `not_subject` carry no VAT amount.'
    VatLine:
      type: object
      required: [rate, base, vat]
      properties:
        rate: {$ref: '#/components/schemas/Decimal'}
        base: {$ref: '#/components/schemas/Amount'}
        vat: {$ref: '#/components/schemas/Amount'}
    ExpenseCategory:
      type: string
      pattern: '^G[0-9]{2}$'
      description: The AEAT expense concept, from `G01` to `G48`, such as `G03` (rent) or `G05` (company Social Security).
    Deductible:
      type: string
      enum: ['yes', 'no', unsure]
    ExpenseFields:
      type: object
      properties:
        supplier_name: {type: string, minLength: 1, maxLength: 200}
        supplier_tax_id: {type: string}
        supplier_country: {type: string, default: ES}
        document_number: {type: string, maxLength: 100}
        expense_date: {type: string, format: date}
        operation_date: {type: string, format: date}
        description: {type: string, maxLength: 500}
        subtotal: {$ref: '#/components/schemas/Amount'}
        vat_rate: {$ref: '#/components/schemas/Decimal'}
        vat_amount: {$ref: '#/components/schemas/Amount'}
        total: {$ref: '#/components/schemas/Amount'}
        irpf_rate: {$ref: '#/components/schemas/Decimal'}
        irpf_amount: {$ref: '#/components/schemas/Amount'}
        vat_treatment: {$ref: '#/components/schemas/VatTreatment'}
        category: {$ref: '#/components/schemas/ExpenseCategory'}
        account:
          type: string
          description: The PGC account to book it to, such as `621` or `629`, for an empresa. It sets the category.
        deductible: {$ref: '#/components/schemas/Deductible'}
        file_id:
          type: string
          format: uuid
          description: The document in Documents that supports it.
    ExpenseInput:
      allOf:
        - $ref: '#/components/schemas/ExpenseFields'
        - type: object
          required: [supplier_name, expense_date, subtotal, vat_amount, total]
    ExpensePatch:
      allOf:
        - $ref: '#/components/schemas/ExpenseFields'
        - type: object
          required: [expected_version]
          properties:
            expected_version: {type: integer, minimum: 1}
    Expense:
      allOf:
        - $ref: '#/components/schemas/ExpenseFields'
        - type: object
          required: [id, supplier_name, expense_date, subtotal, vat_amount, total, deductible, version, created_at]
          properties:
            id: {type: string, format: uuid}
            vat_lines:
              type: array
              items: {$ref: '#/components/schemas/VatLine'}
            items:
              type: array
              items:
                type: object
                properties:
                  description: {type: string}
                  quantity: {$ref: '#/components/schemas/Decimal'}
                  unit_price: {$ref: '#/components/schemas/Decimal'}
                  amount: {$ref: '#/components/schemas/Amount'}
            category_proposed:
              type: boolean
              description: The category is Melso's proposal, not yet confirmed.
            booked_account: {type: string, description: The account it is booked to.}
            deductible_reason: {type: [string, 'null']}
            deductibility:
              type: object
              description: Melso's estimate of deductibility, and the rule it applied.
              properties:
                estimate:
                  type: string
                  enum: [likely, review, unlikely]
                rule: {type: string}
            source:
              type: string
              enum: [upload, whatsapp, gmail, melso, import]
            review_reasons:
              type: array
              items: {type: string}
            currency: {type: string}
            exchange_rate: {type: [string, 'null']}
            version: {type: integer}
            created_at: {type: string, format: date-time}
            updated_at: {type: string, format: date-time}

    # Income
    IncomeInput:
      type: object
      required: [customer_name, issue_date, vat_lines]
      additionalProperties: false
      properties:
        customer_name: {type: string, maxLength: 200}
        customer_tax_id: {type: string, maxLength: 32}
        customer_country: {type: string, default: ES}
        document_number: {type: string, maxLength: 100, description: Unique in the company.}
        issue_date: {type: string, format: date}
        operation_date: {type: string, format: date}
        description: {type: string, maxLength: 200}
        vat_lines:
          type: array
          minItems: 1
          maxItems: 10
          items: {$ref: '#/components/schemas/VatLine'}
          description: One line per VAT rate. A line with a decimal rate is recargo de equivalencia.
        vat_treatment:
          type: string
          enum: [taxable, exempt, reverse_charge, not_subject]
          default: taxable
        irpf_rate: {$ref: '#/components/schemas/Decimal'}
        irpf_amount: {$ref: '#/components/schemas/Amount'}
        file_id: {type: string, format: uuid}
    Income:
      allOf:
        - $ref: '#/components/schemas/IncomeInput'
        - type: object
          required: [id, customer_name, issue_date, subtotal, vat_amount, total, version, created_at]
          properties:
            id: {type: string, format: uuid}
            subtotal: {$ref: '#/components/schemas/Amount'}
            vat_amount: {$ref: '#/components/schemas/Amount'}
            total:
              $ref: '#/components/schemas/Amount'
              description: '`subtotal` plus `vat_amount`; IRPF withheld is not subtracted.'
            currency: {type: string}
            exchange_rate: {type: [string, 'null']}
            source:
              type: string
              enum: [upload, whatsapp, gmail, import]
            review_reasons:
              type: array
              items: {type: string}
            version: {type: integer}
            created_at: {type: string, format: date-time}
            updated_at: {type: string, format: date-time}

    # Documents
    StoredDocument:
      type: object
      required: [id, file_id, filename, content_type, size_bytes, created_at]
      properties:
        id: {type: string}
        file_id:
          type: string
          format: uuid
          description: The ID to use as `file_id` on an expense, an income or a case.
        filename: {type: string}
        content_type: {type: string}
        size_bytes: {type: integer}
        category:
          type: string
          enum: [expenses, contracts, receipts, all]
        created_at: {type: string, format: date-time}
    ExtractionResult:
      type: object
      required: [direction, record, review, duplicate]
      properties:
        direction:
          type: string
          enum: [income, expense]
        record:
          oneOf:
            - $ref: '#/components/schemas/Expense'
            - $ref: '#/components/schemas/Income'
        review:
          type: array
          description: What to check.
          items:
            type: string
            enum: [totals_mismatch, vat_rate_mismatch, missing_number, missing_counterparty_tax_id, converted_currency, ticket_vat_not_deductible, reverse_charge, possible_duplicate, recipient_tax_id_mismatch]
        duplicate: {type: boolean}

    # Banking
    BankAccount:
      type: object
      required: [id, name, currency, created_at]
      properties:
        id: {type: string, format: uuid}
        name: {type: string}
        iban: {type: [string, 'null']}
        currency: {type: string}
        external_id: {type: [string, 'null'], description: Preview.}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    BankMovementInput:
      type: object
      required: [external_id, booked_on, amount, concept]
      properties:
        external_id: {type: string, description: Your movement ID. Melso ignores a movement it already has.}
        booked_on: {type: string, format: date}
        value_on: {type: string, format: date}
        amount:
          $ref: '#/components/schemas/Amount'
          description: Positive for money in, negative for money out.
        currency: {type: string, default: EUR}
        concept: {type: string}
        counterparty_name: {type: string}
        counterparty_iban: {type: string}
        reference: {type: string}
        balance_after: {$ref: '#/components/schemas/Amount'}
    BankMovement:
      type: object
      required: [id, bank_account_id, source, booked_on, amount, currency, concept, created_at]
      properties:
        id: {type: string, format: uuid}
        bank_account_id: {type: string, format: uuid}
        source:
          type: string
          enum: [statement, connection]
        statement_id: {type: [string, 'null'], format: uuid}
        external_id: {type: [string, 'null']}
        booked_on: {type: string, format: date}
        value_on: {type: [string, 'null'], format: date}
        amount: {$ref: '#/components/schemas/Amount'}
        currency: {type: string}
        balance_after: {type: [string, 'null']}
        concept: {type: string}
        counterparty_name: {type: string}
        counterparty_iban: {type: [string, 'null']}
        reference: {type: string}
        reconciliation:
          type: [object, 'null']
          properties:
            id: {type: string, format: uuid}
            created_at: {type: string, format: date-time}
            documents:
              type: array
              items:
                type: object
                properties:
                  type:
                    type: string
                    enum: [expense, income, invoice]
                  id: {type: string, format: uuid}
                  amount: {$ref: '#/components/schemas/Amount'}
        proposal:
          type: [object, 'null']
          description: Melso's proposed match, for a movement not reconciled yet. It needs the exact outstanding amount.
          properties:
            document_type:
              type: string
              enum: [expense, income, invoice]
            document_id: {type: string, format: uuid}
            amount: {$ref: '#/components/schemas/Amount'}
            party: {type: string}
            number: {type: [string, 'null']}
            reasons:
              type: array
              items:
                type: string
                enum: [amount, reference, counterparty]
        created_at: {type: string, format: date-time}
    BankStatement:
      type: object
      properties:
        id: {type: string, format: uuid}
        bank_account_id: {type: string, format: uuid}
        format:
          type: string
          enum: [norma43, csv, xlsx, pdf]
        period_start: {type: string, format: date}
        period_end: {type: string, format: date}
        opening_balance: {$ref: '#/components/schemas/Amount'}
        closing_balance: {$ref: '#/components/schemas/Amount'}
        movements: {type: integer}
        created_at: {type: string, format: date-time}
    BankStatementUpload:
      type: object
      required: [file]
      properties:
        file:
          type: string
          format: binary
          description: Norma 43, CSV, XLSX or PDF, up to 5 MB. CSV and XLSX need a header row naming a date and an amount, or a debit and a credit column.
        bank_account_id:
          type: string
          format: uuid
          description: The account. Without it, Melso uses the saved account with the file's IBAN, or creates one.
        account_name: {type: string, maxLength: 120}
        opening_balance: {$ref: '#/components/schemas/Amount'}
        closing_balance: {$ref: '#/components/schemas/Amount'}
    BankStatementPreview:
      type: object
      properties:
        format:
          type: string
          enum: [norma43, csv, xlsx, pdf]
        iban: {type: [string, 'null']}
        currency: {type: string}
        period_start: {type: string, format: date}
        period_end: {type: string, format: date}
        opening_balance: {type: [string, 'null']}
        closing_balance: {type: [string, 'null']}
        movement_total: {$ref: '#/components/schemas/Amount'}
        difference:
          type: [string, 'null']
          description: How far the balances are from the movements.
        bank_account:
          oneOf:
            - $ref: '#/components/schemas/BankAccount'
            - type: 'null'
        movements:
          type: array
          items:
            type: object
            properties:
              line_number: {type: integer}
              booked_on: {type: string, format: date}
              value_on: {type: [string, 'null'], format: date}
              amount: {$ref: '#/components/schemas/Amount'}
              balance_after: {type: [string, 'null']}
              concept: {type: string}
              counterparty_name: {type: string}
              reference: {type: string}
              duplicate: {type: boolean}
        new_movements: {type: integer}
        duplicates: {type: integer}
        questions:
          type: array
          description: What a person must answer before saving.
          items:
            type: string
            enum: [account, balances]
        can_save: {type: boolean}

    # Accounting
    JournalEntry:
      type: object
      properties:
        id: {type: string, format: uuid}
        entry_date: {type: string, format: date}
        number: {type: [string, 'null']}
        concept: {type: string}
        kind:
          type: string
          enum: [normal, opening, reopening, regularization, closing]
        source_type:
          type: string
          enum: [expense, income, invoice, reconciliation, depreciation, import]
        source_id: {type: string, format: uuid}
        lines:
          type: array
          items:
            type: object
            properties:
              account: {type: string}
              account_name: {type: [string, 'null']}
              concept: {type: [string, 'null']}
              debit: {$ref: '#/components/schemas/Amount'}
              credit: {$ref: '#/components/schemas/Amount'}
    JournalImport:
      type: object
      properties:
        id: {type: string, format: uuid}
        filename: {type: string}
        format:
          type: string
          enum: [csv, xlsx]
        first_date: {type: string, format: date}
        last_date: {type: string, format: date}
        entries: {type: integer}
        lines: {type: integer}
        created_at: {type: string, format: date-time}
    VatBookUpload:
      type: object
      required: [file, direction]
      properties:
        file: {type: string, format: binary}
        direction:
          type: string
          enum: [issued, received]
          description: '`issued` records income; `received` records expenses.'
    VatBookImport:
      type: object
      properties:
        id: {type: string, format: uuid}
        direction:
          type: string
          enum: [issued, received]
        filename: {type: string}
        format: {type: string}
        first_date: {type: string, format: date}
        last_date: {type: string, format: date}
        invoices: {type: integer}
        duplicates: {type: integer}
        base_total: {$ref: '#/components/schemas/Amount'}
        vat_total: {$ref: '#/components/schemas/Amount'}
        created_at: {type: string, format: date-time}
    ImportPreview:
      type: object
      description: What an import file holds, before saving it.
      properties:
        filename: {type: string}
        format: {type: string}
        first_date: {type: string, format: date}
        last_date: {type: string, format: date}
        problems:
          type: array
          items: {type: string}
        can_save: {type: boolean}
      additionalProperties: true
    TrialBalance:
      type: object
      properties:
        year: {type: integer}
        from: {type: string, format: date}
        to: {type: string, format: date}
        accounts:
          type: array
          items:
            type: object
            properties:
              account: {type: string}
              name: {type: string}
              opening: {$ref: '#/components/schemas/Amount'}
              debit: {$ref: '#/components/schemas/Amount'}
              credit: {$ref: '#/components/schemas/Amount'}
              closing: {$ref: '#/components/schemas/Amount'}
        debit: {$ref: '#/components/schemas/Amount'}
        credit: {$ref: '#/components/schemas/Amount'}
    StatementLine:
      type: object
      properties:
        key: {type: string, description: The line's key in the PGC model, as the modelo 200 files it.}
        label: {type: string}
        level: {type: integer}
        amount: {$ref: '#/components/schemas/Amount'}
        compare_amount: {type: [string, 'null']}
        accounts:
          type: array
          items:
            type: object
            properties:
              account: {type: string}
              name: {type: string}
              amount: {$ref: '#/components/schemas/Amount'}
    ProfitAndLoss:
      type: object
      properties:
        from: {type: string, format: date}
        to: {type: string, format: date}
        result: {$ref: '#/components/schemas/Amount'}
        compare_result: {type: [string, 'null']}
        lines:
          type: array
          items: {$ref: '#/components/schemas/StatementLine'}
    BalanceSheet:
      type: object
      properties:
        date: {type: string, format: date}
        compare_date: {type: [string, 'null'], format: date}
        assets:
          type: array
          items: {$ref: '#/components/schemas/StatementLine'}
        equity_liabilities:
          type: array
          items: {$ref: '#/components/schemas/StatementLine'}
        total_assets: {$ref: '#/components/schemas/Amount'}
        total_equity_liabilities: {$ref: '#/components/schemas/Amount'}

    # Taxes
    TaxCalendar:
      type: object
      required: [today, obligations]
      properties:
        today: {type: string, format: date}
        obligations:
          type: array
          items: {$ref: '#/components/schemas/TaxObligation'}
    TaxObligation:
      type: object
      required: [obligation_id, model, name, administration, periodicity, period, opens, deadline, melso]
      properties:
        obligation_id:
          type: string
          description: The calendar rule, such as `303` or `303-simplificado`.
        model: {type: string, description: The form, such as `303`.}
        name: {type: string, description: The form's name in Spanish.}
        administration: {$ref: '#/components/schemas/Administration'}
        periodicity:
          type: string
          enum: [monthly, quarterly, annual, instalments]
        period: {$ref: '#/components/schemas/Period'}
        opens: {type: string, format: date, description: First day the form can be filed.}
        deadline: {type: string, format: date, description: Last day to file, moved for public holidays.}
        direct_debit_deadline:
          type: [string, 'null']
          format: date
          description: Last day to file with payment by direct debit (domiciliación).
        melso:
          type: string
          enum: [calculates, reminds]
          description: '`calculates`: Melso calculates the return. `reminds`: Melso only reminds; `sources` links to the official instructions.'
        sources:
          type: array
          items: {type: string, format: uri}
        filing_id:
          type: [string, 'null']
          format: uuid
          description: The return for this form and period, once one exists.
        filing_status:
          oneOf:
            - $ref: '#/components/schemas/TaxReturnStatus'
            - type: 'null'
    TaxReturnStatus:
      type: string
      enum: [draft, approved, filed]
      description: '`draft`: calculated, still recalculated from the records. `approved`: the owner authorized this exact version. `filed`: filed, with the administration''s reference and receipt when Melso filed it.'
    TaxReturnBox:
      type: object
      properties:
        box:
          type: string
          description: The form's box number, such as `27`. Empty for an internal working line.
        key: {type: string, description: What the box holds.}
        amount: {$ref: '#/components/schemas/Amount'}
    TaxReturnRecord:
      type: object
      description: One line of an informative return (190, 180, 347, 349).
      properties:
        tax_id: {type: string}
        name: {type: string}
        country: {type: string}
        key: {type: string, description: 'The record key: A or B for the 347, I or S for the 349, A or G for the 190.'}
        amount: {$ref: '#/components/schemas/Amount'}
        withheld: {$ref: '#/components/schemas/Amount'}
        quarters:
          type: array
          items: {$ref: '#/components/schemas/Amount'}
          maxItems: 4
        missing:
          type: array
          items:
            type: string
            enum: [tax_id, cadastral_reference]
    TaxReturn:
      type: object
      required: [id, model, period, status, amount, is_estimate]
      properties:
        id:
          type: string
          description: The return's UUID, or `estimate:<model>:<period>` for an estimate.
        model:
          type: string
          description: The form. Melso calculates `303`, `130`, `111`, `115`, `190`, `180`, `347`, `349` and `390` today; a reminder form marked filed has its own model.
        period: {$ref: '#/components/schemas/Period'}
        status: {$ref: '#/components/schemas/TaxReturnStatus'}
        is_estimate:
          type: boolean
          description: An estimate has an amount only. It has no boxes, no version and no approval.
        amount:
          $ref: '#/components/schemas/Amount'
          description: The result. Negative to offset or refund (303); `0.00` for an informative return.
        version:
          type: [integer, 'null']
          description: Raised on every recalculation that changes the draft. Approval needs the exact version.
        approved_version: {type: [integer, 'null']}
        approved_at: {type: [string, 'null'], format: date-time}
        deadline: {type: string, format: date}
        direct_debit_deadline: {type: [string, 'null'], format: date}
        boxes:
          type: array
          items: {$ref: '#/components/schemas/TaxReturnBox'}
        records:
          type: array
          items: {$ref: '#/components/schemas/TaxReturnRecord'}
        review:
          type: array
          description: What to check before approving.
          items:
            type: string
            enum: [missing_tax_id, cadastral_reference, eu_key_assumed, rates_assumed, volume_split]
        unconfirmed_expenses:
          type: integer
          description: Expenses in the return that are not yet confirmed as deductible.
        filed_at: {type: [string, 'null'], format: date-time}
        filing:
          description: Preview. The administration's evidence, when Melso filed the return.
          oneOf:
            - $ref: '#/components/schemas/FilingReceipt'
            - type: 'null'
        recalculated:
          type: [object, 'null']
          description: For a filed return whose records changed afterwards, what it would be today. A difference may need a complementary or corrective return.
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    FilingReceipt:
      type: object
      required: [filed_at, reference]
      properties:
        filed_at: {type: string, format: date-time}
        reference:
          type: string
          description: The administration's filing reference (número de justificante).
        csv:
          type: string
          description: The código seguro de verificación printed on the receipt.
        receipt: {$ref: '#/components/schemas/Document'}
        filed_by:
          type: string
          enum: [melso, company]
    AttestedApprovalInput:
      type: object
      required: [expected_version, approver, evidence]
      additionalProperties: false
      properties:
        expected_version: {type: integer, minimum: 1}
        approver:
          type: object
          required: [name, email, role]
          properties:
            name: {type: string}
            email: {type: string, format: email}
            role:
              type: string
              enum: [owner, legal_representative]
            tax_id:
              type: string
              description: The person's DNI or NIE.
            external_user_id:
              type: string
              description: The person's ID in your product.
        evidence:
          type: object
          required: [approved_at, ip_address, shown_document_sha256]
          properties:
            approved_at: {type: string, format: date-time}
            ip_address: {type: string}
            user_agent: {type: string}
            authentication:
              type: string
              enum: [strong_customer_authentication, password, other]
              description: How your product authenticated the person for this approval. The partner agreement says which methods are accepted.
            shown_document_sha256:
              type: string
              description: SHA-256 of the document as you rendered it to the person, from the return's preview document or the case version's draft.
    TaxEstimate:
      type: object
      required: [period, as_of, set_aside_total, items]
      properties:
        period: {$ref: '#/components/schemas/Period'}
        as_of: {type: string, format: date-time}
        set_aside_total:
          $ref: '#/components/schemas/Amount'
          description: The sum to keep aside for the returns that have a payment. Refunds and offsets do not reduce it.
        items:
          type: array
          items:
            type: object
            properties:
              model: {type: string}
              period: {$ref: '#/components/schemas/Period'}
              amount: {$ref: '#/components/schemas/Amount'}
              deadline: {type: string, format: date}
              basis:
                type: string
                enum: [records_to_date, projected_quarter]
        review:
          type: array
          description: What makes the estimate less certain.
          items: {type: string}

    # Corporate tax
    CorporateTaxInput:
      type: object
      required: [expected_version]
      additionalProperties: false
      properties:
        expected_version:
          type: integer
          minimum: 0
          description: The `version` you read; `0` when nothing was saved yet.
        adjustments:
          type: array
          maxItems: 50
          description: Every adjustment to the accounting result (ajustes extracontables). Replaces the saved list.
          items:
            type: object
            required: [concept, amount]
            properties:
              concept: {type: string, description: 'What it is and the article it rests on, such as "Multa (art. 15.c LIS)".'}
              amount:
                $ref: '#/components/schemas/Amount'
                description: Positive raises the taxable base, negative lowers it.
              temporary: {type: boolean, description: It reverses in later years, with a deferred tax.}
        capitalization_reserve: {type: boolean, description: Apply the capitalization reserve (art. 25 LIS).}
        deductions: {$ref: '#/components/schemas/Amount'}
        instalments:
          type: string
          description: The year's 202 instalments paid. Empty to take them from the books.
        losses_offset:
          type: string
          description: Negative bases of earlier years offset.
        new_entity: {type: boolean, description: A company in its first or second year with a positive base, at 15 %.}
        period_base: {type: boolean, description: The company opted for the 202 on the year's result so far (art. 40.3 LIS).}
        prior_turnover:
          type: string
          description: The year before's turnover, which sets the rate. Empty to take it from the books.
        withholdings:
          type: string
          description: Withholdings suffered. Empty to take them from the books.
        workforce_growth: {type: integer, description: Growth of the average workforce over the year before, in percent.}
    CorporateTax:
      type: object
      properties:
        year: {type: integer}
        version: {type: integer}
        result_before_tax: {$ref: '#/components/schemas/Amount'}
        adjustments_total: {$ref: '#/components/schemas/Amount'}
        capitalization_reserve: {$ref: '#/components/schemas/Amount'}
        losses_offset: {$ref: '#/components/schemas/Amount'}
        taxable_base: {$ref: '#/components/schemas/Amount'}
        rate: {$ref: '#/components/schemas/Decimal'}
        gross_tax: {$ref: '#/components/schemas/Amount'}
        net_tax: {$ref: '#/components/schemas/Amount'}
        withholdings: {$ref: '#/components/schemas/Amount'}
        instalments_paid: {$ref: '#/components/schemas/Amount'}
        to_pay: {$ref: '#/components/schemas/Amount'}
        instalments:
          type: array
          description: The modelo 202 instalments of the next year that build on this result.
          items:
            type: object
            properties:
              period: {$ref: '#/components/schemas/Period'}
              amount: {$ref: '#/components/schemas/Amount'}
              deadline: {type: string, format: date}
        review:
          type: array
          items: {type: string}
      additionalProperties: true
    # Cases
    CaseKindId:
      type: string
      enum: [modelo, alta, renta, constitucion_sl]
      description: |
        `modelo`: prepare and file one AEAT form for one period (details
        `model`, `period`); an empresa cannot ask for the 130, 131 or 100.
        `alta`: register an autónomo with the AEAT (036) and the Social
        Security (RETA), details in `CaseDetailsAlta`; it has no draft to
        approve. `renta`: an autónomo's annual income tax return, modelo 100
        (details `year`, `joint`). `constitucion_sl`: incorporate an S.L. or
        S.L.U., details in `CaseDetailsConstitucionSL`, paid once before Melso
        starts; the owner approves the deed draft. It opens only in a company
        in incorporation, which holds nothing else until the S.L. is
        registered. More kinds are added over time; read them from List case
        kinds.
    CaseKind:
      type: object
      required: [kind, name, details_schema, approval, payment]
      properties:
        kind: {$ref: '#/components/schemas/CaseKindId'}
        name: {type: string}
        description: {type: string}
        details_schema:
          type: object
          description: The JSON Schema `details` must match on Open a case.
        approval:
          type: boolean
          description: Whether the company approves a professional's draft before Melso files it.
        payment:
          type: object
          properties:
            mode:
              type: string
              enum: [plan, service]
              description: '`plan`: included when `covered`. `service`: a one-off price the owner pays before Melso starts.'
            covered: {type: boolean}
            amount_cents: {type: [integer, 'null'], description: The service price in euro cents.}
    CaseStatus:
      type: string
      enum: [draft, review, info, approval, filing, done, cancelled]
      description: |
        `draft`: not sent yet. `review`: Melso's professionals are working on
        it. `info`: they asked the company for something
        (`information_request`). `approval`: a draft waits for the company's
        approval. `filing`: approved, being filed. `done`: filed, with the
        administration's `external_reference`. `cancelled`: withdrawn before
        filing.
    CaseCreate:
      type: object
      required: [kind]
      additionalProperties: false
      properties:
        kind: {$ref: '#/components/schemas/CaseKindId'}
        details:
          type: object
          description: The kind's details, as its `details_schema` describes them.
        note: {type: string, maxLength: 4000}
        file_ids:
          type: array
          maxItems: 20
          items: {type: string, format: uuid}
        submit:
          type: boolean
          default: false
          description: Send it to Melso at once when the plan covers it.
        payer:
          type: string
          enum: [owner, partner]
          default: owner
          description: |
            Preview. Who pays a kind sold as a one-off service. `owner`: the
            case carries `payment_url` and waits for the owner. `partner`: Melso
            charges your partner account under your agreement and sends the
            case at once.
    CaseDetailsAlta:
      allOf:
        - {$ref: '#/components/schemas/SetupDetailsAlta'}
        - required: [start_date]
    SetupDetailsAlta:
      type: object
      additionalProperties: false
      description: |
        The details of an `alta`. `start_date` and `activity` are the live
        contract; the other fields are preview and, until then, go in the
        case's `note`. The tax ID, name, address and activity type come from the
        company's fiscal profile, and phone and email from its owner.
      properties:
        start_date:
          type: string
          format: date
          description: The planned start of the activity. Not in the past when the case is sent.
        activity: {type: string, maxLength: 200, description: 'The activity, in the person''s words.'}
        birth_date: {type: string, format: date}
        nuss:
          type: string
          pattern: '^[0-9]{12}$'
          description: Social Security number (NUSS). Ask Melso for one in the `note` when the person has none.
        activity_address:
          type: string
          maxLength: 300
          description: Where the activity happens, only when it is not the fiscal address.
        clients:
          type: string
          maxLength: 300
          description: Who they expect to invoice, which decides the VAT registrations (ROI for EU clients).
        tarifa_plana:
          type: string
          enum: ['si', 'no', no_se]
          description: Whether they want the reduced flat-rate contribution if they qualify.
        iban:
          type: string
          description: Account the RETA contribution is charged to.
    CaseDetailsConstitucionSL:
      allOf:
        - {$ref: '#/components/schemas/SetupDetailsConstitucionSL'}
        - required: [name_ending, names, activity, capital, partners, registered_office]
          properties:
            partners:
              items:
                required: [type, name, share_percent]
            registered_office:
              required: [street, number, postal_code, municipality, province]
    SetupDetailsConstitucionSL:
      type: object
      additionalProperties: false
      description: |
        The details of a `constitucion_sl`. Melso reserves the name at the
        Registro Mercantil Central, drafts the bylaws and the deed, arranges the
        notary signing, registers the company with the Registro Mercantil and
        obtains its tax ID and census registration. Notary and Registro
        Mercantil fees, the name reservation and the BORME publication are
        included; the share capital, digital certificates and the partners'
        powers of attorney are not.
      properties:
        name_ending:
          type: string
          enum: [sl, sociedad_limitada]
          description: '`sl` ends the name in "S.L.", `sociedad_limitada` in "Sociedad Limitada".'
        names:
          type: array
          minItems: 1
          maxItems: 5
          description: Candidate names in order of preference, without the ending. The first one free at the Registro Mercantil Central is requested.
          items: {type: string, minLength: 1, maxLength: 120}
        activity:
          type: string
          minLength: 3
          maxLength: 1000
          description: What the company will do, in the person's words. A professional derives the official activity codes.
        capital:
          type: integer
          minimum: 1
          maximum: 100000000
          default: 3000
          description: Share capital in whole euros.
        share_value:
          type: integer
          minimum: 1
          maximum: 100000000
          default: 1
          description: Nominal value of each share in whole euros. `capital` must be a multiple of it.
        partners:
          type: array
          minItems: 1
          maxItems: 20
          description: Every partner with their stake. The stakes add up to exactly 100.
          items:
            type: object
            additionalProperties: false
            properties:
              type:
                type: string
                enum: [person, company]
              name: {type: string, minLength: 1, maxLength: 200, description: 'A person''s full name, or a company''s legal name.'}
              tax_id:
                type: string
                maxLength: 20
                pattern: '^$|^[0-9A-Za-z]{8,12}$'
                description: DNI, NIE or company tax ID. Required for a company partner.
              representative: {type: string, maxLength: 200, description: For a company partner, the person who signs for it.}
              share_percent: {type: number, minimum: 0.01, maximum: 100}
              administrator: {type: boolean, description: Whether this partner will be an administrator.}
              has_certificate: {type: boolean, description: Whether this person, or the company's representative, already has a digital certificate.}
        registered_office:
          type: object
          additionalProperties: false
          description: The registered office in Spain.
          properties:
            street_type: {type: string, maxLength: 40, description: Calle, avenida, plaza…}
            street: {type: string, minLength: 1, maxLength: 200}
            number: {type: string, minLength: 1, maxLength: 20}
            floor: {type: string, maxLength: 20}
            door: {type: string, maxLength: 20}
            postal_code: {type: string, pattern: '^[0-9]{5}$'}
            municipality: {type: string, minLength: 1, maxLength: 100}
            province: {type: string, minLength: 1, maxLength: 100}
    Case:
      type: object
      required: [id, kind, status, created_at]
      properties:
        id: {type: string, format: uuid}
        kind: {$ref: '#/components/schemas/CaseKindId'}
        status: {$ref: '#/components/schemas/CaseStatus'}
        deadline: {type: [string, 'null'], format: date}
        current_version: {type: [integer, 'null']}
        approved_version: {type: [integer, 'null']}
        approvals:
          type: array
          description: |
            Preview. Who approved which version, and how.
          items:
            type: object
            required: [version, approver, method, approved_at]
            properties:
              version: {type: integer}
              approver:
                type: object
                properties:
                  name: {type: string}
                  role:
                    type: string
                    enum: [owner, legal_representative]
                  tax_id: {type: string}
              method:
                type: string
                enum: [melso, attested]
                description: '`melso`: on Melso''s approval page. `attested`: attested by your partner account.'
              approved_at: {type: string, format: date-time}
        payment_url:
          type: [string, 'null']
          format: uri
          description: Where the owner pays a service kind before Melso starts.
        information_request:
          type: [object, 'null']
          description: What the professionals need from the company, with the items to answer.
          properties:
            id: {type: string, format: uuid}
            items:
              type: array
              items:
                type: object
                properties:
                  id: {type: string}
                  kind:
                    type: string
                    enum: [document, question]
                  label: {type: string}
        messages:
          type: array
          items:
            type: object
            properties:
              author:
                type: string
                enum: [company, melso]
              content: {type: string}
              created_at: {type: string, format: date-time}
        documents:
          type: array
          description: The professionals' drafts, the company's documents and the filing receipts.
          items: {$ref: '#/components/schemas/Document'}
        external_reference:
          type: [string, 'null']
          description: The administration's reference for what was filed. Always present once `done`.
        completed_on: {type: [string, 'null'], format: date}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    # Administration notifications and certificates
    AdministrationNotification:
      type: object
      required: [id, administration, subject, made_available_at, status]
      properties:
        id: {type: string, format: uuid}
        administration: {$ref: '#/components/schemas/Administration'}
        channel:
          type: string
          enum: [dehu, aeat_sede, tgss_sede, dgt_dev]
          description: Where it was made available.
        subject: {type: string}
        procedure: {type: [string, 'null'], description: 'The procedure it belongs to, such as a requerimiento or a propuesta de liquidación.'}
        made_available_at: {type: string, format: date-time}
        accessed_at: {type: [string, 'null'], format: date-time}
        deemed_received_on:
          type: string
          format: date
          description: When it counts as received, on access or ten calendar days after being made available.
        status:
          type: string
          enum: [available, accessed, deemed_received]
        summary:
          type: [string, 'null']
          description: Melso's plain-language summary of what it asks and by when.
        response_deadline: {type: [string, 'null'], format: date}
        documents:
          type: array
          items: {$ref: '#/components/schemas/Document'}
        case_id:
          type: [string, 'null']
          format: uuid
          description: The case Melso opened to answer it.
    CertificateType:
      type: string
      enum: [aeat_tax_compliance, tgss_compliance, aeat_census_status, aeat_vat_registration]
      description: '`aeat_tax_compliance`: up to date with tax obligations. `tgss_compliance`: up to date with the Social Security. `aeat_census_status`: census situation. `aeat_vat_registration`: registration in the register of exporters and intra-community operators (ROI).'
    Certificate:
      type: object
      required: [id, type, status, requested_at]
      properties:
        id: {type: string, format: uuid}
        type: {$ref: '#/components/schemas/CertificateType'}
        status:
          type: string
          enum: [pending, issued, negative, failed]
          description: '`negative`: the administration issued it as not up to date; `findings` says why.'
        csv: {type: [string, 'null'], description: The código seguro de verificación.}
        issued_at: {type: [string, 'null'], format: date-time}
        findings:
          type: array
          items: {type: string}
        document:
          oneOf:
            - $ref: '#/components/schemas/Document'
            - type: 'null'
        requested_at: {type: string, format: date-time}

    # Employees
    EmploymentData:
      type: object
      description: Preview. What the TGSS and the SEPE need about the job.
      properties:
        contribution_account:
          type: string
          pattern: '^[0-9]{11}$'
          description: The company's código cuenta de cotización (CCC) for this workplace.
        contract_type:
          type: string
          description: The contract key, such as `100` (permanent, full time) or `402` (temporary, full time).
        working_time_coefficient:
          type: integer
          minimum: 1
          maximum: 1000
          description: Part-time coefficient per thousand. `1000` is full time.
        contribution_group:
          type: integer
          minimum: 1
          maximum: 11
          description: The grupo de cotización.
        occupation_code: {type: string, description: The CNO-11 occupation code.}
        collective_agreement: {type: string, description: The código de convenio colectivo.}
    EmployeeInput:
      type: object
      required: [name, start_date]
      additionalProperties: false
      properties:
        name: {type: string, minLength: 1, maxLength: 200}
        start_date: {type: string, format: date}
        details:
          type: object
          description: Free-form notes for Melso's team, such as the job and salary agreed.
        external_id: {type: string, description: Preview. Your ID for the employee.}
        tax_id: {type: string, description: Preview. DNI or NIE.}
        social_security_number: {type: string, description: Preview. The número de afiliación (NAF), 12 digits.}
        birth_date: {type: string, format: date, description: Preview.}
        employment: {$ref: '#/components/schemas/EmploymentData'}
    EmployeePatch:
      type: object
      additionalProperties: false
      properties:
        name: {type: string, maxLength: 200}
        start_date: {type: string, format: date}
        details: {type: object, description: Replaces the whole object.}
        active: {type: boolean}
        external_id: {type: string, description: Preview.}
        tax_id: {type: string, description: Preview.}
        social_security_number: {type: string, description: Preview.}
        birth_date: {type: string, format: date, description: Preview.}
        employment: {$ref: '#/components/schemas/EmploymentData'}
    Employee:
      type: object
      required: [id, name, start_date, details, active, created_at, updated_at]
      properties:
        id: {type: string, format: uuid}
        name: {type: string}
        start_date: {type: string, format: date}
        details: {type: object}
        active: {type: boolean}
        external_id: {type: [string, 'null'], description: Preview.}
        tax_id: {type: [string, 'null'], description: Preview.}
        social_security_number: {type: [string, 'null'], description: Preview.}
        birth_date: {type: [string, 'null'], format: date, description: Preview.}
        employment: {$ref: '#/components/schemas/EmploymentData'}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    EmployeeChange:
      type: object
      properties:
        id: {type: string, format: uuid}
        employee_id: {type: string, format: uuid}
        before: {$ref: '#/components/schemas/Employee'}
        after: {$ref: '#/components/schemas/Employee'}
        created_at: {type: string, format: date-time}

    # Payroll runs
    PayslipInput:
      type: object
      required: [employee_id, days_contributed, gross_pay, irpf, social_security]
      additionalProperties: false
      properties:
        employee_id: {type: string, format: uuid}
        days_contributed: {type: integer, minimum: 0, maximum: 31}
        gross_pay:
          $ref: '#/components/schemas/Amount'
          description: Cash pay before deductions (percepciones dinerarias).
        in_kind_pay:
          $ref: '#/components/schemas/Amount'
          description: Pay in kind at its tax value (retribuciones en especie).
        irpf:
          type: object
          required: [base, rate, withheld]
          properties:
            base: {$ref: '#/components/schemas/Amount'}
            rate: {$ref: '#/components/schemas/Decimal'}
            withheld:
              $ref: '#/components/schemas/Amount'
              description: IRPF withheld on cash pay.
            payment_on_account:
              $ref: '#/components/schemas/Amount'
              description: Ingreso a cuenta on pay in kind.
        social_security:
          type: object
          required: [base_common_contingencies, base_professional_contingencies, employee_contribution]
          properties:
            base_common_contingencies: {$ref: '#/components/schemas/Amount'}
            base_professional_contingencies: {$ref: '#/components/schemas/Amount'}
            overtime_base: {$ref: '#/components/schemas/Amount'}
            employee_contribution:
              $ref: '#/components/schemas/Amount'
              description: The employee's share you deducted.
        concepts:
          type: array
          description: The payslip's concepts, used for the CRA (comunicación de conceptos retributivos abonados).
          items:
            type: object
            required: [code, amount, kind]
            properties:
              code: {type: string}
              description: {type: string}
              amount: {$ref: '#/components/schemas/Amount'}
              kind:
                type: string
                enum: [salary, non_salary, in_kind, severance]
    PayrollRunInput:
      type: object
      required: [period, kind, payment_date, payslips]
      additionalProperties: false
      properties:
        period:
          type: string
          pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$'
          description: The month the pay accrues to, such as `2026-10`.
        kind:
          type: string
          enum: [ordinary, extra, supplementary]
          description: '`extra` for a paga extraordinaria paid apart; `supplementary` for arrears or a correction of an earlier month.'
        payment_date: {type: string, format: date}
        external_id: {type: string, description: Your ID for the run.}
        payslips:
          type: array
          minItems: 1
          items: {$ref: '#/components/schemas/PayslipInput'}
    PayrollRun:
      type: object
      required: [id, period, kind, status, created_at]
      properties:
        id: {type: string, format: uuid}
        period: {type: string}
        kind:
          type: string
          enum: [ordinary, extra, supplementary]
        external_id: {type: [string, 'null']}
        status:
          type: string
          enum: [received, validated, rejected, settled]
          description: '`validated`: usable for the settlement and the returns. `rejected`: see `issues`. `settled`: the month''s contribution settlement is confirmed.'
        totals:
          type: object
          properties:
            employees: {type: integer}
            gross_pay: {$ref: '#/components/schemas/Amount'}
            irpf_withheld: {$ref: '#/components/schemas/Amount'}
            employee_contributions: {$ref: '#/components/schemas/Amount'}
        issues:
          type: array
          items:
            type: object
            properties:
              employee_id: {type: [string, 'null'], format: uuid}
              code:
                type: string
                description: Such as `not_affiliated`, `base_below_minimum`, `base_above_maximum` or `days_exceed_affiliation`.
              message: {type: string}
              blocking: {type: boolean}
        settlement_id: {type: [string, 'null'], format: uuid}
        tax_return_ids:
          type: array
          description: The 111 (and, in January, the 190) this run feeds.
          items: {type: string, format: uuid}
        created_at: {type: string, format: date-time}

    # Social Security
    AffiliationInput:
      type: object
      required: [employee_id, action, effective_date]
      additionalProperties: false
      properties:
        employee_id:
          type: string
          format: uuid
          description: The employee, with `tax_id`, `social_security_number` and `employment` filled.
        action:
          type: string
          enum: [alta, baja, variacion]
        effective_date: {type: string, format: date}
        termination_reason:
          type: string
          enum: [end_of_contract, dismissal, objective_dismissal, voluntary_resignation, probation_not_passed, retirement, other]
          description: Required for a `baja`.
        unused_vacation_days:
          type: integer
          minimum: 0
          description: For a `baja`, the vacation paid and not taken, which extends the contribution period.
        changes:
          $ref: '#/components/schemas/EmploymentData'
          description: For a `variacion`, the new employment data.
    Affiliation:
      type: object
      required: [id, employee_id, action, effective_date, status, created_at]
      properties:
        id: {type: string, format: uuid}
        employee_id: {type: string, format: uuid}
        action:
          type: string
          enum: [alta, baja, variacion]
        effective_date: {type: string, format: date}
        status:
          type: string
          enum: [pending, submitted, accepted, rejected]
        submitted_at: {type: [string, 'null'], format: date-time}
        resolution:
          oneOf:
            - $ref: '#/components/schemas/Document'
            - type: 'null'
          description: The TGSS's resolution once accepted.
        rejection_reason: {type: [string, 'null']}
        created_at: {type: string, format: date-time}
    ContributionSettlement:
      type: object
      required: [id, period, contribution_account, status, due_date]
      properties:
        id: {type: string, format: uuid}
        period: {type: string, description: The month, such as `2026-10`.}
        contribution_account: {type: string, description: The CCC.}
        status:
          type: string
          enum: [requested, calculated, discrepancy, confirmed, paid, overdue]
          description: |
            `calculated`: the TGSS calculated the draft. `discrepancy`: it does
            not match the payroll run; `discrepancies` says where. `confirmed`:
            Melso confirmed it and the direct debit is set. `paid`: charged.
        employer_contribution: {$ref: '#/components/schemas/Amount'}
        employee_contribution: {$ref: '#/components/schemas/Amount'}
        total: {$ref: '#/components/schemas/Amount'}
        workers: {type: integer}
        due_date:
          type: string
          format: date
          description: The last day of the month after `period`.
        payroll_run_id: {type: [string, 'null'], format: uuid}
        discrepancies:
          type: array
          items:
            type: object
            properties:
              employee_id: {type: string, format: uuid}
              field: {type: string}
              payroll_value: {$ref: '#/components/schemas/Amount'}
              tgss_value: {$ref: '#/components/schemas/Amount'}
        documents:
          type: object
          properties:
            rnt: {$ref: '#/components/schemas/Document'}
            rlc: {$ref: '#/components/schemas/Document'}

    # Employment notifications
    ContractNotificationInput:
      type: object
      required: [employee_id, signed_on, start_date, signed_contract_document_id]
      additionalProperties: false
      properties:
        employee_id: {type: string, format: uuid}
        kind:
          type: string
          enum: [contract, extension, conversion]
          default: contract
        signed_on: {type: string, format: date}
        start_date: {type: string, format: date}
        end_date: {type: string, format: date, description: For a temporary contract.}
        weekly_hours: {$ref: '#/components/schemas/Decimal'}
        signed_contract_document_id:
          type: string
          format: uuid
          description: The signed contract, uploaded through Documents.
    CompanyCertificateInput:
      type: object
      required: [employee_id, end_date, termination_reason]
      additionalProperties: false
      properties:
        employee_id: {type: string, format: uuid}
        end_date: {type: string, format: date}
        termination_reason:
          type: string
          enum: [end_of_contract, dismissal, objective_dismissal, collective_dismissal, probation_not_passed, voluntary_resignation, other]
        unused_vacation_days: {type: integer, minimum: 0}
    EmploymentNotification:
      type: object
      required: [id, kind, employee_id, status, created_at]
      properties:
        id: {type: string, format: uuid}
        kind:
          type: string
          enum: [contract, extension, conversion, company_certificate]
        employee_id: {type: string, format: uuid}
        status:
          type: string
          enum: [pending, submitted, registered, rejected]
        reference: {type: [string, 'null'], description: The SEPE's reference.}
        receipt:
          oneOf:
            - $ref: '#/components/schemas/Document'
            - type: 'null'
        rejection_reason: {type: [string, 'null']}
        deadline: {type: string, format: date}
        created_at: {type: string, format: date-time}
