openapi: 3.0.3
info:
  title: BeeL. Public API
  description: |
    Public invoicing API for self-employed professionals in Spain with VeriFactu regulatory compliance.

    Authenticate using API Keys (`Authorization: Bearer <key>`). Keys are scoped and environment-aware:
    `beel_sk_live_*` targets production, `beel_sk_test_*` targets sandbox (no quota consumed, VeriFactu test mode).

    POST endpoints accept an `Idempotency-Key` header (any unique client-generated string) to safely retry requests without creating duplicates.
  version: 1.3.0
  contact:
    name: BeeL. Technical Team
    email: it@beel.es
    url: https://beel.es
  license:
    name: Proprietary
    url: https://beel.es/license
servers:
  - url: https://app.beel.es/api
    description: Production server
security:
  - ApiKeyAuth: []
  - OAuth2: []
tags:
  - name: Invoices
    description: Invoice CRUD operations
  - name: InvoiceLifecycle
    description: Invoice state transitions and scheduling
  - name: InvoiceDelivery
    description: Invoice delivery, bulk operations and exports
  - name: Customers
    description: Customer management
  - name: Customer Import
    description: Bulk creation, CSV import, and Holded import operations
  - name: Customer Templates
    description: Templates and sample files for customer imports
  - name: Products
    description: Product and service catalog management
  - name: ConfigurationTax
    description: Tax configuration and tax type catalogs
  - name: InvoiceSeries
    description: Invoice series management
  - name: ConfigurationVeriFactu
    description: VeriFactu compliance configuration
  - name: ConfigurationPreferences
    description: User language and invoice customization preferences
  - name: NIF
    description: Tax ID validation against AEAT tax registry
  - name: PublicCompanies
    description: Company (sub-account) CRUD operations
  - name: PublicCompanyRepresentations
    description: |
      VeriFactu representation flow for digital signature and AEAT registration.

      **Complete SaaS integration flow:**
      1. `POST /v1/companies` — Create company/NIF (sub-account)
      2. `POST /v1/companies/{id}/representation/generate` — Generate unsigned PDF
      3. `GET /v1/companies/{id}/representation/download` — Download PDF for signing
      4. `POST /v1/companies/{id}/representation/submit` — Upload signed PDF
      5. `GET /v1/companies/{id}/representation/status` — Check VeriFactu status
  - name: PublicCompanyApiKeys
    description: |
      Scoped API key management per company.

      Create API keys scoped to a specific company for SaaS integrations.
      Use master key + `X-Active-Profile: {company_id}` header to operate as that company.
  - name: RecurringInvoices
    description: |
      Recurring invoice template management and automated invoice generation.

      Create templates that automatically generate invoices on a schedule.
      Supports monthly recurrence, pause/resume, skip, manual generation,
      and invoice preview.
  - name: Webhooks
    description: |
      Outbound webhook subscriptions for real-time event notifications.

      Register HTTPS endpoints to receive event payloads when key actions occur
      (e.g. VeriFactu submission accepted/rejected by AEAT).

      All deliveries are signed with HMAC-SHA256 (`BeeL-Signature` header).
      Failed deliveries are retried up to 5 times with exponential backoff.
paths:
  /v1/invoices:
    get:
      tags:
        - Invoices
      summary: List invoices
      description: Returns a paginated list of invoices with optional filters
      operationId: listInvoices
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      parameters:
        - $ref: '#/components/parameters/ActiveProfileHeader'
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: search
          in: query
          description: Global search across invoice number, recipient name, recipient NIF, and series code (partial, case-insensitive)
          schema:
            type: string
        - name: status
          in: query
          description: Filter by invoice status
          schema:
            $ref: '#/components/schemas/InvoiceStatus'
        - name: type
          in: query
          description: Filter by invoice type
          schema:
            $ref: '#/components/schemas/InvoiceType'
        - name: customer_id
          in: query
          description: Filter by customer UUID
          schema:
            $ref: '#/components/schemas/UUID'
        - name: date_from
          in: query
          description: Issue date from (YYYY-MM-DD)
          schema:
            type: string
            format: date
        - name: date_to
          in: query
          description: Issue date to (YYYY-MM-DD)
          schema:
            type: string
            format: date
        - name: invoice_number
          in: query
          description: Search by invoice number (e.g., 2025/0001)
          schema:
            type: string
        - name: recipient_name
          in: query
          description: Filter by recipient's fiscal name (partial, case-insensitive search)
          schema:
            type: string
        - name: recipient_nif
          in: query
          description: Filter by recipient's NIF (partial search)
          schema:
            type: string
        - name: series_code
          in: query
          description: Filter by series code
          schema:
            type: string
        - name: taxable_base_min
          in: query
          description: Minimum taxable base
          schema:
            type: number
            format: double
        - name: taxable_base_max
          in: query
          description: Maximum taxable base
          schema:
            type: number
            format: double
        - name: total_min
          in: query
          description: Minimum invoice total
          schema:
            type: number
            format: double
        - name: total_max
          in: query
          description: Maximum invoice total
          schema:
            type: number
            format: double
        - name: verifactu_status
          in: query
          description: |
            Filter by VeriFactu status. Special values:
            - "NO_VERIFACTU": Invoices where VeriFactu is not enabled
            - "PENDING": Invoices with VeriFactu enabled pending submission
            - "ACCEPTED": Invoices submitted and accepted by AEAT
            - "REJECTED": Invoices submitted and rejected by AEAT
          schema:
            type: string
            enum:
              - NO_VERIFACTU
              - PENDING
              - ACCEPTED
              - REJECTED
        - name: metadata
          in: query
          description: |
            Filter by metadata key/value pairs (exact match, AND between keys).
            Repeat the bracket-style param to filter on multiple keys.
            Max 50 pairs per request. Keys must match `^[A-Za-z0-9_\-.]{1,64}$`.
            Example: `?metadata[external_order_id]=ORD-42&metadata[tenant]=acme`
          required: false
          style: deepObject
          explode: true
          schema:
            type: object
            additionalProperties:
              type: string
            maxProperties: 50
        - name: sort_by
          in: query
          description: Field to sort by (e.g., issue_date, invoice_number, invoice_total)
          schema:
            type: string
        - name: sort_order
          in: query
          description: Sort direction
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
      responses:
        '200':
          description: Invoice list retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        required:
                          - invoices
                          - pagination
                        properties:
                          invoices:
                            type: array
                            items:
                              $ref: '#/components/schemas/Invoice'
                          pagination:
                            $ref: '#/components/schemas/Pagination'
              examples:
                list_success:
                  $ref: '#/components/examples/list-invoices-success'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags:
        - Invoices
      summary: Create invoice
      description: |
        Creates a new invoice.

        **Idempotency:** Supports `Idempotency-Key` header to prevent duplicates.
      operationId: createInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - $ref: '#/components/parameters/ActiveProfileHeader'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInvoiceRequest'
            examples:
              basic_invoice:
                $ref: '#/components/examples/create-basic'
              adhoc_invoice:
                $ref: '#/components/examples/create-with-adhoc-customer'
              issue_directly:
                $ref: '#/components/examples/create-issue-directly'
              custom_email:
                $ref: '#/components/examples/create-with-custom-email'
              simplified_with_nif:
                $ref: '#/components/examples/simplified-with-nif'
              simplified_under_3000:
                $ref: '#/components/examples/under-3000'
              standard_spanish_b2b:
                $ref: '#/components/examples/spanish-b2b'
              standard_multiple_vat:
                $ref: '#/components/examples/multiple-vat-rates'
              standard_irpf:
                $ref: '#/components/examples/irpf-withholding'
              standard_recargo:
                $ref: '#/components/examples/recargo-equivalencia'
              standard_eu_goods_e5:
                $ref: '#/components/examples/b2b-intra-eu-goods-e5'
              standard_eu_services_n2:
                $ref: '#/components/examples/b2b-intra-eu-services-n2'
              standard_b2c_oss_over:
                $ref: '#/components/examples/b2c-oss-over-threshold'
              standard_b2c_oss_under:
                $ref: '#/components/examples/b2c-under-oss-threshold'
              standard_export_e2:
                $ref: '#/components/examples/export-non-eu-goods-e2'
              standard_services_noeu:
                $ref: '#/components/examples/services-non-eu-n2'
              standard_exempt_art20:
                $ref: '#/components/examples/exempt-art-20-medical'
              standard_igic:
                $ref: '#/components/examples/igic-canary-islands'
              standard_not_subject:
                $ref: '#/components/examples/not-subject-art-7-9'
              standard_isp_reverse:
                $ref: '#/components/examples/isp-reverse-charge-s2'
              standard_rebu:
                $ref: '#/components/examples/rebu-used-goods'
      responses:
        '201':
          description: Invoice created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
              examples:
                created_success:
                  $ref: '#/components/examples/created-success'
        '400':
          $ref: '#/components/responses/InvalidJsonFormat'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Conflict - Idempotency key already processed or duplicate invoice number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                conflict:
                  $ref: '#/components/examples/conflict-409'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                validation_error:
                  $ref: '#/components/examples/validation-422'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}:
    get:
      tags:
        - Invoices
      summary: Get invoice by ID
      description: Retrieves complete details of a specific invoice
      operationId: getInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      parameters:
        - $ref: '#/components/parameters/ActiveProfileHeader'
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '200':
          description: Invoice retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
              examples:
                get_success:
                  $ref: '#/components/examples/get-invoice-success'
                stripe_generated:
                  $ref: '#/components/examples/generated-invoice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      tags:
        - Invoices
      summary: Update invoice
      description: |
        Updates an existing draft invoice.

        **Restriction:** Only invoices in `DRAFT` status can be modified.
      operationId: updateInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - $ref: '#/components/parameters/ActiveProfileHeader'
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateInvoiceRequest'
            examples:
              update_invoice:
                $ref: '#/components/examples/update-invoice'
      responses:
        '200':
          description: Invoice updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          $ref: '#/components/responses/InvalidJsonFormat'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      tags:
        - Invoices
      summary: Delete invoice
      description: |
        Deletes (marks as deleted) an invoice.

        **Soft delete:** The invoice is not physically deleted, only marked as deleted.

        **Restriction:** Only invoices in `DRAFT` status can be deleted.
      operationId: deleteInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - $ref: '#/components/parameters/ActiveProfileHeader'
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '204':
          description: Invoice deleted successfully
        '400':
          description: Cannot delete (invoice already sent/paid)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/bulk/pdf:
    post:
      tags:
        - InvoiceDelivery
      summary: Download multiple invoice PDFs as ZIP
      description: |
        Downloads multiple invoice PDFs as a single ZIP file.

        **Limits:**
        - Maximum 20 invoices per request
        - Maximum 50MB total ZIP size

        **Partial success:** If some PDFs are not available (not generated, not found),
        the response will include a `failures` array with details. The ZIP will still
        be generated with the available PDFs.

        **Error handling:**
        - If NO PDFs are available, returns 400 Bad Request
        - If some PDFs are unavailable, returns 200 with partial ZIP + failures list
      operationId: downloadInvoicesPdfBulk
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - invoice_ids
              properties:
                invoice_ids:
                  type: array
                  description: List of invoice IDs to download
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: '#/components/schemas/UUID'
            examples:
              download_5_invoices:
                summary: Download 5 invoices
                value:
                  invoice_ids:
                    - 550e8400-e29b-41d4-a716-446655440001
                    - 550e8400-e29b-41d4-a716-446655440002
                    - 550e8400-e29b-41d4-a716-446655440003
                    - 550e8400-e29b-41d4-a716-446655440004
                    - 550e8400-e29b-41d4-a716-446655440005
      responses:
        '200':
          description: ZIP file with invoice PDFs
          headers:
            X-Bulk-Total:
              description: Total number of requested invoices
              schema:
                type: integer
            X-Bulk-Successful:
              description: Number of PDFs included in ZIP
              schema:
                type: integer
            X-Bulk-Failed:
              description: Number of PDFs that could not be included
              schema:
                type: integer
            X-Bulk-Failures:
              description: JSON array with failure details (base64 encoded if present)
              schema:
                type: string
          content:
            application/zip:
              schema:
                type: string
                format: binary
        '400':
          description: No valid PDFs available or invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Validation error (too many invoices, empty list)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/bulk/send:
    post:
      tags:
        - InvoiceDelivery
      summary: Send multiple invoices by email
      description: |
        Sends multiple invoices by email to the specified recipients.

        **Limits:**
        - Maximum 20 invoices per request
        - Maximum 25MB total attachment size

        **Email behavior:**
        - Single email with all PDFs as attachments
        - Uses BULK_INVOICES template

        **Validations:**
        - Invoices must have status ISSUED, PAID, SENT or OVERDUE (not DRAFT or VOIDED)
        - At least one recipient required (from request or user's accountant email)

        **Partial success:** If some invoices cannot be attached, the email will
        still be sent with available PDFs. Response includes `failures` array.
      operationId: sendInvoicesBulkEmail
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - invoice_ids
              properties:
                invoice_ids:
                  type: array
                  description: List of invoice IDs to send
                  minItems: 1
                  maxItems: 200
                  items:
                    $ref: '#/components/schemas/UUID'
                recipients:
                  type: array
                  description: |
                    Email recipients. If not provided, uses accountant email from user profile.
                  items:
                    $ref: '#/components/schemas/Email'
                cc:
                  type: array
                  description: CC recipients
                  items:
                    $ref: '#/components/schemas/Email'
                subject:
                  type: string
                  description: Custom email subject. If not provided, uses default template.
                  maxLength: 200
                message:
                  type: string
                  description: Custom message body. If not provided, uses default template.
                  maxLength: 2000
                language:
                  allOf:
                    - $ref: '#/components/schemas/Language'
                  description: Email language. If not provided, uses user's language.
            examples:
              send_to_accountant:
                summary: Send to accountant
                value:
                  invoice_ids:
                    - 550e8400-e29b-41d4-a716-446655440001
                    - 550e8400-e29b-41d4-a716-446655440002
                  recipients:
                    - accountant@example.com
                  subject: Invoices for January 2025
                  message: Please find attached the invoices from last month for your review.
              send_with_cc:
                summary: Send with CC
                value:
                  invoice_ids:
                    - 550e8400-e29b-41d4-a716-446655440001
                  recipients:
                    - accountant@example.com
                  cc:
                    - copy@example.com
      responses:
        '200':
          description: Email sent successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        required:
                          - email_id
                          - sent_to
                          - sent_at
                          - total_invoices
                          - invoices_attached
                        properties:
                          email_id:
                            $ref: '#/components/schemas/UUID'
                          sent_to:
                            type: array
                            items:
                              $ref: '#/components/schemas/Email'
                          sent_at:
                            type: string
                            format: date-time
                          total_invoices:
                            type: integer
                            description: Total number of requested invoices
                          invoices_attached:
                            type: integer
                            description: Number of PDFs successfully attached
                          failures:
                            type: array
                            description: List of invoices that could not be attached
                            items:
                              type: object
                              properties:
                                invoice_id:
                                  $ref: '#/components/schemas/UUID'
                                error_code:
                                  type: string
                                  enum:
                                    - NOT_FOUND
                                    - PDF_NOT_GENERATED
                                    - INVALID_STATUS
                                    - INTERNAL_ERROR
                                error_message:
                                  type: string
        '400':
          description: No valid invoices to send or no recipients specified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Validation error (too many invoices, invalid recipients)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/bulk/status:
    post:
      tags:
        - InvoiceDelivery
      summary: Change status of multiple invoices
      description: |
        Changes the status of multiple invoices in a single operation.

        **Supported status changes:**
        - **SENT:** Mark multiple invoices as sent (from ISSUED)
        - **PAID:** Mark multiple invoices as paid (from ISSUED, SENT, or OVERDUE)

        **Partial success:** If some invoices cannot be updated, the operation
        continues with the others. The response includes details of failures.

        **Limits:**
        - Maximum 50 invoices per request

        **Validations:**
        - All invoices must belong to the authenticated user
        - Each invoice must be in a valid status for the requested transition
        - For PAID: payment_date is required
      operationId: changeInvoicesStatusBulk
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - invoice_ids
                - new_status
              properties:
                invoice_ids:
                  type: array
                  description: List of invoice IDs to update
                  minItems: 1
                  maxItems: 50
                  items:
                    $ref: '#/components/schemas/UUID'
                new_status:
                  type: string
                  description: Target status for the invoices
                  enum:
                    - SENT
                    - PAID
                payment_date:
                  type: string
                  format: date
                  description: Payment date (required when new_status is PAID)
                  example: '2025-01-15'
            examples:
              mark_as_sent:
                summary: Mark invoices as sent
                value:
                  invoice_ids:
                    - 550e8400-e29b-41d4-a716-446655440001
                    - 550e8400-e29b-41d4-a716-446655440002
                  new_status: SENT
              mark_as_paid:
                summary: Mark invoices as paid
                value:
                  invoice_ids:
                    - 550e8400-e29b-41d4-a716-446655440001
                    - 550e8400-e29b-41d4-a716-446655440002
                  new_status: PAID
                  payment_date: '2025-01-15'
      responses:
        '200':
          description: Status change operation completed (may include partial failures)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        required:
                          - total
                          - successful
                          - failed
                        properties:
                          total:
                            type: integer
                            description: Total number of invoices processed
                            example: 5
                          successful:
                            type: integer
                            description: Number of invoices successfully updated
                            example: 4
                          failed:
                            type: integer
                            description: Number of invoices that failed to update
                            example: 1
                          failures:
                            type: array
                            description: Details of failed updates
                            items:
                              type: object
                              required:
                                - invoice_id
                                - reason
                              properties:
                                invoice_id:
                                  $ref: '#/components/schemas/UUID'
                                reason:
                                  type: string
                                  description: Reason for the failure
                                  example: Cannot change from PAID to SENT
        '400':
          description: Invalid request (empty list, missing payment_date for PAID)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Validation error (unsupported status, too many invoices)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/pdf:
    get:
      tags:
        - InvoiceDelivery
      summary: Get invoice PDF download URL
      description: |
        Generates a temporary pre-signed URL to download the invoice PDF.

        **Security:**
        - URL expires automatically in 5 minutes
        - HMAC signature prevents tampering
        - Only allows download (GET), no modification

        **Benefits:**
        - Direct download from storage (bypasses backend)
        - Better performance and lower server load
        - Compatible with browser cache and CDN
        - Ideal for large files

        **Frontend usage:**
        ```javascript
        const response = await fetch('/v1/invoices/{id}/pdf');
        const { download_url } = await response.json();
        window.open(download_url, '_blank'); // Direct download
        ```

        **Format:** PDF is generated according to Spanish tax regulations.
      operationId: generateInvoicePdf
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '200':
          description: Download URL generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicePdfResponse'
        '400':
          description: PDF has not been generated yet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/pdf/preview:
    get:
      tags:
        - InvoiceDelivery
      summary: Preview draft invoice PDF
      description: |
        Generates a PDF preview for a draft invoice and returns the binary content directly.

        **Requirements:**
        - Invoice must be in BORRADOR (draft) status
        - The PDF shows "BORRADOR" as the invoice number
        - No VeriFactu QR code is included

        **Behavior:**
        - PDF is generated on-the-fly (not stored)
        - Each request regenerates the PDF (reflects latest draft changes)
        - Response is `application/pdf` for direct browser preview
      operationId: previewDraftInvoicePdf
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID (must be a draft)
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '200':
          description: PDF preview generated successfully
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '400':
          description: Invoice is not in draft status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/send:
    post:
      tags:
        - InvoiceDelivery
      summary: Send invoice by email
      description: |
        Sends the invoice by email to the customer.

        **Email:** Sent to the billing emails configured for the customer.

        **Attachments:** Includes the invoice PDF.
      operationId: sendInvoiceEmail
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEmailRequest'
      responses:
        '200':
          description: Email sent successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          email_id:
                            $ref: '#/components/schemas/UUID'
                          sent_to:
                            type: array
                            items:
                              $ref: '#/components/schemas/Email'
                          sent_at:
                            type: string
                            format: date-time
        '400':
          description: Customer has no configured emails
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/mark-paid:
    post:
      tags:
        - InvoiceLifecycle
      summary: Mark invoice as paid
      description: |
        Marks an invoice as paid.

        **Status change:** `SENT` → `PAID`

        **Note:** Send `Content-Type: application/json` even when body is empty.
      operationId: markInvoicePaid
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                payment_date:
                  type: string
                  format: date
                  description: Payment date (defaults to today)
                  example: '2025-01-15'
                payment_method:
                  $ref: '#/components/schemas/PaymentInfo'
                  description: |
                    Payment details object (not a string enum). Example:
                    `{ "method": "BANK_TRANSFER", "iban": "ES9121000418450200051332" }`
      responses:
        '200':
          description: Invoice marked as paid successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot mark as paid (invalid status)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/void:
    post:
      tags:
        - InvoiceLifecycle
      summary: Void invoice
      description: |
        Voids an invoice by changing its status to `VOIDED`.

        **Status change:** `ISSUED`, `SENT`, `OVERDUE`, `PAID` or `RECTIFIED` → `VOIDED`

        **Use when:** The operation never took place (invoice issued by mistake, duplicate, etc.)
        For invoices already submitted to VeriFactu, the cancellation is sent to AEAT automatically.

        **Note:** If the operation did take place but with errors, use the corrective invoice endpoint instead.
      operationId: voidInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 10
                  maxLength: 500
                  description: Void reason (minimum 10 characters)
                  example: Invoice issued with incorrect customer data
                void_date:
                  type: string
                  format: date
                  description: Void date (defaults to today)
                  example: '2025-01-20'
            examples:
              void_by_error:
                $ref: '#/components/examples/void-issued-by-error'
      responses:
        '200':
          description: Invoice voided successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot void invoice (invalid status or incorrect data)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/corrective:
    post:
      tags:
        - InvoiceLifecycle
      summary: Create corrective invoice
      description: |
        Creates a corrective invoice to correct or void an issued invoice.

        **Rectification types:**
        - **TOTAL**: Completely voids the original invoice (original status → VOIDED)
        - **PARTIAL**: Partially corrects the original invoice (original status → RECTIFIED)

        **VeriFactu codes:**
        - R1: Legal error and Art. 80 One, Two and Six LIVA
        - R2: Article 80 Three LIVA (Bankruptcy)
        - R3: Article 80 Four LIVA (Bad debts)
        - R4: Other causes
        - R5: Simplified invoices (only for simplified)

        **Validations:**
        - Original invoice must be in status ISSUED, SENT, PAID, OVERDUE or RECTIFIED
        - Cannot rectify a DRAFT invoice (must be issued first)
        - Cannot rectify a VOIDED invoice (final status)
        - For TOTAL rectification: Another total corrective cannot already exist
        - For PARTIAL rectification: Multiple partial correctives are allowed

        **Lines:**
        - TOTAL: Optional (if not provided, original lines are copied with negated amounts)
        - PARTIAL: REQUIRED (adjustment lines with positive or negative amounts)
      operationId: createCorrectiveInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID to rectify
          schema:
            $ref: '#/components/schemas/UUID'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCorrectiveInvoiceRequest'
            examples:
              corrective_total_r1:
                $ref: '#/components/examples/corrective-total-r1'
              corrective_total_r4:
                $ref: '#/components/examples/corrective-total-r4'
              corrective_partial_r2:
                $ref: '#/components/examples/corrective-partial-r2'
              corrective_partial_r3:
                $ref: '#/components/examples/corrective-partial-r3'
              corrective_r1_partial_discount:
                $ref: '#/components/examples/r1-partial-discount'
              corrective_r5_total_simplified:
                $ref: '#/components/examples/r5-total-simplified'
      responses:
        '201':
          description: Corrective invoice created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Invalid rectification data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Original invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: A total corrective invoice already exists for this invoice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Original invoice cannot be rectified (invalid status)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/issue:
    post:
      tags:
        - InvoiceLifecycle
      summary: Issue invoice (DRAFT → ISSUED)
      description: |
        Finalizes a draft invoice and marks it as issued.

        **Status change:** `DRAFT` → `ISSUED`

        **Actions performed:**
        - Assigns definitive invoice number according to configured series
        - Marks invoice as finalized (not modifiable)
        - Prepares invoice for sending to customer

        **Validations:**
        - Invoice must be in DRAFT status
        - All invoice data must be complete and valid

        **Note:** After issuing, the invoice can be sent to the customer via
        the `/invoices/{id}/send` endpoint
      operationId: issueInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
        - name: wait_for_pdf
          in: query
          required: false
          description: |
            If `true`, waits for PDF generation and returns the URL in the response.
            Adds ~1-2s latency but guarantees PDF is immediately available.
            If `false` (default), PDF is generated asynchronously.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Invoice issued successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot issue invoice (invalid status or incomplete data)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Invoice cannot be issued (validation failed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/mark-sent:
    post:
      tags:
        - InvoiceLifecycle
      summary: Mark invoice as sent
      description: |
        Marks an invoice as sent manually.

        **Use case:** When the invoice was sent via WhatsApp, paper, or any other
        channel outside of the email system.

        **Status change:** `ISSUED` → `SENT`

        **Records:** Sets `sent_at` timestamp for tracking.

        **Note:** Send `Content-Type: application/json` even when body is empty.
      operationId: markInvoiceSent
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                sent_at:
                  type: string
                  format: date-time
                  description: |
                    Custom timestamp for when the invoice was sent.
                    If not provided, the current time will be used.
                  example: '2025-01-29T18:45:00Z'
      responses:
        '200':
          description: Invoice marked as sent successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot mark as sent (invalid status - must be ISSUED)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/revert-to-issued:
    post:
      tags:
        - InvoiceLifecycle
      summary: Revert invoice to issued status
      description: |
        Reverts an invoice from SENT to ISSUED status.

        **Use case:** To correct errors when an invoice was marked as sent by mistake.

        **Status change:** `SENT` → `ISSUED`

        **Side effects:** Clears the `sent_at` timestamp.
      operationId: revertInvoiceToIssued
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '200':
          description: Invoice reverted to ISSUED successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot revert (invoice must be in SENT status)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/schedule:
    post:
      tags:
        - InvoiceLifecycle
      summary: Schedule invoice for future processing
      description: |
        Schedules a draft invoice to be automatically processed on a future date.

        **Status change:** `DRAFT` → `SCHEDULED`

        **Actions available:**
        - `DRAFT`: Convert to draft for manual review on the scheduled date
        - `ISSUE_AND_SEND`: Issue and send automatically on the scheduled date
      operationId: scheduleInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - scheduled_for
              properties:
                scheduled_for:
                  type: string
                  format: date
                  description: Date when the invoice should be processed (must be today or future)
                  example: '2025-02-15'
                action:
                  $ref: '#/components/schemas/GenerationAction'
      responses:
        '200':
          description: Invoice scheduled successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot schedule (invalid date or invoice state)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/unschedule:
    post:
      tags:
        - InvoiceLifecycle
      summary: Cancel scheduled invoice
      description: |
        Cancels a scheduled invoice and converts it back to draft.

        **Status change:** `SCHEDULED` → `DRAFT`

        The invoice can then be edited or manually issued.
      operationId: unscheduleInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '200':
          description: Invoice unscheduled successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot unschedule (invoice must be in SCHEDULED status)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/reschedule:
    patch:
      tags:
        - InvoiceLifecycle
      summary: Reschedule invoice
      description: |
        Changes the scheduled date for a scheduled invoice.

        **Status:** Remains `SCHEDULED`

        The new date must be today or in the future.
      operationId: rescheduleInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: Invoice ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - scheduled_for
              properties:
                scheduled_for:
                  type: string
                  format: date
                  description: New date when the invoice should be processed
                  example: '2025-03-01'
      responses:
        '200':
          description: Invoice rescheduled successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot reschedule (invalid date or invoice state)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/{invoice_id}/duplicate:
    post:
      tags:
        - Invoices
      summary: Duplicate invoice as draft
      description: |
        Creates a copy of an existing invoice as a new draft.

        **Use case:** Quickly create a new invoice based on an existing one,
        useful for recurring invoices to the same customer.

        **What is copied:**
        - Customer data (recipient)
        - Invoice lines (descriptions, quantities, prices, taxes)
        - Payment method
        - Series (if specified, otherwise uses default)
        - Observations

        **What is NOT copied (reset to defaults):**
        - Invoice number (null - assigned when issued)
        - Status (always DRAFT)
        - Issue date (always set to today)
        - Due date (recalculated based on new issue date)
        - VeriFactu data
        - PDF
        - Payment date
        - Sent date

        **Result:** A new draft invoice ready for review and emission.

        **Note:** Send `Content-Type: application/json` even when body is empty.
      operationId: duplicateInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          description: ID of the invoice to duplicate
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                series_id:
                  allOf:
                    - $ref: '#/components/schemas/UUID'
                  description: |
                    Series ID for the new invoice.
                    If not provided, uses the same series as the original.
                notes:
                  type: string
                  maxLength: 2000
                  description: |
                    Observations for the new invoice.
                    If not provided, copies from the original invoice.
            examples:
              simple_duplicate:
                summary: Simple duplication (defaults)
                value: {}
              duplicate_other_series:
                summary: Duplication to different series
                value:
                  series_id: 550e8400-e29b-41d4-a716-446655440099
      responses:
        '201':
          description: Invoice duplicated successfully as draft
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '400':
          description: Cannot duplicate invoice (e.g., invoice is deleted)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Original invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/invoices/export/excel:
    post:
      tags:
        - InvoiceDelivery
      summary: Export invoices to Excel
      description: |
        Exports invoices to an Excel file (XLSX format).

        **Two modes of operation:**
        - **By IDs:** Export specific invoices by providing `invoice_ids`
        - **By filters:** Export all invoices matching filter criteria

        **Two export formats (like Holded):**
        - **SUMMARY:** One row per invoice with aggregated totals (default)
        - **ITEMS:** One row per invoice line item, ideal for import compatibility

        **Columns for SUMMARY format:**
        - Date, Number, Due Date, Customer, NIF
        - Description, Subtotal, IVA, Withholding, Equivalence Surcharge
        - Total, Collected, Pending, Status, Payment Method

        **Columns for ITEMS format:**
        - Invoice Number, Date, Due Date, Draft, Contact, Contact NIF
        - Address, Postal Code, City, Province, Country
        - SKU, Item, Units, Unit Price, Discount %
        - Subtotal, Tax Type, Tax %, Tax Amount
        - Surcharge %, Surcharge Amount, IRPF %, IRPF Amount, Total

        **Limits:**
        - Maximum 5000 invoices per export
        - All invoices must belong to the authenticated user (RLS enforced)

        **Usage example (Frontend):**
        ```javascript
        const response = await fetch('/v1/invoices/export/excel', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ invoice_ids: [...], format: 'ITEMS' })
        });
        const blob = await response.blob();
        // Download the blob as file
        ```
      operationId: exportInvoicesExcel
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                invoice_ids:
                  type: array
                  description: |
                    List of specific invoice IDs to export.
                    If provided, filters are ignored.
                  items:
                    $ref: '#/components/schemas/UUID'
                format:
                  type: string
                  description: |
                    Export format.
                    - **SUMMARY**: One row per invoice with totals (default)
                    - **ITEMS**: One row per invoice line item
                  enum:
                    - SUMMARY
                    - ITEMS
                  default: SUMMARY
                status:
                  $ref: '#/components/schemas/InvoiceStatus'
                type:
                  $ref: '#/components/schemas/InvoiceType'
                date_from:
                  type: string
                  format: date
                  description: Issue date from (YYYY-MM-DD)
                date_to:
                  type: string
                  format: date
                  description: Issue date to (YYYY-MM-DD)
                customer_id:
                  $ref: '#/components/schemas/UUID'
                recipient_name:
                  type: string
                  description: Filter by recipient name (partial match)
                recipient_nif:
                  type: string
                  description: Filter by recipient NIF (partial match)
                series_code:
                  type: string
                  description: Filter by series code
            examples:
              export_by_ids:
                summary: Export specific invoices (default format)
                value:
                  invoice_ids:
                    - 550e8400-e29b-41d4-a716-446655440001
                    - 550e8400-e29b-41d4-a716-446655440002
                    - 550e8400-e29b-41d4-a716-446655440003
              export_items_format:
                summary: Export as items (one row per line)
                value:
                  invoice_ids:
                    - 550e8400-e29b-41d4-a716-446655440001
                  format: ITEMS
              export_q1_2025:
                summary: Export Q1 2025 invoices
                value:
                  date_from: '2025-01-01'
                  date_to: '2025-03-31'
              export_paid:
                summary: Export all paid invoices
                value:
                  status: PAID
      responses:
        '200':
          description: Excel file generated successfully
          headers:
            Content-Disposition:
              description: Suggested filename for download
              schema:
                type: string
              example: attachment; filename="facturas_2025-01-15.xlsx"
            X-Total-Invoices:
              description: Number of invoices included in the export
              schema:
                type: integer
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
        '400':
          description: Invalid request (must provide invoice_ids or filters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Validation error (too many invoices, invalid filters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/recurring-invoices:
    post:
      tags:
        - RecurringInvoices
      summary: Create a recurring invoice from scratch
      description: Creates a new recurring invoice template with all template data (lines, recipient, series, payment) and recurrence configuration, without needing an existing invoice.
      operationId: createRecurringInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRecurringInvoiceRequest'
      responses:
        '201':
          description: Recurring invoice created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RecurringInvoiceResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
    get:
      tags:
        - RecurringInvoices
      summary: List recurring invoices
      description: Lists all recurring invoices for the authenticated user with filters and pagination.
      operationId: listRecurringInvoices
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - ACTIVE
              - PAUSED
              - COMPLETED
        - name: customer_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: sortBy
          in: query
          required: false
          schema:
            type: string
            enum:
              - name
              - next_generation
              - status
              - created_at
            default: created_at
        - name: sortOrder
          in: query
          required: false
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
      responses:
        '200':
          description: Recurring invoices list retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/RecurringInvoiceResponse'
                      pagination:
                        $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/recurring-invoices/{recurring_invoice_id}:
    get:
      tags:
        - RecurringInvoices
      summary: Get a recurring invoice by ID
      description: Retrieves the full details of a recurring invoice including its schedule, template lines, and next generation date.
      operationId: getRecurringInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Recurring invoice retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RecurringInvoiceResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
        - RecurringInvoices
      summary: Update a recurring invoice
      description: Updates the schedule, template lines, or recipient of a recurring invoice. Only allowed while the invoice is active or paused.
      operationId: updateRecurringInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateRecurringInvoiceRequest'
      responses:
        '200':
          description: Recurring invoice updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RecurringInvoiceResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      tags:
        - RecurringInvoices
      summary: Delete a recurring invoice
      description: Permanently deletes a recurring invoice and cancels any pending scheduled generations.
      operationId: deleteRecurringInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Recurring invoice deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/recurring-invoices/{recurring_invoice_id}/pause:
    post:
      tags:
        - RecurringInvoices
      summary: Pause a recurring invoice
      description: Pauses automatic invoice generation. The recurring invoice can be resumed later without losing its schedule configuration.
      operationId: pauseRecurringInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Recurring invoice paused successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RecurringInvoiceResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/recurring-invoices/{recurring_invoice_id}/resume:
    post:
      tags:
        - RecurringInvoices
      summary: Resume a paused recurring invoice
      description: Resumes automatic invoice generation for a previously paused recurring invoice. The next generation date is recalculated from the current date.
      operationId: resumeRecurringInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Recurring invoice resumed successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RecurringInvoiceResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/recurring-invoices/{recurring_invoice_id}/generate:
    post:
      tags:
        - RecurringInvoices
      summary: Generate an invoice now from a recurring template
      description: Manually triggers invoice generation from a recurring template.
      operationId: generateInvoiceNow
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '201':
          description: Invoice generated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          invoice_id:
                            type: string
                            format: uuid
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/recurring-invoices/{recurring_invoice_id}/skip:
    post:
      tags:
        - RecurringInvoices
      summary: Skip the next generation
      description: Skips the next scheduled invoice generation and advances the generation date to the following period.
      operationId: skipNextGeneration
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Next generation skipped successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RecurringInvoiceResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/recurring-invoices/{recurring_invoice_id}/preview:
    get:
      tags:
        - RecurringInvoices
      summary: Preview next invoice from recurring template
      description: |
        Returns a computed preview of what the next invoice would look like when
        generated from this recurring template. Uses current issuer, recipient,
        and series data. Nothing is persisted.
      operationId: previewRecurringInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          description: UUID of the recurring invoice template
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Invoice preview computed successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/recurring-invoices/{recurring_invoice_id}/history:
    get:
      tags:
        - RecurringInvoices
      summary: Get generation history for a recurring invoice
      description: Returns the list of invoices previously generated from this recurring template, including their status and generation dates.
      operationId: getRecurringHistory
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:read
      parameters:
        - name: recurring_invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Generation history retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/GenerationHistoryResponse'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/invoices/{invoice_id}/create-recurring:
    post:
      tags:
        - RecurringInvoices
      summary: Create a recurring invoice from an existing invoice
      description: Creates a new recurring invoice template using the lines, recipient, and configuration from an existing invoice.
      operationId: createRecurringFromInvoice
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - invoices:write
      parameters:
        - name: invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRecurringFromInvoiceRequest'
      responses:
        '201':
          description: Recurring invoice created from existing invoice
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RecurringInvoiceResponse'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/customers:
    get:
      tags:
        - Customers
      summary: List customers
      description: Returns a paginated list of customers with optional filters
      operationId: listCustomers
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:read
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: active
          in: query
          description: Filter by active/inactive status
          schema:
            type: boolean
        - name: search
          in: query
          description: Global search by name, NIF or email
          schema:
            type: string
        - name: legal_name
          in: query
          description: Filter by legal name (partial search case-insensitive)
          schema:
            type: string
        - name: nif
          in: query
          description: Filter by NIF (partial search)
          schema:
            type: string
        - name: email
          in: query
          description: Filter by email (partial search)
          schema:
            type: string
        - name: phone
          in: query
          description: Filter by phone (partial search)
          schema:
            type: string
        - name: city
          in: query
          description: Filter by city
          schema:
            type: string
        - name: province
          in: query
          description: Filter by province
          schema:
            type: string
        - name: sort_by
          in: query
          description: Field to sort by (eg. legal_name, nif, created_at)
          schema:
            type: string
        - name: sort_order
          in: query
          description: Sort order direction
          schema:
            type: string
            enum:
              - asc
              - desc
            default: asc
      responses:
        '200':
          description: Customer list retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        required:
                          - customers
                          - pagination
                        properties:
                          customers:
                            type: array
                            items:
                              $ref: '#/components/schemas/Customer'
                          pagination:
                            $ref: '#/components/schemas/Pagination'
              examples:
                list_success:
                  $ref: '#/components/examples/list-customers-success'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags:
        - Customers
      summary: Create customer
      description: |
        Creates a new customer.

        **Idempotency:** Supports `Idempotency-Key` header to prevent duplicates
        when a request is retried (network timeout, double-click, etc.).
      operationId: createCustomer
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:write
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCustomerRequest'
            examples:
              cliente_empresa:
                $ref: '#/components/examples/company-customer'
              cliente_autonomo:
                $ref: '#/components/examples/freelancer-customer'
              cliente_extranjero_ue:
                $ref: '#/components/examples/eu_customer'
              cliente_extranjero_usa:
                $ref: '#/components/examples/us_customer'
              cliente_spanish_business:
                $ref: '#/components/examples/spanish-business'
      responses:
        '201':
          description: Customer created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Customer'
              examples:
                created_success:
                  $ref: '#/components/examples/created-success-2'
        '400':
          $ref: '#/components/responses/InvalidJsonFormat'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Duplicate customer (same NIF)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                conflict:
                  $ref: '#/components/examples/conflict-409'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                validation_error:
                  $ref: '#/components/examples/validation-422'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/customers/{customer_id}:
    get:
      tags:
        - Customers
      summary: Get customer by ID
      description: Retrieves complete details of a specific customer
      operationId: getCustomer
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:read
      parameters:
        - name: customer_id
          in: path
          required: true
          description: Customer ID
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '200':
          description: Customer retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Customer'
              examples:
                get_success:
                  $ref: '#/components/examples/get-customer-success'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      tags:
        - Customers
      summary: Update customer
      description: Updates an existing customer
      operationId: updateCustomer
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:write
      parameters:
        - name: customer_id
          in: path
          required: true
          description: Customer ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCustomerRequest'
      responses:
        '200':
          description: Customer updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Customer'
        '400':
          $ref: '#/components/responses/InvalidJsonFormat'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      tags:
        - Customers
      summary: Deactivate customer
      description: |
        Deactivates (marks as deleted) a customer.

        **Soft delete:** The customer is not physically deleted, only marked as inactive.

        **Restriction:** Cannot deactivate a customer with associated invoices.
      operationId: deactivateCustomer
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:write
      parameters:
        - name: customer_id
          in: path
          required: true
          description: Customer ID
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '200':
          description: Customer deactivated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        required:
                          - message
                        properties:
                          message:
                            type: string
                            example: Customer deactivated successfully
        '400':
          description: Cannot deactivate (has associated invoices)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/customers/bulk:
    post:
      tags:
        - Customer Import
      summary: Create or validate multiple customers
      description: |
        Creates multiple customers in a single operation or validates data without persisting.

        **Validation flow:**
        1. `dry_run=true`: Full validation only (NIFs, duplicates, format) without persistence
        2. `dry_run=false`: Validation + actual creation

        **Validations performed:**
        - Individual validation of each customer (same rules as single customer creation)
        - Detection of duplicate NIFs within the batch
        - Detection of duplicate NIFs against existing database
        - Parallel NIF validation against AEAT registry (VeriFactu)
        - Field format verification (email, phone, etc.)

        **Result statuses:**
        - `VALID`: Customer valid, ready to create
        - `WARNING`: Customer valid but with warnings (e.g., missing email)
        - `ERROR`: Customer invalid, cannot process
        - `DUPLICATE`: Duplicate NIF (in batch or existing DB)
        - `NIF_INVALID`: NIF not valid in AEAT registry

        **Atomicity:**
        - `dry_run=true`: Validation only, no persistence
        - `dry_run=false`: If any customer fails, ALL customers are rejected (full atomicity)
      operationId: createCustomersBulk
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:write
      parameters:
        - name: dry_run
          in: query
          description: |
            Operation mode:
            - `true`: Validation only without persistence (recommended for frontend preview)
            - `false`: Validation + actual creation (atomic operation)
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customers
              properties:
                customers:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: '#/components/schemas/CreateCustomerRequest'
      responses:
        '200':
          description: |
            Validation completed successfully (dry_run=true).
            Returns validation results without persisting data using unified format.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/CustomerValidationUnifiedResult'
        '201':
          description: |
            Customers created successfully (dry_run=false).
            Atomic operation: all valid customers were created successfully.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          customers:
                            type: array
                            items:
                              $ref: '#/components/schemas/Customer'
                          total_created:
                            type: integer
                            example: 5
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Validation error in one or more customers
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - type: object
                    properties:
                      error:
                        type: object
                        properties:
                          code:
                            type: string
                            example: BULK_VALIDATION_ERROR
                          message:
                            type: string
                          errors:
                            type: array
                            items:
                              type: object
                              properties:
                                index:
                                  type: integer
                                  description: Index of the customer with error (0-based)
                                field:
                                  type: string
                                message:
                                  type: string
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      tags:
        - Customer Import
      summary: Delete multiple customers
      description: |
        Deactivates (marks as deleted) multiple customers in a single operation.

        **Soft delete:** Customers are not physically deleted, only marked as inactive.

        **Partial behavior:** Valid customers will be deactivated, failures are reported in the errors array.
      operationId: deactivateCustomersBulk
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:write
      parameters:
        - name: ids
          in: query
          required: true
          description: Comma-separated customer IDs
          schema:
            type: string
            example: 550e8400-e29b-41d4-a716-446655440000,550e8400-e29b-41d4-a716-446655440001
      responses:
        '200':
          description: Operation completed (may have partial errors)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        required:
                          - total
                          - successful
                          - failed
                          - deactivated_ids
                        properties:
                          total:
                            type: integer
                            description: Total customers processed
                            example: 10
                          successful:
                            type: integer
                            description: Number of successful deactivations
                            example: 8
                          failed:
                            type: integer
                            description: Number of failed deactivations
                            example: 2
                          deactivated_ids:
                            type: array
                            description: IDs of successfully deactivated customers
                            items:
                              $ref: '#/components/schemas/UUID'
                          errors:
                            type: array
                            description: Details of customers that could not be deactivated
                            items:
                              type: object
                              required:
                                - customer_id
                                - error
                              properties:
                                customer_id:
                                  $ref: '#/components/schemas/UUID'
                                error:
                                  type: object
                                  required:
                                    - code
                                    - message
                                  properties:
                                    code:
                                      type: string
                                      example: CLIENT_HAS_INVOICES
                                    message:
                                      type: string
                                      example: Cannot deactivate customer because it has associated invoices
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/customers/import-csv-preview:
    post:
      tags:
        - Customer Import
      summary: Import customers from CSV with structured preview
      description: |
        Imports multiple customers from a CSV file with full preview of all records.

        **Key features**:
        - **Structured preview**: Returns all parsed records (valid and with errors)
        - **Parallel validation**: NIFs validated concurrently against VeriFactu
        - **Granular feedback**: Customer-specific errors and warnings
        - **Editable data**: Structured JSON for editable table in frontend

        **Recommended flow**:
        1. **Initial upload**: Call with `dry_run=true` to get preview
        2. **Display table**: Frontend shows parsed data with errors/warnings
        3. **Inline editing**: User corrects data directly in the table
        4. **Final import**: Send corrected data with `dry_run=false`

        **Format**: Must follow CSV template format (downloadable from `/customers/template-csv`).

        **Security limits**:
        - Max size: 5MB
        - Max 1,000 records per file
        - Rate limiting: 3 imports per hour

        **Validation**:
        - Required headers per template
        - Full validation of each row with business rules
        - Parallel NIF validation against AEAT registry (VeriFactu)
        - Duplicate detection in CSV and database

        **Validation statuses**:
        - `VALID`: Customer valid, ready to import
        - `WARNING`: Customer valid but with warnings (eg: missing email)
        - `ERROR`: Customer invalid, cannot process
        - `DUPLICATE`: Duplicate customer (in CSV or DB)
        - `NIF_INVALID`: NIF invalid in AEAT registry
      operationId: importCustomersCsvPreview
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:write
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV file with customers to import
                dry_run:
                  type: boolean
                  default: true
                  description: |
                    If true (recommended initially): preview only without persistence.
                    If false: preview + persistence of valid customers.
      responses:
        '200':
          description: CSV processing completed with structured preview using unified format
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/CustomerValidationUnifiedResult'
        '400':
          description: |
            Invalid file for one of the following reasons:
            - Format is not valid CSV
            - Encoding is not UTF-8
            - Required headers missing
            - Size exceeds 5MB
            - More than 1,000 records
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - type: object
                    properties:
                      error:
                        type: object
                        properties:
                          details:
                            type: object
                            properties:
                              file_size_mb:
                                type: number
                                description: File size in MB
                              record_count:
                                type: integer
                                description: Number of records found
                              missing_headers:
                                type: array
                                items:
                                  type: string
                                description: Required headers that are missing
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: File too large (>5MB)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: |
            Structural CSV validation error (corrupted format, invalid encoding)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limit exceeded (maximum 3 imports per hour)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/customers/import-holded-contacts:
    post:
      tags:
        - Customer Import
      summary: Import contacts from Holded Excel
      description: |
        Imports multiple customers from an Excel file exported from Holded.

        **Key features**:
        - **Specific parsing**: Reads exact structure of Holded contacts Excel (29 columns)
        - **Automatic mapping**: Converts Holded contacts to Customer entities automatically
        - **Full validation**: Validates NIFs, addresses and required data
        - **Preview mode**: Allows viewing parsed data before importing

        **Recommended flow**:
        1. **Initial upload**: Call with `preview=true` to get preview
        2. **Display data**: Frontend shows parsed Holded contacts
        3. **Final import**: Call with `preview=false` to import definitively

        **Expected Excel structure** (contacts exported from Holded):
        - Sheet: "Holded"
        - Rows to skip: 2 (header + empty)
        - Headers in row 2: Created, Name, ID, Email, Phone, Mobile, etc. (29 columns)
        - Footer: "Report created automatically with Holded..."

        **Security limits**:
        - Max size: 10MB
        - Max 5,000 records per file
        - Rate limiting: 5 imports per hour

        **Automatic Holded → Customer mapping**:
        - Name → legal_name
        - ID → identifier (NIF/CIF)
        - Email → email
        - Phone/Mobile → phone (prioritizes mobile)
        - Full address → address
        - Tags → notes
      operationId: importHoldedContacts
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:write
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: Excel file (.xlsx) of contacts exported from Holded
                preview:
                  type: boolean
                  default: true
                  description: |
                    If true: parsing and mapping only without importing to DB.
                    If false: parsing + mapping + actual import.
      responses:
        '200':
          description: |
            Holded Excel processing completed with unified format.
            Returns parsed, validated and mapped customers using the same format as CSV import.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/CustomerValidationUnifiedResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: File too large (>10MB)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          description: Rate limit exceeded (maximum 5 imports per hour)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/customers/templates/csv:
    post:
      tags:
        - Customer Templates
      summary: Download CSV template for customers
      description: |
        Downloads a sample CSV file for customer import.

        **Format**: The file includes the required headers and example rows
        with UTF-8 BOM for Excel compatibility.
      operationId: downloadCustomerTemplateCsv
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - customers:read
      responses:
        '200':
          description: CSV template downloaded successfully
          content:
            text/csv:
              schema:
                type: string
                format: binary
              example: |
                nombre_fiscal;nif;email;telefono;direccion_calle;direccion_numero;direccion_codigo_postal;direccion_poblacion;direccion_provincia
                "Construcciones García y Asociados SL";B12345678;contacto@construccionesgarcia.es;915551234;"Calle Gran Vía";45;28013;Madrid;Madrid
          headers:
            Content-Disposition:
              description: File name for download
              schema:
                type: string
                example: attachment; filename="template_clientes_beel.csv"
            Content-Type:
              description: File MIME type
              schema:
                type: string
                example: text/csv; charset=utf-8
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/products:
    get:
      tags:
        - Products
      summary: List products/services from catalog
      description: Returns a paginated list of products/services with optional filters
      operationId: listProducts
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - products:read
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: category
          in: query
          description: Filter by product category
          schema:
            $ref: '#/components/schemas/ProductCategory'
        - name: active
          in: query
          description: Filter by active/inactive status
          schema:
            type: boolean
        - name: search
          in: query
          description: Global search by name, code or description
          schema:
            type: string
            maxLength: 100
        - name: name
          in: query
          description: Filter by name (partial search case-insensitive)
          schema:
            type: string
        - name: code
          in: query
          description: Filter by code (partial search)
          schema:
            type: string
        - name: min_price
          in: query
          description: Minimum price
          schema:
            type: number
            minimum: 0
        - name: max_price
          in: query
          description: Maximum price
          schema:
            type: number
            minimum: 0
        - name: sort_by
          in: query
          description: Field to sort by
          schema:
            type: string
            enum:
              - name
              - code
              - category
              - default_price
              - created_at
            default: name
        - name: sort_order
          in: query
          description: Sort order direction
          schema:
            type: string
            enum:
              - asc
              - desc
            default: asc
      responses:
        '200':
          description: Product list retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        required:
                          - products
                          - pagination
                        properties:
                          products:
                            type: array
                            items:
                              $ref: '#/components/schemas/Product'
                          pagination:
                            $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags:
        - Products
      summary: Create new product/service
      description: Creates a new product or service in the catalog
      operationId: createProduct
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - products:write
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProductRequest'
      responses:
        '201':
          description: Product created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Product'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Duplicate code or request already processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/products/{product_id}:
    parameters:
      - name: product_id
        in: path
        required: true
        description: Product unique UUID
        schema:
          type: string
          format: uuid
    get:
      tags:
        - Products
      summary: Get product by ID
      description: Retrieves details of a specific product
      operationId: getProduct
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - products:read
      responses:
        '200':
          description: Product retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      tags:
        - Products
      summary: Update product
      description: Updates an existing product
      operationId: updateProduct
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - products:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProductRequest'
      responses:
        '200':
          description: Product updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Product'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Duplicate code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      tags:
        - Products
      summary: Delete product
      description: Deletes a product from catalog
      operationId: deleteProduct
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - products:write
      responses:
        '204':
          description: Product deleted successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/products/search:
    get:
      tags:
        - Products
      summary: Search and autocomplete
      description: Optimized endpoint for active product autocomplete
      operationId: searchProducts
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - products:read
      parameters:
        - name: q
          in: query
          description: Search term (empty to get recent products)
          required: false
          schema:
            type: string
            maxLength: 100
        - name: limit
          in: query
          description: Result limit (max 20)
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 10
      responses:
        '200':
          description: Search results retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Product'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/products/bulk:
    post:
      tags:
        - Products
      summary: Create multiple products
      description: Creates multiple products in one operation (max 100)
      operationId: createProductsBulk
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - products:write
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - products
              properties:
                products:
                  type: array
                  items:
                    $ref: '#/components/schemas/CreateProductRequest'
                  minItems: 1
                  maxItems: 100
      responses:
        '200':
          description: Bulk operation completed
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          created_products:
                            type: array
                            items:
                              $ref: '#/components/schemas/Product'
                          errors:
                            type: array
                            items:
                              type: object
                              properties:
                                index:
                                  type: integer
                                code:
                                  type: string
                                name:
                                  type: string
                                error:
                                  type: string
                          summary:
                            type: object
                            properties:
                              total_processed:
                                type: integer
                              successful:
                                type: integer
                              failed:
                                type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: Payload too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      tags:
        - Products
      summary: Delete multiple products
      description: Deletes multiple products from catalog (max 100)
      operationId: deleteProductsBulk
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - products:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - product_ids
              properties:
                product_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  minItems: 1
                  maxItems: 100
      responses:
        '200':
          description: Bulk delete operation completed
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          deleted_products:
                            type: array
                            items:
                              type: string
                              format: uuid
                          errors:
                            type: array
                            items:
                              type: object
                              properties:
                                product_id:
                                  type: string
                                  format: uuid
                                error:
                                  type: string
                          summary:
                            type: object
                            properties:
                              total_processed:
                                type: integer
                              successful:
                                type: integer
                              failed:
                                type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: Payload too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/verifactu:
    get:
      tags:
        - ConfigurationVeriFactu
      summary: Get VeriFactu configuration
      description: Retrieves the current VeriFactu configuration
      operationId: getVeriFactuConfiguration
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - configuration:read
      responses:
        '200':
          description: Configuration retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/VeriFactuConfiguration'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      tags:
        - ConfigurationVeriFactu
      summary: Update VeriFactu configuration
      description: |
        Updates the user's VeriFactu configuration.

        **Business rules:**
        - If `enabled` is false, `apply_by_default` must also be false
        - If `apply_by_default` is true, `enabled` must be true
      operationId: updateVeriFactuConfiguration
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - configuration:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VeriFactuConfiguration'
      responses:
        '200':
          description: Configuration updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/VeriFactuConfiguration'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/tax-types:
    get:
      tags:
        - ConfigurationTax
      summary: Get complete tax types catalog
      description: |
        Retrieves the complete catalog of available tax types with structured information
        for tax configuration in Spain:

        - **Tax regimes**: VAT (Peninsula), IGIC (Canary Islands), IPSI (Ceuta/Melilla), OTHERS
        - **Percentages per regime**: Valid percentages for each tax type
        - **Regime codes**: VeriFactu codes for each tax type
        - **IRPF**: Available withholding percentages
        - **Equivalence surcharge**: Automatic mappings based on VAT percentage
        - **Utilities**: Validation functions and geographic auto-detection

        This endpoint provides all necessary information for the frontend
        without duplicating tax validation logic.
      operationId: getTaxTypes
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - taxes:read
      responses:
        '200':
          description: Catalog retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          tax_regimes:
                            type: array
                            items:
                              $ref: '#/components/schemas/TaxRegime'
                          irpf_types:
                            type: array
                            items:
                              $ref: '#/components/schemas/IrpfType'
                          equivalence_surcharges:
                            type: array
                            items:
                              $ref: '#/components/schemas/EquivalenceSurcharge'
                          exemption_reasons:
                            type: array
                            items:
                              $ref: '#/components/schemas/ExemptionReasonCatalogEntry'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/taxes:
    get:
      tags:
        - ConfigurationTax
      summary: Get user tax configuration
      description: |
        Retrieves the user's tax configuration, including:
        - Default tax regime (VAT, IGIC, IPSI, OTHERS)
        - Default main tax percentage
        - IRPF and equivalence surcharge configuration
      operationId: getTaxConfiguration
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - taxes:read
      responses:
        '200':
          description: Configuration retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/TaxConfiguration'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    put:
      tags:
        - ConfigurationTax
      summary: Update tax configuration
      description: Updates the user's tax configuration
      operationId: updateTaxConfiguration
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - configuration:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTaxConfigurationRequest'
      responses:
        '200':
          description: Configuration updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/TaxConfiguration'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/series:
    get:
      tags:
        - InvoiceSeries
      summary: List invoice series
      description: Retrieves all invoice series for the user
      operationId: listSeries
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - series:read
      parameters:
        - name: active
          in: query
          description: Filter by active/inactive series
          schema:
            type: boolean
        - name: document_type
          in: query
          description: Filter by document type (SIN_ASIGNAR series are always included)
          schema:
            $ref: '#/components/schemas/DocumentType'
      responses:
        '200':
          description: Series retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/InvoiceSeries'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      tags:
        - InvoiceSeries
      summary: Create invoice series
      description: |
        Creates a new invoice series.

        **Note:** The first series created by a user is automatically marked as default.
      operationId: createSeries
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - series:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSeriesRequest'
      responses:
        '201':
          description: Series created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/InvoiceSeries'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Duplicate series (same code)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/series/defaults:
    post:
      tags:
        - InvoiceSeries
      summary: Create default invoice series
      description: |
        Idempotently ensures the user has a default invoice series for each
        relevant `DocumentType` (FACTURA_ORDINARIA, FACTURA_SIMPLIFICADA,
        FACTURA_RECTIFICATIVA) in the current environment.

        - If a default already exists for a tipo, it is returned unchanged.
        - If a default is missing, a new series is created with code F/S/R and
          format `{CODIGO}-{YYYY}-{NUM:4}`, marked as default and active.
        - If the code is already in use by a manually-created series, that
          tipo is skipped (omitted from the response).

        Used by integration setup screens after connecting Stripe Connect, and
        as a recovery action for users who connected before automatic creation
        was wired in.
      operationId: createDefaultSeries
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - series:write
      responses:
        '200':
          description: Default series ensured
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/InvoiceSeries'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/series/defaults-status:
    get:
      tags:
        - InvoiceSeries
      summary: Get default series status per document type
      description: |
        Reports whether the user has a default invoice series for each
        `DocumentType` relevant to automatic invoicing flows (Stripe Connect, etc.).

        Used by integration setup screens to surface missing defaults at config
        time, preventing webhooks from being skipped at runtime with
        `failure.payment.skip.missing_default_series`.

        Environment is resolved via RLS from the request context — no input needed.
      operationId: getDefaultSeriesStatus
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - series:read
      responses:
        '200':
          description: Default series status retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/DocumentTypeDefaultStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/series/{series_id}:
    put:
      tags:
        - InvoiceSeries
      summary: Update invoice series
      description: |
        Updates an existing invoice series.

        Can only update:
        - Description
        - Active/inactive status
        - Whether it is default

        Cannot modify: code, prefix, number_format (to maintain consistency with existing invoices).

        Business rules:
        - Cannot deactivate a default series (must set another as default first)
        - Cannot mark an inactive series as default
      operationId: updateSeries
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - series:write
      parameters:
        - name: series_id
          in: path
          required: true
          description: Series ID
          schema:
            $ref: '#/components/schemas/UUID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSeriesRequest'
      responses:
        '200':
          description: Series updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/InvoiceSeries'
        '400':
          description: Cannot update (has associated invoices)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      tags:
        - InvoiceSeries
      summary: Delete series
      description: |
        Soft-deletes an invoice series. If the series is active, it is automatically
        deactivated before deletion. The series code becomes available for reuse.

        **Restriction:** Cannot delete the default series. Assign another series as default first.
      operationId: deleteSeries
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - series:write
      parameters:
        - name: series_id
          in: path
          required: true
          description: Series ID
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '204':
          description: Series deleted successfully
        '400':
          description: Cannot delete the default series
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/series/{series_id}/default:
    post:
      tags:
        - InvoiceSeries
      summary: Mark series as default
      description: |
        Marks an invoice series as the user's default.

        **Business rules:**
        - Only one series can be default per user
        - The series must be active (cannot mark an inactive series)
        - The previous default series is automatically unmarked

        **Note:** This operation is idempotent - calling it multiple times
        with the same series has no additional effects.
      operationId: setDefaultSeries
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - series:write
      parameters:
        - name: series_id
          in: path
          required: true
          description: Series ID to mark as default
          schema:
            $ref: '#/components/schemas/UUID'
      responses:
        '200':
          description: Series marked as default successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/InvoiceSeries'
        '400':
          description: Cannot mark as default (inactive series)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/language:
    put:
      tags:
        - ConfigurationPreferences
      summary: Update user preferred language
      description: |
        Updates the authenticated user's preferred language.

        This language is used for:
        - UI translations
        - Template names and colors in invoice customization
        - Emails sent to the user

        **Supported languages:**
        - `es` - Spanish (default)
        - `en` - English
        - `ca` - Catalan
      operationId: updateLanguage
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - configuration:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - language
              properties:
                language:
                  $ref: '#/components/schemas/Language'
            example:
              language: ca
      responses:
        '200':
          description: Language updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          language:
                            $ref: '#/components/schemas/Language'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/configuration/invoice-customization-options:
    get:
      tags:
        - ConfigurationPreferences
      summary: Get invoice customization options
      description: |
        Retrieves available option catalogs to customize invoices:

        **Template types:**
        - MODERN_TABLE: Structured table design, ideal for standard products/services
        - PROFESSIONAL_SERVICE: Text-based design, ideal for notaries/consultancies

        **Suggested color palette:**
        - BeeL default colors (orange, blue)
        - Professional palette (dark grays)
        - Creative palette (violet, pink, amber, emerald green)
        - Classic palette (blue, red, green, purple)

        **Usage:**
        User can choose a template and accent color to customize
        their invoices. These values are saved in their profile and automatically applied
        when generating new invoice PDFs.

        Template and color names are returned translated to the user's invoice language preference.
      operationId: getInvoiceCustomizationOptions
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - configuration:read
      responses:
        '200':
          description: Customization options retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/InvoiceCustomizationOptionsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/nif/validate:
    post:
      tags:
        - NIF
      summary: Validate NIF against AEAT registry
      description: |
        **Public endpoint** to validate a NIF/CIF against the AEAT registry using VeriFactu.

        This endpoint is useful for:
        - Pre-validation before registration (proactive feedback)
        - Validating customer NIFs before creating invoices
        - Verifying that a NIF exists in the registry

        **Behavior:**
        - **VALID**: The NIF exists in the AEAT registry. Returns the fiscal name.
        - **INVALID**: The NIF has correct format but doesn't exist in the registry.
        - **PENDING**: VeriFactu API is temporarily unavailable. The NIF has valid syntax but couldn't be verified against the registry.
        - **ERROR**: Technical error during validation (invalid syntax, timeout, etc.)

        **Note about PENDING:**
        If you use this endpoint for pre-validation during registration, a PENDING status
        indicates that the NIF has valid format and the user can continue. The system
        will validate the NIF automatically when the service becomes available.

        **Rate limiting:**
        - With API Key: 100 requests/hour
        - With Session Cookie: 1000 requests/hour

        **Authentication:**
        This endpoint accepts both API Keys and Session Cookies.
      operationId: validateNif
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - nif:validate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateNifRequest'
            example:
              nif: B12345678
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/ValidateNifResponse'
              examples:
                valid:
                  summary: Valid NIF (exists in AEAT registry)
                  value:
                    success: true
                    data:
                      valid: true
                      status: VALID
                      legal_name: JUAN PEREZ GARCIA
                      message: NIF successfully validated in AEAT registry
                      validated_at: '2025-01-15T10:30:00Z'
                    meta:
                      timestamp: '2025-01-15T10:30:00Z'
                      request_id: 123e4567-e89b-12d3-a456-426614174000
                invalid:
                  summary: Invalid NIF (not in AEAT registry)
                  value:
                    success: true
                    data:
                      valid: false
                      status: INVALID
                      legal_name: null
                      message: NIF does not exist in AEAT registry
                      validated_at: null
                    meta:
                      timestamp: '2025-01-15T10:30:00Z'
                      request_id: 123e4567-e89b-12d3-a456-426614174000
                pending:
                  summary: VeriFactu API unavailable (validation pending)
                  value:
                    success: true
                    data:
                      valid: false
                      status: PENDING
                      legal_name: null
                      message: NIF validation is pending. The system will validate it automatically when the service is available.
                      validated_at: null
                    meta:
                      timestamp: '2025-01-15T10:30:00Z'
                      request_id: 123e4567-e89b-12d3-a456-426614174000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies:
    post:
      operationId: createCompany
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Create a company (sub-account)
      description: |
        Creates a new NIF/company under the authenticated account.
        Invoice series are created automatically with default values.

        The `environment` field decides the creation flow:

        * **`TEST`** — created immediately, free. `201` + `CompanyData`.
        * **`PROD` + owner already `PRODUCTION_ACTIVE`** (card on file) —
          created immediately as a production NIF (`environment=PROD`). Real
          emission stays blocked by `NIF_REPRESENTATION_REQUIRED` until the
          AEAT representation is signed for this `(owner, nif)`. The new NIF
          is added to the subscription (pay-as-you-go). `201` + `CompanyData`.
        * **`PROD` + owner `SANDBOX_ONLY`** (no card) — no company is created
          yet. Returns `402` with `error.code=CHECKOUT_REQUIRED` and
          `error.details.checkout_url` (Stripe Checkout setup session). The
          company is materialized once the card is captured, confirmed via
          `POST /v1/companies/verify-creation` (or the
          `checkout.session.completed` webhook). `success_url`/`cancel_url`
          are required in this case.
      tags:
        - PublicCompanies
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCompanyRequest'
      responses:
        '201':
          description: Company created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyResponse201'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: |
            Payment method required before creating a production NIF. The
            owner account is `SANDBOX_ONLY` (no card on file). No company is
            created. `error.details.checkout_url` points to the Stripe Checkout
            setup session; after capturing the card the creation is confirmed
            via `POST /v1/companies/verify-creation`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutRequiredResponse'
        '403':
          description: |
            Multi-NIF (company) management is not available for this account's
            plan. `error.code=MULTI_NIF_NOT_ENABLED`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
    get:
      operationId: listCompanies
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: List all companies
      description: Returns all companies (sub-accounts) under the authenticated account.
      tags:
        - PublicCompanies
      responses:
        '200':
          description: List of companies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListCompanies200Response'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/stats:
    get:
      operationId: listCompanyStats
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: Per-company invoice aggregates
      description: |
        Returns invoice aggregates (count + last emission date) per company
        under the authenticated account. Decoupled from `GET /v1/companies`
        so the CompanySwitcher stays cheap; only callers that need the
        densities (e.g. the EmpresasPage table) pay the aggregate cost.
      tags:
        - PublicCompanies
      responses:
        '200':
          description: Per-company invoice stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListCompanyStats200Response'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/verify-creation:
    post:
      operationId: verifyCompanyCreation
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Confirm a Stripe Checkout result for a pending PROD company creation
      description: |
        Synchronously resolves a Stripe Checkout setup session so the frontend
        can finish creating a production NIF without depending on webhook
        delivery. The frontend calls this when Stripe redirects to the
        `success_url` with `?session_id={CHECKOUT_SESSION_ID}`.

        The backend retrieves the session from Stripe, validates its metadata
        (the pending creation intent stashed at `createCompany` time), captures
        the card on the account, and then materializes the production company
        from the stored payload. Idempotent — receiving both this call and the
        `checkout.session.completed` webhook is safe; the company is created
        once.

        This endpoint replaces the former `promotion/verify`: there is no
        `company_id` in the path because the company does not exist until the
        payment is confirmed.
      tags:
        - PublicCompanies
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyCreationRequest'
      responses:
        '200':
          description: |
            Verification resolved. When the payment was captured the response
            carries the newly created `CompanyData`. If the session is not yet
            paid, `company` is `null` and `status=PENDING`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyCreationResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/creation-status:
    get:
      operationId: getCompanyCreationStatus
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: Poll whether a pending PROD company creation was materialized
      description: |
        Lightweight, read-only status check for a pending PROD company
        creation. The frontend polls this after landing on the `success_url`
        while the `checkout.session.completed` webhook (the durable source of
        truth) materializes the company asynchronously.

        Unlike `POST /v1/companies/verify-creation`, this endpoint does **not**
        call Stripe and does **not** run the activation pipeline. It only reads
        the pending-creation buzón keyed by `session_id`:

        * buzón consumed → `status=CREATED` + the materialized `company`.
        * buzón exists, not yet consumed → `status=PENDING`, `company=null`.
        * no buzón for that `(session, owner)` → `422` `CHECKOUT_SESSION_MISMATCH`.

        Shares the `VerifyCreationResponse` shape with `verify-creation` so the
        frontend can treat both responses uniformly.
      tags:
        - PublicCompanies
      parameters:
        - name: session_id
          in: query
          required: true
          description: |
            Stripe Checkout Session ID returned in the `success_url` as the
            `{CHECKOUT_SESSION_ID}` template variable.
          schema:
            type: string
          example: cs_test_a1b2c3...
      responses:
        '200':
          description: |
            Status resolved from the buzón. `status=CREATED` carries the
            materialized `company`; `status=PENDING` has `company=null`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyCreationResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{company_id}:
    parameters:
      - name: company_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      operationId: getCompany
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: Get company details
      description: Returns details of a specific company including VeriFactu status.
      tags:
        - PublicCompanies
      responses:
        '200':
          description: Company details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyResponse201'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    patch:
      operationId: updateCompany
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Update company details
      description: |
        Updates editable fields of a company.
        NIF, legal name, and entity type are immutable after creation.
      tags:
        - PublicCompanies
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCompanyRequest'
      responses:
        '200':
          description: Company updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyResponse201'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      operationId: deleteCompany
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Delete a company
      description: |
        Deletes a company (sub-account). Cannot delete the primary profile.
      tags:
        - PublicCompanies
      responses:
        '204':
          description: Company deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/representations/{nif}/generate:
    parameters:
      - name: nif
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9]{8,15}$
    post:
      operationId: generateRepresentationByNif
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Generate unsigned representation PDF for a NIF
      description: |
        Generates the VeriFactu representation PDF for the given NIF using the
        fiscal profile of the caller's account. The caller must own at least
        one company with this NIF.
      tags:
        - PublicRepresentations
      responses:
        '200':
          description: PDF generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationActionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/representations/{nif}/download:
    parameters:
      - name: nif
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9]{8,15}$
    get:
      operationId: downloadRepresentationByNif
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: Download representation PDF for a NIF
      description: Returns a presigned URL (5min) to download the representation PDF.
      tags:
        - PublicRepresentations
      responses:
        '200':
          description: Presigned download URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationDownloadResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/representations/{nif}/sign:
    parameters:
      - name: nif
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9]{8,15}$
    post:
      operationId: signRepresentationByNif
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Submit a signed representation PDF for a NIF
      description: |
        Uploads the digitally signed representation PDF for the NIF.
        VeriFactu validates the signature asynchronously; on success the
        backend registers a `nif_firmas` row for the `(owner, nif)` pair and
        activates VeriFactu PROD for every sub-account of the owner with that
        NIF. After validation, all PROD sub-accounts of this NIF can emit
        real invoices.
      tags:
        - PublicRepresentations
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: Signed PDF file
      responses:
        '200':
          description: PDF submitted for validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationActionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/representations/{nif}/status:
    parameters:
      - name: nif
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9]{8,15}$
    get:
      operationId: getRepresentationStatusByNif
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: Get representation status for a NIF
      description: |
        Returns the current status of the VeriFactu representation for the
        given NIF. `ACTIVE` means the `nif_firmas` row is registered and
        production emission is unblocked for every sub-account of this NIF.
      tags:
        - PublicRepresentations
      responses:
        '200':
          description: Representation status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/representations/{nif}/cancel:
    parameters:
      - name: nif
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9]{8,15}$
    delete:
      operationId: cancelRepresentationByNif
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Cancel representation for a NIF
      description: |
        Cancels the current representation process for the given NIF.
        Production emission for every sub-account of this NIF is blocked
        again until a new signed PDF is submitted.
      tags:
        - PublicRepresentations
      responses:
        '200':
          description: Representation cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationActionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{company_id}/representation/generate:
    parameters:
      - name: company_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      operationId: generateRepresentation
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Generate unsigned representation PDF
      description: Generates the VeriFactu representation PDF for digital signature.
      tags:
        - PublicCompanyRepresentations
      responses:
        '200':
          description: PDF generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationActionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{company_id}/representation/download:
    parameters:
      - name: company_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      operationId: downloadRepresentation
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: Download representation PDF
      description: Returns a presigned URL (5min) to download the representation PDF.
      tags:
        - PublicCompanyRepresentations
      responses:
        '200':
          description: Presigned download URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationDownloadResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{company_id}/representation/submit:
    parameters:
      - name: company_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      operationId: submitRepresentation
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Submit signed representation PDF
      description: |
        Uploads the digitally signed representation PDF.
        VeriFactu will validate the signature asynchronously.
      tags:
        - PublicCompanyRepresentations
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: Signed PDF file
      responses:
        '200':
          description: PDF submitted for validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationActionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{company_id}/representation/status:
    parameters:
      - name: company_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      operationId: getRepresentationStatus
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: Get representation status
      description: Returns the current status of the VeriFactu representation process.
      tags:
        - PublicCompanyRepresentations
      responses:
        '200':
          description: Representation status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{company_id}/representation/cancel:
    parameters:
      - name: company_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    delete:
      operationId: cancelRepresentation
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Cancel representation
      description: Cancels the current representation process.
      tags:
        - PublicCompanyRepresentations
      responses:
        '200':
          description: Representation cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepresentationActionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{company_id}/api-keys:
    parameters:
      - name: company_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      operationId: createCompanyApiKey
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Create API key scoped to a company
      description: |
        Creates an API key that authenticates directly as this company.
        The created key cannot have more scopes than the parent key.
      tags:
        - PublicCompanyApiKeys
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCompanyApiKeyRequest'
      responses:
        '201':
          description: API key created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateCompanyApiKey201Response'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    get:
      operationId: listCompanyApiKeys
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:read
      summary: List API keys for a company
      description: Returns all API keys associated with this company.
      tags:
        - PublicCompanyApiKeys
      responses:
        '200':
          description: List of API keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListCompanyApiKeys200Response'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/companies/{company_id}/api-keys/{key_id}:
    parameters:
      - name: company_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: key_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    delete:
      operationId: revokeCompanyApiKey
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - business_profiles:write
      summary: Revoke an API key
      description: Revokes (deletes) an API key associated with this company.
      tags:
        - PublicCompanyApiKeys
      responses:
        '204':
          description: API key revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /v1/webhooks:
    post:
      tags:
        - Webhooks
      summary: Create webhook subscription
      description: Register an HTTPS endpoint to receive real-time event notifications. Every delivery is signed with BeeL-Signature (HMAC-SHA256). The secret is returned once on creation — store it securely. Maximum 10 active webhook subscriptions per account.
      operationId: createWebhookSubscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookSubscriptionRequest'
            example:
              url: https://yourapp.com/webhooks/beel
              events:
                - verifactu.status.updated
      callbacks:
        webhookEvent:
          '{$request.body#/url}':
            post:
              summary: Webhook event delivered to your endpoint
              description: |
                Beel sends HTTP POST to your URL when a subscribed event occurs.
                Your endpoint must respond with HTTP 2xx within 10 seconds.
                Failed deliveries are retried up to 5 times with exponential backoff.
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/WebhookEvent'
                    examples:
                      verifactuAccepted:
                        summary: VeriFactu submission accepted by AEAT
                        value:
                          id: 3f7a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c
                          type: verifactu.status.updated
                          created_at: '2026-03-07T16:20:26Z'
                          api_version: YYYY-MM
                          livemode: true
                          data:
                            invoice_id: 550e8400-e29b-41d4-a716-446655440000
                            invoice_number: 2025-001
                            verifactu_registration_id: 660e8400-e29b-41d4-a716-446655440001
                            previous_status: PENDING
                            new_status: ACCEPTED
                            qr_url: https://sede.agenciatributaria.gob.es/Sede/verifactu?id=ABC123XYZ
                            qr_base64: iVBORw0KGgoAAAANSUhEUgAAAMg...
                            invoice_hash: B11F3A015173AD99075E2720F61E2DE1FF08CBFEDD85C6F73C77AD835301B2A3
                            error_code: null
                            error_message: null
                      verifactuRejected:
                        summary: VeriFactu submission rejected by AEAT
                        value:
                          id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                          type: verifactu.status.updated
                          created_at: '2026-03-07T16:22:10Z'
                          api_version: YYYY-MM
                          livemode: true
                          data:
                            invoice_id: 550e8400-e29b-41d4-a716-446655440000
                            invoice_number: 2025-001
                            verifactu_registration_id: 660e8400-e29b-41d4-a716-446655440001
                            previous_status: PENDING
                            new_status: REJECTED
                            qr_url: null
                            qr_base64: null
                            invoice_hash: null
                            error_code: '1105'
                            error_message: NIF del emisor no registrado en VeriFactu
              responses:
                2XX:
                  description: Your endpoint acknowledged the event successfully.
                4XX:
                  description: Client error — delivery will NOT be retried.
                5XX:
                  description: Server error — delivery will be retried with exponential backoff.
              headers:
                BeeL-Signature:
                  required: true
                  schema:
                    type: string
                    example: t=1741362026,v1=3c4f7a2e1b9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f
                BeeL-Event:
                  required: true
                  schema:
                    type: string
                    example: verifactu.status.updated
                BeeL-Event-Id:
                  required: true
                  description: UUID del evento webhook — idéntico en todos los reintentos del mismo evento lógico. Coincide con el campo `id` del payload.
                  schema:
                    type: string
                    format: uuid
                BeeL-Delivery-Id:
                  required: true
                  description: UUID del intento de entrega — único por cada llamada HTTP, incluyendo reintentos. Coincide con el `id` del log de entrega en el dashboard.
                  schema:
                    type: string
                    format: uuid
                Idempotency-Key:
                  required: true
                  description: Mismo UUID que `BeeL-Event-Id`. Permite al receptor deduplicar entregas automáticamente.
                  schema:
                    type: string
                    format: uuid
      responses:
        '201':
          description: Webhook subscription created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/WebhookSubscriptionWithSecret'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
    get:
      tags:
        - Webhooks
      summary: List webhook subscriptions
      description: Returns all webhook subscriptions for the authenticated account.
      operationId: listWebhookSubscriptions
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
      responses:
        '200':
          description: List of webhook subscriptions
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          webhooks:
                            type: array
                            items:
                              $ref: '#/components/schemas/WebhookSubscription'
                          pagination:
                            $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/webhooks/{webhook_id}:
    patch:
      tags:
        - Webhooks
      summary: Update webhook subscription
      description: |
        Partially updates a webhook subscription. Only provided fields are updated.

        Use this endpoint to:
        - **Change the endpoint URL** (`url`)
        - **Change subscribed events** (`events`)
        - **Enable or disable** the subscription (`active`)
      operationId: updateWebhookSubscription
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookSubscriptionRequest'
      responses:
        '200':
          description: Webhook subscription updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/WebhookSubscription'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      tags:
        - Webhooks
      summary: Delete webhook subscription
      description: Permanently deletes a webhook subscription. No further events will be delivered.
      operationId: deleteWebhookSubscription
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Subscription deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks/{webhook_id}/deliveries:
    get:
      tags:
        - Webhooks
      summary: List webhook delivery logs
      description: Returns the last 50 delivery attempts for a webhook subscription.
      operationId: listWebhookDeliveries
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: List of delivery logs
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/WebhookDeliveryLog'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/retry:
    post:
      tags:
        - Webhooks
      summary: Retry a failed webhook delivery
      description: |
        Immediately retries a webhook delivery using the original payload.
        Creates a new delivery log entry with the result.
      operationId: retryWebhookDelivery
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: delivery_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Retry result
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/WebhookDeliveryLog'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks/{webhook_id}/secret:
    post:
      tags:
        - Webhooks
      summary: Rotate webhook secret
      description: |
        Generates a new HMAC secret for a webhook subscription.
        The old secret is **immediately invalidated** — update your signature verification
        logic before rotating to avoid missing events during the transition.
        The new secret is only returned **once** in this response.
      operationId: rotateWebhookSecret
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Secret rotated — new secret returned (only shown once)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/WebhookSubscriptionWithSecret'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  parameters:
    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      description: |
        Idempotency key to prevent duplicates in sensitive operations.

        - Any unique client-generated string (e.g. an order id). A UUID also works but is not required
        - Allowed characters: letters, digits, `_` and `-` (max 255 chars)
        - If the same key is sent twice, the result of the first operation is returned
        - Keys expire after processing
      required: false
      schema:
        type: string
        pattern: ^[a-zA-Z0-9_-]+$
        maxLength: 255
      example: invoice-order-12345
    LocaleHeader:
      name: X-Locale
      in: header
      description: |
        Preferred language for response messages and localized content.

        Supported values: es (Spanish), en (English), ca (Catalan)
        Falls back to Accept-Language header if not provided.
      required: false
      schema:
        type: string
        enum:
          - es
          - en
          - ca
        default: es
      example: es
    ActiveProfileHeader:
      name: X-Active-Profile
      in: header
      description: |
        UUID of the business profile (sub-account / company) to operate as for this request.

        Use with a master account API key to act on behalf of any owned sub-account
        without provisioning a separate key per company. Without this header the
        request operates as the API key owner.

        Requirements:
        - The API key must belong to a master account (sub-account keys ignore this header).
        - The API key must have the `business_profiles:read` scope.
        - The target profile must be a sub-account owned by the key owner.

        Returns `403 Forbidden` if the profile is not owned by the key owner.
      required: false
      schema:
        type: string
        format: uuid
      example: 550e8400-e29b-41d4-a716-446655440000
    PageParam:
      name: page
      in: query
      description: Page number (starts at 1)
      schema:
        type: integer
        minimum: 1
        default: 1
      example: 1
    LimitParam:
      name: limit
      in: query
      description: Number of items per page
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
      example: 20
  schemas:
    SuccessResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          description: Response data (type varies by endpoint - can be object or array)
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    ErrorResponse:
      type: object
      description: |
        Error response shared by all Beel APIs.

        The payload carries **two contracts at once** (additive, non-breaking):

        - **Legacy** (`success`, `error.{code,message,details}`, `meta`) — kept
          intact for existing consumers.
        - **RFC 9457** (`type`, `title`, `detail`, `instance`) — new fields
          for integrators following Problem Details for HTTP APIs. The
          `type` URI is the stable, shareable link to the error's
          documentation page (e.g. `https://docs.beel.es/errors/{code}`).

        Future migration: the legacy fields will be deprecated via
        `Deprecation`/`Sunset` headers after a sufficient adoption window,
        and the response Content-Type will move to
        `application/problem+json`.
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
        error:
          $ref: '#/components/schemas/ErrorDetail'
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        type:
          type: string
          format: uri
          description: |
            URI estable que identifica el tipo de problema y donde el
            integrador encontrará su documentación.
          example: https://docs.beel.es/errors/INVOICE_NO_LINES
        title:
          type: string
          description: |
            Resumen corto del tipo de problema. Estable entre ocurrencias
            del mismo `type`.
          example: INVOICE_NO_LINES
        detail:
          type: string
          description: |
            Mensaje específico para esta ocurrencia, localizado según
            `Accept-Language`. Coincide con `error.message` legacy.
          example: La factura debe tener al menos una línea
        instance:
          type: string
          format: uri-reference
          description: |
            URI que identifica la ocurrencia concreta del problema —
            típicamente el path del recurso afectado.
          example: /v1/invoices/abc-123
    ErrorDetail:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: VALIDATION_ERROR
        message:
          type: string
          example: The provided data is not valid
        details:
          type: object
          additionalProperties: true
          example:
            field: specific error message
    ResponseMeta:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
          example: '2025-01-15T10:30:00Z'
        request_id:
          type: string
          example: 4bf92f3577b34da6a3ce929d0e0e4736
    Pagination:
      type: object
      required:
        - current_page
        - total_pages
        - total_items
        - items_per_page
      properties:
        current_page:
          type: integer
          example: 1
        total_pages:
          type: integer
          example: 5
        total_items:
          type: integer
          example: 87
        items_per_page:
          type: integer
          example: 20
        has_next:
          type: boolean
        has_previous:
          type: boolean
    LegalRepresentative:
      type: object
      required:
        - full_name
        - nif
        - address
      description: |
        Legal representative data for a legal entity.
        Only used when entity_type = LEGAL_ENTITY.
      properties:
        full_name:
          type: string
          minLength: 1
          maxLength: 255
          description: Full name of the legal representative
          example: María García López
        nif:
          type: string
          pattern: ^(\d{8}[A-Z]|[ABCDEFGHJKLMNPQRSUVW]\d{7}[A-Z0-9]|[XYZ]\d{7}[A-Z])$
          minLength: 9
          maxLength: 9
          description: Tax ID of the legal representative (DNI/CIF/NIE)
          example: 12345678A
        address:
          allOf:
            - $ref: '#/components/schemas/Address'
            - description: Address of the legal representative
    Address:
      type: object
      required:
        - street
        - number
        - postal_code
        - city
        - province
      properties:
        street:
          type: string
          minLength: 1
          maxLength: 255
          pattern: ^[a-zA-Z0-9À-ÿ\u0100-\u017F\u00B7\s\.,\-\/'ºª°:;"()&#]+$
          description: Full address (street, number, floor, etc.) - Latin characters only
          example: Calle Mayor, 123
        number:
          type: string
          minLength: 1
          maxLength: 20
          description: Street number
          example: '123'
        floor:
          type: string
          maxLength: 10
          description: Floor or level
          example: 2º A
        door:
          type: string
          maxLength: 10
          description: Door or apartment
          example: A
        postal_code:
          type: string
          minLength: 1
          maxLength: 20
          description: Postal code (5 digits for Spain, free format for other countries)
          example: '28001'
        city:
          type: string
          minLength: 1
          maxLength: 100
          pattern: ^[a-zA-Z0-9À-ÿ\u0100-\u017F\u00B7\u2018\u2019\u0060\u00B4\s\.,\-\/'ºª()]+$
          description: City or town - Latin characters only
          example: Madrid
        province:
          type: string
          minLength: 1
          maxLength: 100
          pattern: ^[a-zA-Z0-9À-ÿ\u0100-\u017F\u00B7\u2018\u2019\u0060\u00B4\s\.,\-\/'ºª]+$
          description: Province or state - Latin characters only
          example: Madrid
        country:
          type: string
          minLength: 1
          maxLength: 100
          pattern: ^[a-zA-Z0-9À-ÿ\u0100-\u017F\u00B7\u2018\u2019\u0060\u00B4\s\.,\-\/'ºª]+$
          default: España
          description: Country - Latin characters only
          example: España
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
          minLength: 2
          maxLength: 2
          default: ES
          description: ISO 3166-1 alpha-2 country code
          example: ES
    PaymentMethod:
      type: string
      description: |
        Payment method for invoices and recurring invoices.
        NONE means no payment information will be shown.
      enum:
        - NONE
        - BANK_TRANSFER
        - CARD
        - CASH
        - CHECK
        - DIRECT_DEBIT
        - BIZUM
        - OTHER
      example: BANK_TRANSFER
    EntityType:
      type: string
      description: |
        Taxpayer type.
        INDIVIDUAL: Natural person (individual self-employed).
        LEGAL_ENTITY: Legal entity (company with legal form: SL, SA, etc.).
      enum:
        - INDIVIDUAL
        - LEGAL_ENTITY
      example: INDIVIDUAL
    Language:
      type: string
      description: Supported languages
      enum:
        - es
        - en
        - ca
      default: es
      example: es
    VatCategory:
      type: string
      description: VAT type
      enum:
        - GENERAL
        - REDUCED
        - SUPER_REDUCED
        - EXEMPT
      example: GENERAL
    IrpfCategory:
      type: string
      description: Personal income tax/withholding type
      enum:
        - PROFESSIONAL_15
        - PROFESSIONAL_7
        - AGRICULTURAL_2
        - LIVESTOCK_2
        - FORESTRY_2
        - BUSINESS_ACTIVITIES_1
        - EXEMPT
      example: PROFESSIONAL_15
    CommonTaxRegime:
      $ref: '#/components/schemas/TaxRegime-2'
    CommonVatPercentage:
      $ref: '#/components/schemas/VatPercentage'
    IrpfPercentage:
      type: integer
      enum:
        - 0
        - 1
        - 2
        - 7
        - 15
        - 19
        - 24
      description: |
        Personal income tax/withholding percentage in integer format.
        Allowed values: 0 (exempt), 1 (agricultural/livestock/forestry), 2 (reduced for modules), 7, 15, 19, 24 (non-residents).
      example: 15
    EquivalenceSurchargePercentage:
      type: number
      enum:
        - 0
        - 0.5
        - 0.625
        - 1.4
        - 5.2
      description: |
        Equivalence surcharge percentage in decimal format.
        Pairs allowed (rate ↔ recargo): 4↔0.5, 5↔0.625 (RD-ley 11/2022), 10↔1.4, 21↔5.2.
        The backend automatically normalizes equivalent formats (5.20 → 5.2).
      example: 5.2
    NifValidationStatus:
      type: string
      description: |
        Tax ID validation status against the AEAT tax registry.

        - PENDING: Not yet validated or VeriFactu API temporarily unavailable
        - VALID: Successfully validated against the AEAT tax registry
        - INVALID: Invalid tax ID or does not exist in the AEAT tax registry
        - ERROR: Technical error after exhausting retries
      enum:
        - PENDING
        - VALID
        - INVALID
        - ERROR
      example: VALID
    UUID:
      type: string
      format: uuid
      description: Universally Unique Identifier (UUID v4)
      example: 550e8400-e29b-41d4-a716-446655440000
    NIF:
      type: string
      pattern: ^(\d{8}[A-Z]|[ABCDEFGHJKLMNPQRSUVW]\d{7}[A-Z0-9]|[XYZ]\d{7}[A-Z])$
      minLength: 9
      maxLength: 9
      description: |
        Spanish Tax ID (9 characters, uppercase only). Structural validation:
        - DNI: 8 digits + 1 letter (e.g., 12345678A)
        - NIE: X/Y/Z + 7 digits + 1 letter (e.g., X1234567A)
        - CIF: Organization letter + 7 digits + 1 control digit/letter (e.g., B12345678)
      example: 12345678A
    Email:
      type: string
      format: email
      minLength: 5
      maxLength: 255
      description: Email address (minimum valid email is 5 chars, e.g. a@b.co)
      example: user@example.com
    Phone:
      type: string
      minLength: 9
      maxLength: 20
      pattern: ^[+]?[0-9\s\-\(\)]+$
      description: Phone number. Allows digits, spaces, dashes, parentheses, and optional leading +
      example: +34 612 345 678
    IBAN:
      type: string
      pattern: ^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$
      minLength: 15
      maxLength: 34
      description: |
        IBAN (International Bank Account Number).
        Required when payment method is BANK_TRANSFER.
      example: ES1234567890123456789012
    SWIFT:
      type: string
      pattern: ^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$
      minLength: 8
      maxLength: 11
      description: SWIFT/BIC code
      example: ABCDESMMXXX
    Invoice:
      type: object
      required:
        - id
        - series
        - type
        - status
        - issue_date
        - issuer
        - recipient
        - lines
        - totals
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique invoice UUID
          example: 550e8400-e29b-41d4-a716-446655440000
        invoice_number:
          type: string
          nullable: true
          example: 2025/0001
          description: |
            Complete invoice number (series + sequential).
            **Null for draft invoices** — assigned automatically when issued.
        series:
          $ref: '#/components/schemas/SeriesInfo'
        number:
          type: integer
          nullable: true
          example: 1
          description: |
            Sequential number within the series.
            **Null for draft invoices** — assigned automatically when issued.
        type:
          $ref: '#/components/schemas/InvoiceType'
        status:
          $ref: '#/components/schemas/InvoiceStatus'
        issue_date:
          type: string
          format: date
          description: |
            Invoice issue date. Always set to the current date when the invoice is created.
            If the operation occurred on a different date, use `operation_date`.
          example: '2025-01-15'
        operation_date:
          type: string
          format: date
          nullable: true
          description: |
            Date when the operation actually occurred. Used when invoicing for a past operation.
            If null, the operation date is the same as the issue date.
          example: '2025-01-10'
        due_date:
          type: string
          format: date
          description: Payment due date (must be the same as or after `issue_date`)
          example: '2025-02-14'
        payment_date:
          type: string
          format: date
          description: |
            Business date when the payment was received (e.g., the date on the bank statement).
            Set by the user when marking the invoice as paid. Only present when status is PAID.
            Contrast with `paid_at`, which is the system timestamp of when the status change was recorded.
          example: '2025-01-20'
        sent_at:
          type: string
          format: date-time
          nullable: true
          description: |
            System timestamp when the invoice was marked as sent.
            Present when status is SENT or later.
          example: '2025-01-29T18:45:00Z'
        paid_at:
          type: string
          format: date-time
          nullable: true
          description: |
            System timestamp when the payment was recorded in the system.
            Automatically set when the invoice status changes to PAID.
            Contrast with `payment_date`, which is the business date chosen by the user.
          example: '2025-02-05T10:30:00Z'
        auto_emit_after:
          type: string
          format: date
          nullable: true
          description: |
            Date when this draft will be auto-emitted if not manually issued.
            Only present for drafts created from recurring invoices with preview_days > 0.
          example: '2025-03-20'
        scheduled_for:
          type: string
          format: date
          nullable: true
          description: |
            Date when the invoice should be automatically processed.
            Only present when status is SCHEDULED.
          example: '2025-02-15'
        scheduled_action:
          $ref: '#/components/schemas/GenerationAction'
        issuer:
          $ref: '#/components/schemas/IssuerData'
        recipient:
          $ref: '#/components/schemas/RecipientData'
        lines:
          type: array
          items:
            $ref: '#/components/schemas/InvoiceLine'
          minItems: 1
        totals:
          $ref: '#/components/schemas/InvoiceTotals'
        payment_info:
          $ref: '#/components/schemas/PaymentInfo'
        notes:
          type: string
          maxLength: 1000
          description: Additional observations or notes
        rectified_invoice_id:
          type: string
          format: uuid
          description: UUID of the invoice being rectified (only for corrective invoices)
        rectification_reason:
          type: string
          maxLength: 500
          description: Reason for rectification (only for corrective invoices)
        recurring_invoice_id:
          type: string
          format: uuid
          nullable: true
          description: UUID of the recurring invoice that generated this invoice (if any)
        recurring_invoice_name:
          type: string
          nullable: true
          description: Name of the recurring invoice (denormalized for display)
        rectification_type:
          $ref: '#/components/schemas/RectificationType'
        rectification_code:
          $ref: '#/components/schemas/VeriFactuRectificationCode'
        metadata:
          type: object
          additionalProperties: true
          description: |
            Additional metadata in key-value format. Invoices auto-generated from a
            connected payment platform carry system keys you can filter on:
            - external_customer_id: Payment-platform customer (e.g. Stripe `cus_…`), present when the payment carried a customer (absent on flows with no customer, e.g. Terminal / payment links without customer collection)
            - external_payment_id: Canonical payment reference (PaymentIntent / Charge / Stripe Invoice id)
            - payment_intent_id: Stripe PaymentIntent id, when the payment has one
            - charge_id: Stripe Charge id, when the payment has one
            - payment_provider: Origin platform (e.g. STRIPE_CONNECT)
            Plus any keys you set yourself on manually-created invoices (order ids, tenants, …).
            See the "Filtering by metadata" guide for the full list and query rules.
          example:
            external_customer_id: cus_ULGk8bzIr88aag
            external_payment_id: pi_3NqFGb2eZvKYlo2C0z1234AB
            payment_intent_id: pi_3NqFGb2eZvKYlo2C0z1234AB
            charge_id: ch_3NqFGb2eZvKYlo2C1234CDEF
            payment_provider: STRIPE_CONNECT
            external_order_id: ORD-2025-0042
        send_automatically:
          type: boolean
          nullable: true
          description: |
            Whether the invoice will be automatically sent by email after issuing.
            Only relevant for DRAFT and SCHEDULED invoices.
        email_config:
          allOf:
            - $ref: '#/components/schemas/EmailConfiguration'
          nullable: true
          description: |
            Email configuration used when `send_automatically` is true.
            If null, the recipient's default email is used.
        pdf_download_url:
          type: string
          nullable: true
          description: |
            Relative URL to download the PDF via the API endpoint.
            Path is relative to the API base URL (e.g., https://api.beel.es/api).
            Only present when the invoice has a generated PDF.
          example: /v1/invoices/550e8400-e29b-41d4-a716-446655440000/pdf
        verifactu:
          $ref: '#/components/schemas/VeriFactu'
        attachments:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              url:
                type: string
              type:
                type: string
        sending_history:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              date:
                type: string
                format: date-time
              recipient:
                $ref: '#/components/schemas/Email'
              status:
                type: string
                enum:
                  - SENT
                  - FAILED
              error:
                type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type: string
          format: date-time
          nullable: true
    SeriesInfo:
      type: object
      required:
        - id
        - code
      properties:
        id:
          type: string
          format: uuid
          description: Invoice series UUID
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        code:
          type: string
          description: Alphanumeric series code
          example: FAC
          pattern: ^[A-Z0-9\-_]{1,50}$
    InvoiceType:
      type: string
      enum:
        - STANDARD
        - CORRECTIVE
        - SIMPLIFIED
      description: |
        - STANDARD: Standard invoice
        - CORRECTIVE: Corrects or cancels a previous invoice
        - SIMPLIFIED: Simplified invoice without all requirements (up to 400€, or 3,000€ with NIF)
    InvoiceStatus:
      type: string
      enum:
        - SCHEDULED
        - DRAFT
        - ISSUED
        - SENT
        - PAID
        - OVERDUE
        - RECTIFIED
        - VOIDED
      description: |
        - SCHEDULED: Scheduled invoice to be issued automatically on a future date
        - DRAFT: Draft invoice not sent yet (modifiable)
        - ISSUED: Finalized invoice with definitive number but not sent
        - SENT: Invoice sent to customer
        - PAID: Invoice paid
        - OVERDUE: Overdue invoice (not paid after due date)
        - RECTIFIED: Partially corrected invoice (one or more PARTIAL corrective invoices)
        - VOIDED: Completely cancelled invoice (TOTAL corrective invoice)
    RectificationType:
      type: string
      enum:
        - TOTAL
        - PARTIAL
      description: |
        Type of rectification applied to a corrective invoice:
        - TOTAL: Completely cancels the original invoice (status → VOIDED)
        - PARTIAL: Partially corrects the original invoice (status → RECTIFIED)
    VeriFactuRectificationCode:
      type: string
      enum:
        - R1
        - R2
        - R3
        - R4
        - R5
      description: |
        Rectification codes according to VeriFactu regulations (AEAT):
        - R1: Error founded in law and Art. 80 One, Two and Six LIVA
        - R2: Article 80 Three LIVA (Bankruptcy proceedings)
        - R3: Article 80 Four LIVA (Uncollectable debts)
        - R4: Other causes
        - R5: Simplified invoices (Art. 80 One and Two LIVA) - ONLY for simplified invoices
    GenerationAction:
      type: string
      enum:
        - DRAFT
        - ISSUE_AND_SEND
      description: |
        Action to perform when processing a scheduled invoice:
        - DRAFT: Create as draft for manual review
        - ISSUE_AND_SEND: Issue and send automatically via email
    IssuerData:
      type: object
      required:
        - legal_name
        - nif
        - address
      properties:
        legal_name:
          type: string
          minLength: 1
          maxLength: 255
          description: Issuer legal name
          example: Juan Pérez García
        trade_name:
          type: string
          minLength: 1
          maxLength: 255
          nullable: true
          description: Issuer trade name (optional)
          example: JP Web Development
        nif:
          type: string
          pattern: ^[A-Za-z0-9]{9}$
          minLength: 9
          maxLength: 9
          description: |
            Spanish Tax ID (9 alphanumeric characters).
            Valid formats:
            - DNI: 8 digits + letter (e.g., 12345678A)
            - NIE: X/Y/Z + 7 digits + letter (e.g., X1234567A)
            - CIF: Letter + 7 digits + digit/letter (e.g., B12345678)
          example: 12345678A
        address:
          $ref: '#/components/schemas/Address'
        phone:
          $ref: '#/components/schemas/Phone'
        email:
          $ref: '#/components/schemas/Email'
        web:
          type: string
          minLength: 1
          maxLength: 500
          nullable: true
          description: Issuer website (optional)
          example: https://beel.es
        logo_url:
          type: string
          minLength: 1
          maxLength: 500
          nullable: true
          description: Issuer logo URL (optional)
        additional_info:
          type: string
          maxLength: 500
          nullable: true
          description: |
            Additional issuer information (collegiate number, professional registration, etc.)
          example: 'Nº Colegiado: 12345'
    RecipientData:
      type: object
      required:
        - legal_name
      description: |
        Recipient data in the invoice response.
        Note: For simplified invoices, only legal_name may be present.
        NIF and address are optional for SIMPLIFIED type invoices.
      properties:
        customer_id:
          type: string
          format: uuid
          nullable: true
          description: Customer UUID in the system (optional)
        legal_name:
          type: string
          minLength: 1
          maxLength: 255
          description: Recipient legal name
          example: Empresa SL
        trade_name:
          type: string
          minLength: 1
          maxLength: 255
          nullable: true
          description: Recipient trade name (optional)
          example: Empresa
        nif:
          type: string
          pattern: ^[A-Za-z0-9]{9}$
          minLength: 9
          maxLength: 9
          nullable: true
          description: |
            Spanish Tax ID (9 alphanumeric characters).
            Optional for simplified invoices (SIMPLIFIED type).
            Valid formats:
            - DNI: 8 digits + letter (e.g., 12345678A)
            - NIE: X/Y/Z + 7 digits + letter (e.g., X1234567A)
            - CIF: Letter + 7 digits + digit/letter (e.g., B12345678)
          example: 12345678A
        alternative_id:
          allOf:
            - $ref: '#/components/schemas/AlternativeIdentifier'
            - description: Alternative identifier for foreign customers (mutually exclusive with nif)
        address:
          allOf:
            - $ref: '#/components/schemas/Address'
          description: Address (optional for simplified invoices)
        phone:
          $ref: '#/components/schemas/Phone'
        email:
          $ref: '#/components/schemas/Email'
    InvoiceLine:
      type: object
      required:
        - description
        - quantity
        - unit_price
        - line_total
      properties:
        description:
          type: string
          minLength: 1
          maxLength: 500
          description: Description of the invoiced concept
          example: Web application development
        quantity:
          type: number
          description: Product/service quantity (can be negative in corrective invoices)
          example: 40
        unit:
          type: string
          default: hours
          example: hours
        unit_price:
          type: number
          minimum: 0
          exclusiveMinimum: true
          maximum: 999999.9999
          example: 50
          description: |
            Unit price before taxes.
            Supports up to 4 decimal places for micro-pricing (e.g., €0.0897/unit for labels, packaging).
            Final amounts are always rounded to 2 decimals.
        discount_percentage:
          type: number
          minimum: 0
          maximum: 100
          default: 0
          example: 10
          description: Discount percentage applied (0-100)
        main_tax:
          $ref: '#/components/schemas/TaxInfo'
        equivalence_surcharge_rate:
          $ref: '#/components/schemas/EquivalenceSurchargePercentage'
        irpf_rate:
          $ref: '#/components/schemas/IrpfPercentage'
        exemption_reason:
          $ref: '#/components/schemas/ExemptionReason'
          nullable: true
        exemption_reason_text:
          type: string
          nullable: true
          maxLength: 500
          description: Custom exemption text. Only used when exemption_reason is OTRO.
        taxable_base:
          type: number
          example: 1800
          description: Line taxable base (after discount, can be negative in corrective invoices)
        line_total:
          type: number
          example: 2178
          description: Line total with taxes (can be negative in corrective invoices)
    InvoiceTotals:
      type: object
      required:
        - taxable_base
        - total_vat
        - total_equivalence_surcharge
        - total_irpf
        - invoice_total
      properties:
        taxable_base:
          type: number
          example: 2000
          description: Total taxable base (can be negative in corrective invoices)
        total_discounts:
          type: number
          minimum: 0
          default: 0
          example: 0
        vat_breakdown:
          type: array
          items:
            type: object
            required:
              - type
              - base
              - amount
            properties:
              type:
                type: number
                example: 21
              base:
                type: number
                example: 2000
              amount:
                type: number
                example: 420
        total_vat:
          type: number
          example: 420
          description: Total VAT (can be negative in corrective invoices)
        surcharge_breakdown:
          type: array
          items:
            type: object
            required:
              - type
              - base
              - amount
            properties:
              type:
                type: number
              base:
                type: number
              amount:
                type: number
        total_equivalence_surcharge:
          type: number
          default: 0
          example: 0
          description: Total equivalence surcharge (can be negative in corrective invoices)
        irpf_breakdown:
          type: array
          items:
            type: object
            required:
              - type
              - base
              - amount
            properties:
              type:
                type: number
              base:
                type: number
              amount:
                type: number
        total_irpf:
          type: number
          default: 0
          example: 300
          description: Total personal income tax withheld (can be negative in corrective invoices)
        invoice_total:
          type: number
          example: 2120
          description: Total amount to pay (base + VAT + RE - IRPF, can be negative in corrective invoices)
    PaymentInfo:
      type: object
      properties:
        method:
          allOf:
            - $ref: '#/components/schemas/PaymentMethod'
          default: BANK_TRANSFER
          description: |
            Preferred payment method.
            If NONE is selected, no payment information will be shown on the invoice.
        iban:
          $ref: '#/components/schemas/IBAN'
        swift:
          $ref: '#/components/schemas/SWIFT'
        payment_term_days:
          type: integer
          minimum: 0
          maximum: 365
          default: 30
          nullable: true
          description: Payment term in days (optional, default 30)
          example: 30
    VeriFactu:
      type: object
      description: |
        VeriFactu compliance information (optional).
        Some self-employed workers may be required to use this AEAT system,
        while others may choose not to.
      properties:
        enabled:
          type: boolean
          default: false
          description: Whether VeriFactu is enabled for this invoice
        invoice_hash:
          type: string
          description: Invoice SHA-256 hash according to VeriFactu regulations
          example: 3A5B7C9D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7A8B
        chaining_hash:
          type: string
          description: Chaining hash with previous invoice
          example: 7F8E9D0C1B2A3F4E5D6C7B8A9F0E1D2C3B4A5F6E7D8C9B0A1F2E3D4C5B6A7F8
        registration_number:
          type: string
          description: Registration number in the VeriFactu system
          example: VERIFACTU2025000001
        qr_url:
          type: string
          description: QR code URL for verification
          example: https://verifactu.agenciatributaria.gob.es/v?id=ABC123XYZ
        qr_base64:
          type: string
          nullable: true
          description: |
            QR code as base64-encoded PNG for embedding in custom PDFs.
            Only present when submission_status = ACCEPTED.
          example: iVBORw0KGgoAAAANSUhEUgAAAMgAAADI...
        registration_date:
          type: string
          format: date-time
          description: VeriFactu registration date and time
        submission_status:
          type: string
          enum:
            - PENDING
            - SENT
            - ACCEPTED
            - VOIDED
            - REJECTED
          description: Submission status to AEAT
        error_code:
          type: string
          nullable: true
          description: Error code returned by AEAT. Present when submission_status is REJECTED.
          example: '3000'
        error_message:
          type: string
          nullable: true
          description: Human-readable error description returned by AEAT. Present when submission_status is REJECTED.
          example: Factura ya existe en el sistema
    Recipient:
      type: object
      properties:
        customer_id:
          type: string
          format: uuid
          description: |
            UUID of a registered customer. If present, the invoice uses the customer's
            stored data and all other recipient fields are ignored.
          example: 4f244735-980b-8d9c-80e8-6331fa0b1958
        legal_name:
          type: string
          minLength: 1
          maxLength: 255
          description: |
            Recipient legal name. Required when customer_id is not provided
            (except for SIMPLIFIED invoices where all fields are optional).
          example: Tech Solutions SL
        trade_name:
          type: string
          minLength: 1
          maxLength: 255
          nullable: true
          description: Recipient trade name (optional)
          example: TechSol
        nif:
          type: string
          pattern: ^[A-Za-z0-9]{9}$
          minLength: 9
          maxLength: 9
          description: |
            Spanish Tax ID (9 alphanumeric characters).
            Required when customer_id is not provided and alternative_id is absent.
            Optional for SIMPLIFIED invoices (with NIF: limit 3000€, without: limit 400€).
          example: B12345678
        alternative_id:
          allOf:
            - $ref: '#/components/schemas/AlternativeIdentifier'
            - description: Alternative identifier for foreign customers (mutually exclusive with nif)
        address:
          $ref: '#/components/schemas/Address'
        phone:
          $ref: '#/components/schemas/Phone'
        email:
          $ref: '#/components/schemas/Email'
      example:
        customer_id: 4f244735-980b-8d9c-80e8-6331fa0b1958
    CreateInvoiceRequest:
      type: object
      required:
        - type
        - recipient
        - lines
      properties:
        type:
          $ref: '#/components/schemas/InvoiceType'
        series_id:
          type: string
          format: uuid
          description: Invoicing series ID (if not specified, uses default)
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        operation_date:
          type: string
          format: date
          description: |
            Date when the operation actually occurred. Optional.

            Use when invoicing for a past operation (e.g., services delivered last month
            but invoiced this month). **Must be today or a past date.**

            If omitted, the operation date is assumed to be the same as the issue date (today).

            The `issue_date` is always set automatically to today per Spanish anti-fraud law
            (Ley Antifraude / VeriFactu). To issue an invoice on a future date, create a
            draft and use `POST /v1/invoices/{id}/schedule`.
          example: '2025-01-10'
        due_date:
          type: string
          format: date
          description: |
            Payment due date. If not specified, calculated according to payment method.
            **Must be the same as or after the issue date (today).**
          example: '2025-02-14'
        recipient:
          $ref: '#/components/schemas/Recipient'
        lines:
          type: array
          items:
            type: object
            required:
              - description
              - quantity
              - unit_price
            properties:
              description:
                type: string
                minLength: 1
                maxLength: 500
                description: Description of invoiced concept
                example: Web application development - Sprint 1
              quantity:
                type: number
                description: Product/service quantity (can be negative for franchises or discounts)
                example: 40
              unit:
                type: string
                example: hours
              unit_price:
                type: number
                minimum: 0
                exclusiveMinimum: true
                maximum: 999999.9999
                description: |
                  Unit price before taxes.
                  Supports up to 4 decimal places for micro-pricing (e.g., €0.0897/unit for labels, packaging).
                example: 50
              discount_percentage:
                type: number
                minimum: 0
                maximum: 100
                default: 0
                example: 10
                description: Discount percentage applied (0-100)
              main_tax:
                $ref: '#/components/schemas/TaxInfo'
              equivalence_surcharge_rate:
                $ref: '#/components/schemas/EquivalenceSurchargePercentage'
              irpf_rate:
                $ref: '#/components/schemas/IrpfPercentage'
              exemption_reason:
                $ref: '#/components/schemas/ExemptionReason'
                nullable: true
              exemption_reason_text:
                type: string
                nullable: true
                maxLength: 500
                description: Custom exemption text. Only used when exemption_reason is OTRO.
          minItems: 1
          example:
            - description: Web application development - Sprint 1
              quantity: 40
              unit: hours
              unit_price: 50
              discount_percentage: 0
              main_tax:
                type: IVA
                percentage: 21
                regime_key: '01'
              irpf_rate: 15
        payment_info:
          $ref: '#/components/schemas/PaymentInfo'
        notes:
          type: string
          maxLength: 1000
          example: Payment by bank transfer. Includes technical support for 30 days.
        rectified_invoice_id:
          type: string
          format: uuid
          description: Required if type is "CORRECTIVE"
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        rectification_reason:
          type: string
          maxLength: 500
          description: Required if type is "CORRECTIVE"
          example: Amount correction due to calculation error in hours
        metadata:
          type: object
          additionalProperties: true
          description: |
            Your own key/value pairs to cross-reference this invoice with records in
            your system (order ids, tenants, internal codes). Namespace them to avoid
            clashing with the system keys BeeL adds on payment-generated invoices.
          example:
            external_order_id: ORD-2025-0042
            tenant: acme
            project_code: PROJ-123
        options:
          $ref: '#/components/schemas/InvoiceProcessingOptions'
      example:
        type: STANDARD
        series_id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        issue_date: '2025-01-15'
        due_date: '2025-02-14'
        recipient:
          customer_id: 4f244735-980b-8d9c-80e8-6331fa0b1958
        lines:
          - description: Web application development - Sprint 1
            quantity: 40
            unit: hours
            unit_price: 50
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
            irpf_rate: 15
        payment_info:
          method: BANK_TRANSFER
          iban: ES9121000418450200051332
          payment_term_days: 30
        notes: Payment by bank transfer. Includes technical support for 30 days.
        metadata:
          project_code: PROJ-123
          client_reference: REF-2025-001
        options:
          verifactu_enabled: false
          issue_directly: true
          wait_for_pdf: false
          send_automatically: false
    InvoiceProcessingOptions:
      type: object
      description: |
        Controls how the invoice is processed after creation.
        All fields default to `false` if not specified.

        **Common combinations:**
        - Draft (default): omit `options` or set all to `false`
        - Issue immediately: `{ issue_directly: true }`
        - Issue + wait for PDF: `{ issue_directly: true, wait_for_pdf: true }`
        - Issue + send email: `{ issue_directly: true, send_automatically: true }`
        - Full automation: `{ issue_directly: true, wait_for_pdf: true, send_automatically: true, email_config: { ... } }`
      properties:
        verifactu_enabled:
          type: boolean
          default: false
          description: Whether VeriFactu information should be generated for this invoice
        issue_directly:
          type: boolean
          default: false
          description: |
            If `true`, creates the invoice directly as **ISSUED** with a definitive number and PDF.
            If `false` (default), creates as **DRAFT** without number (editable, no PDF).
        wait_for_pdf:
          type: boolean
          default: false
          description: |
            Only applies when `issue_directly` is `true`.
            If `true`, waits for PDF generation before returning the response (~1-3s).
            If `false` (default), PDF is generated asynchronously in the background.
        send_automatically:
          type: boolean
          default: false
          description: |
            Only applies when `issue_directly` is `true`.
            If `true`, sends the invoice by email with PDF attachment after issuing.
            Email is sent asynchronously via the outbox pattern.
        email_config:
          allOf:
            - $ref: '#/components/schemas/EmailConfiguration'
          description: |
            Only applies when `send_automatically` is `true`.
            Overrides default email settings. If not provided, uses the recipient's email.
      example:
        verifactu_enabled: false
        issue_directly: true
        wait_for_pdf: false
        send_automatically: false
    UpdateInvoiceRequest:
      type: object
      description: |
        Partial update for a DRAFT invoice. Only fields present in the body are applied;
        omitted fields preserve their existing value.
      properties:
        type:
          allOf:
            - $ref: '#/components/schemas/InvoiceType'
          description: |
            New invoice type. Allowed transitions: `STANDARD` ↔ `SIMPLIFIED`.
            Transitions to/from `CORRECTIVE` are rejected (corrective invoices have a
            dedicated `POST /v1/invoices/{invoice_id}/corrective` endpoint).
        series_id:
          allOf:
            - $ref: '#/components/schemas/UUID'
          description: |
            Series ID. If provided, changes the invoice series (only for DRAFT invoices).
            The invoice number will be reassigned from the new series when issued.
        operation_date:
          type: string
          format: date
          nullable: true
          description: |
            Date when the operation occurred. **Must be today or a past date.**
            Set to null to clear (operation date = issue date).
            If not provided, keeps the existing value.
        due_date:
          type: string
          format: date
          description: New due date. Must be the same as or after `issue_date`.
        recipient:
          allOf:
            - $ref: '#/components/schemas/Recipient'
          description: |
            Replaces the recipient when present. Provide `customer_id` to switch to a
            registered client, or inline `legal_name`/`nif`/`address` for an ad-hoc receptor.
            Omit to keep the current recipient.
        lines:
          type: array
          items:
            type: object
            required:
              - description
              - quantity
              - unit_price
            properties:
              description:
                type: string
                minLength: 1
                maxLength: 500
              quantity:
                type: number
              unit:
                type: string
              unit_price:
                type: number
                minimum: 0
                exclusiveMinimum: true
                maximum: 999999.9999
                description: |
                  Unit price before taxes.
                  Supports up to 4 decimal places for micro-pricing (e.g., €0.0897/unit for labels, packaging).
                  Final amounts are always rounded to 2 decimals.
              discount_percentage:
                type: number
              main_tax:
                $ref: '#/components/schemas/TaxInfo'
              equivalence_surcharge_rate:
                $ref: '#/components/schemas/EquivalenceSurchargePercentage'
              irpf_rate:
                $ref: '#/components/schemas/IrpfPercentage'
              exemption_reason:
                $ref: '#/components/schemas/ExemptionReason'
                nullable: true
              exemption_reason_text:
                type: string
                nullable: true
                maxLength: 500
        payment_info:
          allOf:
            - $ref: '#/components/schemas/PaymentInfo'
          description: |
            Replaces the payment information when present (sets method/IBAN/SWIFT/term days).
            Omit to keep the current payment info.
        notes:
          type: string
        options:
          type: object
          description: |
            Processing options for the draft. Fields are optional and follow partial update
            semantics: omitted fields preserve the existing value.
          properties:
            send_automatically:
              type: boolean
              description: Whether the invoice should be auto-emailed after issuing.
            email_config:
              allOf:
                - $ref: '#/components/schemas/EmailConfiguration'
              nullable: true
              description: Email configuration for auto-send. null clears the existing config.
            verifactu_enabled:
              type: boolean
              description: Whether VeriFactu submission is enabled at issue time.
    EmailConfiguration:
      type: object
      required:
        - recipients
      properties:
        recipients:
          type: array
          items:
            $ref: '#/components/schemas/Email'
          minItems: 1
          description: List of recipient emails (at least 1 required)
          example:
            - client@example.com
        cc:
          type: array
          items:
            $ref: '#/components/schemas/Email'
          description: List of CC emails (optional)
          example:
            - copy@example.com
        subject:
          type: string
          minLength: 1
          maxLength: 200
          description: Custom email subject (optional, if not specified uses a default)
          example: Invoice 2025/0001 - Web development services
        message:
          type: string
          minLength: 1
          maxLength: 2000
          description: Custom message (optional, added to email body)
          example: Dear customer, please find attached the invoice for the services provided. Thank you for your trust.
      example:
        recipients:
          - client@example.com
        cc:
          - accounting@example.com
        subject: Invoice 2025/0001 - Development services
        message: Please find attached the requested invoice. We remain at your disposal for any clarification.
    SendEmailRequest:
      type: object
      properties:
        recipients:
          type: array
          items:
            $ref: '#/components/schemas/Email'
          description: If not specified, uses the customer's email
        cc:
          type: array
          items:
            $ref: '#/components/schemas/Email'
        subject:
          type: string
          minLength: 1
          maxLength: 200
          description: Email subject (optional, if not specified uses a default)
        message:
          type: string
          minLength: 1
          maxLength: 2000
          description: Custom message (optional, added before standard message)
        attach_pdf:
          type: boolean
          default: true
        language:
          $ref: '#/components/schemas/Language'
    CreateCorrectiveInvoiceRequest:
      type: object
      required:
        - rectification_type
        - rectification_code
        - reason
      properties:
        rectification_type:
          $ref: '#/components/schemas/RectificationType'
        rectification_code:
          $ref: '#/components/schemas/VeriFactuRectificationCode'
        reason:
          type: string
          minLength: 10
          maxLength: 1000
          description: Detailed reason for rectification (minimum 10 characters)
          example: Amount correction due to calculation error in hours worked during the project
        lines:
          type: array
          items:
            type: object
            required:
              - description
              - quantity
              - unit_price
            properties:
              description:
                type: string
                minLength: 1
                maxLength: 500
                description: Concept description
                example: Adjustment for incorrectly invoiced hours
              quantity:
                type: number
                description: Quantity (can be negative for corrective invoices)
                example: -10
              unit:
                type: string
                example: hours
              unit_price:
                type: number
                maximum: 999999.9999
                description: |
                  Unit price before taxes (can be negative in corrective invoices).
                  Supports up to 4 decimal places for micro-pricing (e.g., €0.0897/unit for labels, packaging).
                  Final amounts are always rounded to 2 decimals.
                example: 50
              discount_percentage:
                type: number
                minimum: 0
                maximum: 100
                default: 0
              main_tax:
                $ref: '#/components/schemas/TaxInfo'
              equivalence_surcharge_rate:
                $ref: '#/components/schemas/EquivalenceSurchargePercentage'
              irpf_rate:
                $ref: '#/components/schemas/IrpfPercentage'
              exemption_reason:
                $ref: '#/components/schemas/ExemptionReason'
                nullable: true
              exemption_reason_text:
                type: string
                nullable: true
                maxLength: 500
          description: |
            **TOTAL**: Optional (if not sent, original invoice lines are copied negated)
            **PARTIAL**: REQUIRED (adjustment lines with positive or negative amounts)
        notes:
          type: string
          maxLength: 1000
          description: Additional observations about the rectification
          example: Rectification requested by the customer due to quantity error
        series_id:
          type: string
          format: uuid
          description: Series for the corrective invoice (optional, if not specified uses the original invoice's series)
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        options:
          $ref: '#/components/schemas/InvoiceProcessingOptions'
      example:
        rectification_type: PARTIAL
        rectification_code: R4
        reason: Amount correction due to calculation error in hours worked during the project
        lines:
          - description: Adjustment for hours error - Sprint 1
            quantity: -5
            unit: hours
            unit_price: 50
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
            irpf_rate: 15
        notes: Rectification agreed with the customer on 2025-01-20
        options:
          verifactu_enabled: false
          issue_directly: true
          send_automatically: false
    InvoicePdfResponse:
      type: object
      required:
        - success
        - data
        - meta
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required:
            - download_url
            - expires_in_seconds
            - file_name
          properties:
            download_url:
              type: string
              format: uri
              description: Pre-signed URL to download the PDF (valid for 5 minutes)
              example: https://minio.beel.es/beel-invoices/invoices/user-uuid/factura-uuid.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...
            expires_in_seconds:
              type: integer
              description: Seconds until the URL expires
              example: 300
              minimum: 1
            file_name:
              type: string
              description: Suggested filename for download
              example: factura_2025-001.pdf
              pattern: ^factura_.+\.pdf$
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    Customer:
      type: object
      required:
        - id
        - legal_name
        - address
        - created_at
      properties:
        id:
          type: string
          format: uuid
          example: 123e4567-e89b-12d3-a456-426614174000
        legal_name:
          type: string
          example: Empresa Cliente SL
        trade_name:
          type: string
          example: EmpresaCliente
        nif:
          $ref: '#/components/schemas/NIF'
        address:
          $ref: '#/components/schemas/Address'
        phone:
          $ref: '#/components/schemas/Phone'
        email:
          type: string
          format: email
          minLength: 5
          maxLength: 255
          nullable: true
          description: Email address (minimum valid email is 5 chars, e.g. a@b.co)
        web:
          type: string
          maxLength: 255
          nullable: true
          description: Website URL
          pattern: ^(https?://.+|)$
        billing_emails:
          type: array
          items:
            $ref: '#/components/schemas/Email'
          description: Additional emails for invoice delivery
        contact_person:
          type: string
          example: María García
        notes:
          type: string
          maxLength: 1000
        preferred_payment_method:
          $ref: '#/components/schemas/PaymentInfo'
        general_discount:
          type: number
          minimum: 0
          maximum: 100
          default: 0
        active:
          type: boolean
          default: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        alternative_id:
          $ref: '#/components/schemas/AlternativeIdentifier'
    AlternativeIdentifier:
      type: object
      nullable: true
      required:
        - type
        - number
      description: |
        Alternative identifier for customers without Spanish Tax ID.

        ### VeriFactu rules (enforced server-side, returns `422 ALTERNATIVE_ID_INVALID` on violation)
        - If `country_code = ES`, then `type` **must** be `PASSPORT` (03) or `NOT_REGISTERED` (07).
        - If `type = NOT_REGISTERED` (07), then `country_code` **must** be `ES`.

        ### Matrix of allowed combinations
        | `type`                 | `country_code = ES` | `country_code ≠ ES` |
        |------------------------|:-------------------:|:-------------------:|
        | `NIF_IVA` (02)         | ✗                   | ✓                   |
        | `PASSPORT` (03)        | ✓                   | ✓                   |
        | `COUNTRY_ID` (04)      | ✗                   | ✓                   |
        | `RESIDENCE_CERTIFICATE` (05) | ✗             | ✓                   |
        | `OTHER_DOCUMENT` (06)  | ✗                   | ✓                   |
        | `NOT_REGISTERED` (07)  | ✓                   | ✗                   |
      properties:
        type:
          type: string
          enum:
            - NIF_IVA
            - PASSPORT
            - COUNTRY_ID
            - RESIDENCE_CERTIFICATE
            - OTHER_DOCUMENT
            - NOT_REGISTERED
            - '02'
            - '03'
            - '04'
            - '05'
            - '06'
            - '07'
          description: |
            Identifier type. Use descriptive names:
            - **NIF_IVA**: VAT-ID (intra-community EU) — *not allowed when `country_code = ES`*
            - **PASSPORT**: Passport — *allowed for any country*
            - **COUNTRY_ID**: Country of residence ID — *not allowed when `country_code = ES`*
            - **RESIDENCE_CERTIFICATE**: Residence certificate — *not allowed when `country_code = ES`*
            - **OTHER_DOCUMENT**: Other supporting document — *not allowed when `country_code = ES`*
            - **NOT_REGISTERED**: Not registered in AEAT — *requires `country_code = ES`*

            **⚠️ DEPRECATED numeric codes** (will be removed in v2):
            02, 03, 04, 05, 06, 07 — use the descriptive names above instead.
        number:
          type: string
          minLength: 1
          maxLength: 20
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
          minLength: 2
          maxLength: 2
          description: |
            ISO 3166-1 alpha-2 country code. Constrains the allowed `type` values;
            see the VeriFactu rules on the parent schema.
    CreateCustomerRequest:
      type: object
      required:
        - legal_name
        - address
      properties:
        legal_name:
          type: string
          minLength: 1
          maxLength: 120
          pattern: ^\S.*$
          description: Customer legal name (required)
        trade_name:
          type: string
          maxLength: 120
          nullable: true
          description: Customer trade name (optional)
        nif:
          allOf:
            - $ref: '#/components/schemas/NIF'
          description: Spanish Tax ID (required if id_otro is not provided)
        alternative_id:
          $ref: '#/components/schemas/AlternativeIdentifier'
        address:
          $ref: '#/components/schemas/Address'
        phone:
          $ref: '#/components/schemas/Phone'
        email:
          type: string
          format: email
          minLength: 5
          maxLength: 255
          nullable: true
          description: Email address (minimum valid email is 5 chars, e.g. a@b.co)
        web:
          type: string
          maxLength: 255
          nullable: true
          description: Website URL
          pattern: ^(https?://.+|)$
        billing_emails:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Email'
          description: Additional emails for invoice delivery (optional)
        contact_person:
          type: string
          maxLength: 200
          nullable: true
          description: Contact person name (optional)
        notes:
          type: string
          nullable: true
          description: Additional notes about the customer (optional)
        preferred_payment_method:
          $ref: '#/components/schemas/PaymentInfo'
        general_discount:
          type: number
          minimum: 0
          maximum: 100
          nullable: true
          description: General discount percentage (optional)
    UpdateCustomerRequest:
      type: object
      description: |
        Request to update an existing customer.
        NOTE: The tax identifier (NIF/id_otro) CANNOT be modified once the customer is created.
      properties:
        legal_name:
          type: string
          minLength: 1
          maxLength: 120
          description: Customer legal name
        trade_name:
          type: string
          maxLength: 120
          nullable: true
          description: Customer trade name (optional)
        address:
          $ref: '#/components/schemas/Address'
        phone:
          $ref: '#/components/schemas/Phone'
        email:
          type: string
          format: email
          minLength: 5
          maxLength: 255
          nullable: true
          description: Email address (minimum valid email is 5 chars, e.g. a@b.co)
        web:
          type: string
          maxLength: 255
          nullable: true
          description: Website URL
          pattern: ^(https?://.+|)$
        billing_emails:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Email'
          description: Additional emails for invoice delivery (optional)
        contact_person:
          type: string
          maxLength: 200
          nullable: true
          description: Contact person name (optional)
        notes:
          type: string
          nullable: true
          description: Additional notes about the customer (optional)
        preferred_payment_method:
          $ref: '#/components/schemas/PaymentInfo'
        general_discount:
          type: number
          minimum: 0
          maximum: 100
          nullable: true
          description: General discount percentage (optional)
        active:
          type: boolean
          description: Whether the customer is active or inactive
    CsvImportResult:
      type: object
      required:
        - total_processed
        - successful
        - failed
        - duplicates_skipped
      properties:
        total_processed:
          type: integer
          minimum: 0
          description: Total records processed from CSV
          example: 150
        successful:
          type: integer
          minimum: 0
          description: Number of customers successfully created
          example: 145
        failed:
          type: integer
          minimum: 0
          description: Number of records that failed
          example: 5
        duplicates_skipped:
          type: integer
          minimum: 0
          description: Number of records skipped as duplicates (same Tax ID)
          example: 3
        created_customers:
          type: array
          items:
            $ref: '#/components/schemas/Customer'
          description: List of successfully created customers
        errors:
          type: array
          items:
            $ref: '#/components/schemas/CsvValidationError'
          description: Error details by row
        warnings:
          type: array
          items:
            type: string
          description: Non-critical warnings (missing optional fields)
          example:
            - 'Row 2: Email not provided for customer ''TechSolutions Barcelona SA'''
            - 'Row 5: Invalid phone number for customer ''María López Fernández'''
    CsvValidationError:
      type: object
      required:
        - row
        - field
        - value
        - error
      properties:
        row:
          type: integer
          minimum: 1
          description: Row number with error (1-based, excluding header)
          example: 12
        field:
          type: string
          description: Field name with error
          example: nif
        value:
          type: string
          description: Value that caused the error
          example: 12345678X
        error:
          type: string
          description: Detailed error message
          example: 'Invalid Tax ID: incorrect format'
    TemplateCsvInfo:
      type: object
      required:
        - filename
        - headers
        - example_rows
      properties:
        filename:
          type: string
          description: Template file name
          example: template_customers_example.csv
        headers:
          type: array
          items:
            type: string
          description: Required CSV headers
          example:
            - legal_name
            - nif
            - email
            - phone
            - address_street
            - address_number
            - address_postal_code
            - address_city
            - address_province
        example_rows:
          type: integer
          description: Number of example rows in the template
          example: 4
        max_records:
          type: integer
          description: Maximum number of allowed records
          example: 1000
        max_file_size_mb:
          type: number
          description: Maximum file size in MB
          example: 5
    CsvImportPreviewResult:
      type: object
      required:
        - metadata
        - customers_preview
        - statistics
      description: |
        Complete result of a CSV import with structured preview.
        Contains all parsed records, statistics and processing metadata.
      properties:
        metadata:
          $ref: '#/components/schemas/CsvImportMetadata'
        customers_preview:
          type: array
          items:
            $ref: '#/components/schemas/CsvCustomerPreview'
          description: Complete list of parsed customers with their validation status
        statistics:
          $ref: '#/components/schemas/CsvImportStatistics'
    CsvImportMetadata:
      type: object
      required:
        - filename
        - file_size_bytes
        - total_rows
        - is_dry_run
        - processing_time_ms
      description: Metadata about the processed CSV file
      properties:
        filename:
          type: string
          description: Original file name
          example: customers_january.csv
        file_size_bytes:
          type: integer
          description: File size in bytes
          example: 1048576
        total_rows:
          type: integer
          description: Total rows processed (excluding headers)
          example: 250
        is_dry_run:
          type: boolean
          description: Whether it was a test processing without persistence
          example: true
        processing_time_ms:
          type: integer
          description: Processing time in milliseconds
          example: 3450
    CsvCustomerPreview:
      type: object
      required:
        - row_number
        - customer
        - status
        - errors
        - warnings
      description: |
        Preview of an individual customer parsed from CSV.
        Includes the parsed customer, validation status and specific errors/warnings.
      properties:
        row_number:
          type: integer
          description: Row number in CSV (starting from 1)
          example: 15
        customer:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/Customer'
          description: Parsed customer (null if there are critical parsing errors)
        status:
          $ref: '#/components/schemas/ValidationStatus'
        errors:
          type: array
          items:
            $ref: '#/components/schemas/CsvValidationError'
          description: List of validation errors for this customer
        warnings:
          type: array
          items:
            type: string
          description: List of warnings (customer processable but with observations)
          example:
            - 'Row 15: Email not provided for customer ''Empresa ABC'''
    ValidationStatus:
      type: string
      enum:
        - VALID
        - WARNING
        - ERROR
        - DUPLICATE
        - NIF_INVALID
      description: |
        Validation status of the parsed customer:
        - VALID: Valid customer, ready to import
        - WARNING: Valid customer but with non-critical warnings
        - ERROR: Invalid customer, cannot be processed
        - DUPLICATE: Duplicate customer (in CSV or database)
        - NIF_INVALID: Tax ID not valid according to AEAT registry (VeriFactu)
    CsvImportStatistics:
      type: object
      required:
        - total_processed
        - successful
        - with_warnings
        - failed
        - duplicates
        - invalid_nifs
        - success_rate
      description: Calculated statistics from CSV processing
      properties:
        total_processed:
          type: integer
          description: Total records processed
          example: 250
        successful:
          type: integer
          description: Valid customers without warnings
          example: 180
        with_warnings:
          type: integer
          description: Valid customers but with warnings
          example: 45
        failed:
          type: integer
          description: Customers with validation errors
          example: 15
        duplicates:
          type: integer
          description: Duplicate customers (in CSV or DB)
          example: 8
        invalid_nifs:
          type: integer
          description: Customers with invalid Tax IDs in AEAT
          example: 2
        success_rate:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: Success rate (successful + with_warnings) / total
          example: 0.9
    Product:
      type: object
      required:
        - id
        - name
        - category
        - main_tax
        - active
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique UUID of the product/service
          example: 550e8400-e29b-41d4-a716-446655440000
        code:
          type: string
          maxLength: 50
          pattern: ^[a-zA-Z0-9_-]*$
          nullable: true
          description: Unique alphanumeric product code (optional)
          example: SERV-001
        name:
          type: string
          maxLength: 255
          description: Product/service name
          example: Technical consulting
        description:
          type: string
          description: Detailed product/service description (optional)
          example: Specialized technical consulting services in web development
        category:
          $ref: '#/components/schemas/ProductCategory'
        default_price:
          type: number
          minimum: 0
          multipleOf: 0.0001
          description: Suggested default price (optional)
          example: 85.5
        unit:
          type: string
          maxLength: 50
          description: Unit of measure (optional)
          example: hours
        main_tax:
          $ref: '#/components/schemas/TaxInfo'
        equivalence_surcharge:
          type: number
          minimum: 0
          maximum: 100
          multipleOf: 0.01
          description: Equivalence surcharge percentage (optional)
          example: 5.2
        irpf:
          type: number
          minimum: 0
          maximum: 100
          multipleOf: 0.01
          description: IRPF withholding percentage (optional)
          example: 15
        active:
          type: boolean
          description: Indicates whether the product is active
          example: true
        created_at:
          type: string
          format: date-time
          description: Creation date and time
          example: '2025-01-18T10:30:00Z'
        updated_at:
          type: string
          format: date-time
          description: Last update date and time
          example: '2025-01-18T15:45:30Z'
    ProductCategory:
      type: string
      enum:
        - PRODUCT
        - SERVICE
        - CONSULTING
        - SOFTWARE
        - TRAINING
        - OTHER
      description: |
        Product/service category:
        * PRODUCT - Physical, tangible products
        * SERVICE - General services
        * CONSULTING - Consulting and advisory services
        * SOFTWARE - Development, licenses, SaaS
        * TRAINING - Courses, workshops, training
        * OTHER - Other unclassified types
      example: CONSULTING
    CreateProductRequest:
      type: object
      required:
        - name
      properties:
        code:
          type: string
          maxLength: 50
          pattern: ^[a-zA-Z0-9_-]*$
          nullable: true
          description: Unique alphanumeric product code (optional)
          example: SERV-001
        name:
          type: string
          maxLength: 255
          description: Product/service name
          example: Technical consulting
        description:
          type: string
          description: Detailed description (optional)
          example: Specialized technical consulting services
        category:
          $ref: '#/components/schemas/ProductCategory'
        default_price:
          type: number
          minimum: 0
          multipleOf: 0.0001
          description: Suggested default price (optional)
          example: 85.5
        unit:
          type: string
          maxLength: 50
          description: Unit of measure (optional)
          example: hours
        main_tax:
          $ref: '#/components/schemas/TaxInfo'
        equivalence_surcharge:
          type: number
          minimum: 0
          maximum: 100
          multipleOf: 0.01
          description: Equivalence surcharge percentage (optional)
          example: 5.2
        irpf:
          type: number
          minimum: 0
          maximum: 100
          multipleOf: 0.01
          description: IRPF withholding percentage (optional)
          example: 15
    UpdateProductRequest:
      type: object
      properties:
        code:
          type: string
          maxLength: 50
          pattern: ^[a-zA-Z0-9_-]*$
          nullable: true
          description: Unique alphanumeric product code (optional)
          example: SERV-001
        name:
          type: string
          maxLength: 255
          description: Product/service name
          example: Technical consulting
        description:
          type: string
          description: Detailed description (optional)
          example: Specialized technical consulting services
        category:
          $ref: '#/components/schemas/ProductCategory'
        default_price:
          type: number
          minimum: 0
          multipleOf: 0.0001
          description: Suggested default price (optional)
          example: 85.5
        unit:
          type: string
          maxLength: 50
          description: Unit of measure (optional)
          example: hours
        main_tax:
          $ref: '#/components/schemas/TaxInfo'
        equivalence_surcharge:
          type: number
          minimum: 0
          maximum: 100
          multipleOf: 0.01
          description: Equivalence surcharge percentage (optional)
          example: 5.2
        irpf:
          type: number
          minimum: 0
          maximum: 100
          multipleOf: 0.01
          description: IRPF withholding percentage (optional)
          example: 15
        active:
          type: boolean
          description: Indicates whether the product is active
          example: true
    ProductResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessResponse'
        - type: object
          properties:
            data:
              $ref: '#/components/schemas/Product'
    ProductsListResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                products:
                  type: array
                  items:
                    $ref: '#/components/schemas/Product'
                pagination:
                  $ref: '#/components/schemas/Pagination'
    ProductQueryParams:
      type: object
      properties:
        category:
          $ref: '#/components/schemas/ProductCategory'
        active:
          type: boolean
          description: Filter by active/inactive status
          example: true
        search:
          type: string
          maxLength: 100
          description: Search in name, description or code
          example: consulting
        page:
          type: integer
          minimum: 0
          default: 0
          description: Page number (0-indexed)
          example: 0
        size:
          type: integer
          minimum: 1
          maximum: 100
          default: 20
          description: Page size
          example: 20
    SeriesFormat:
      type: string
      minLength: 1
      maxLength: 255
      pattern: ^[A-Z0-9\-_/{}:]*$
      example: '{CODIGO}-{YYYY}-{NUM:4}'
      description: |
        Format template with available variables (UPPERCASE ONLY):
        - {CODIGO}: Series code (e.g., "FAC")
        - {YYYY}: Year with 4 digits (e.g., "2025")
        - {YY}: Year with 2 digits (e.g., "25")
        - {MM}: Month with 2 digits (e.g., "01")
        - {NUM}: Sequential number without padding (e.g., "1")
        - {NUM:X}: Sequential number with padding (e.g., {NUM:4} → "0001")

        **REQUIRED**: Must contain at least {NUM} or {NUM:X}
        **IMPORTANT**: Only uppercase (rejects {yy}, {mm}, {codigo}, etc.)

        Valid examples:
        - "{CODIGO}-{YYYY}-{NUM:4}" → "FAC-2025-0001"
        - "{CODIGO}/{NUM:6}" → "FAC/000001"
        - "{YYYY}{MM}-{NUM:3}" → "202501-001"
    CounterReset:
      type: string
      enum:
        - NEVER
        - ANNUAL
        - MONTHLY
      default: ANNUAL
      description: |
        Counter reset policy:
        - NEVER: Counter never resets (continuous numbering)
        - ANNUAL: Counter resets yearly
        - MONTHLY: Counter resets monthly
    SeriesCode:
      type: string
      pattern: ^[A-Z0-9\-_]{1,50}$
      minLength: 1
      maxLength: 50
      example: FAC
      description: |
        Alphanumeric series code (used in {CODIGO} variable).
        Allows uppercase letters, numbers, hyphens and underscores.
    VeriFactuConfiguration:
      type: object
      required:
        - enabled
        - apply_by_default
      properties:
        enabled:
          type: boolean
          default: false
          description: |
            Whether VeriFactu is enabled for this user.
            When enabled, the user can submit invoices to AEAT.
            Freelancers who don't need to submit invoices to AEAT can leave it disabled.
        apply_by_default:
          type: boolean
          default: false
          description: |
            Whether VeriFactu should be automatically applied to new invoices.
            Requires 'enabled' to be true.
        nif_status:
          type: string
          nullable: true
          enum:
            - PENDING
            - ACTIVATED
            - DEACTIVATED
            - ERROR
          description: |
            VeriFactu registration status (can be null if VeriFactu was never enabled):
            - PENDING: Initial registration in progress
            - ACTIVATED: NIF validated and user created in the corresponding environment
            - DEACTIVATED: Deactivated (data retained for 30 days)
            - ERROR: Some step of the process failed
        nif_registered_at:
          type: string
          format: date-time
          nullable: true
          description: Successful activation date in VeriFactu production
    TaxConfiguration:
      type: object
      required:
        - default_main_tax
        - apply_equivalence_surcharge
        - apply_irpf
      properties:
        default_main_tax:
          allOf:
            - $ref: '#/components/schemas/TaxInfo'
            - description: |
                Default main tax configuration for new invoices.
                Includes tax regime type (IVA/IGIC/IPSI/OTHER), percentage and regime key.

                Common examples:
                - Standard VAT: { type: "IVA", percentage: 21, regime_key: "01" }
                - Special Reduced IGIC: { type: "IGIC", percentage: 7, regime_key: "01" }
                - Standard IPSI: { type: "IPSI", percentage: 10, regime_key: "01" }
            - default:
                type: IVA
                percentage: 21
                regime_key: '01'
        apply_equivalence_surcharge:
          type: boolean
          default: false
          description: |
            Whether the freelancer is under the equivalence surcharge regime.

            **Business rule**: If `true`, the `default_equivalence_surcharge` field is REQUIRED.
        default_equivalence_surcharge:
          allOf:
            - $ref: '#/components/schemas/EquivalenceSurchargePercentage'
          description: |
            Default equivalence surcharge.

            **Business rule**: REQUIRED if `apply_equivalence_surcharge` is `true`.
        apply_irpf:
          type: boolean
          default: true
          description: |
            Whether IRPF withholding should be applied to invoices.

            **Business rule**: If `true` and `irpf_exempt` is `false`,
            the `default_irpf_rate` field is REQUIRED.
        default_irpf_rate:
          allOf:
            - $ref: '#/components/schemas/IrpfPercentage'
          description: |
            Default IRPF for new invoices.

            **Business rule**: REQUIRED if `apply_irpf` is `true` and `irpf_exempt` is `false`.
        irpf_exempt:
          type: boolean
          default: false
          description: |
            Whether the freelancer is exempt from IRPF withholding (e.g., business start).

            **Note**: When `true`, any provided `default_irpf_rate` is saved but not applied to invoices.
            This allows pre-configuring the rate for when the exemption ends.
        default_payment_method:
          allOf:
            - $ref: '#/components/schemas/PaymentMethod'
          default: BANK_TRANSFER
          description: |
            Default payment method for new invoices.
            If NONE is selected, no payment information will be shown on the invoice.
        payment_term_days:
          type: integer
          nullable: true
          minimum: 0
          maximum: 365
          description: Default payment term in days (0-365). Null means no default term.
    InvoiceSeries:
      type: object
      required:
        - id
        - name
        - code
        - format
        - counter_reset
        - active
        - default_series
        - document_type
      properties:
        id:
          type: string
          format: uuid
          description: Unique series ID
        document_type:
          $ref: '#/components/schemas/DocumentType'
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: Main Series
          description: Descriptive name of the series
        code:
          $ref: '#/components/schemas/SeriesCode'
        description:
          type: string
          maxLength: 1000
          example: Series for standard invoices
          description: Optional series description
        format:
          $ref: '#/components/schemas/SeriesFormat'
        counter_reset:
          $ref: '#/components/schemas/CounterReset'
        initial_number:
          type: integer
          format: int64
          minimum: 1
          maximum: 999999
          default: 1
          example: 1
          description: |
            Initial number configured for this series counter.
            Defines from which number invoice numbering will start.
        active:
          type: boolean
          default: true
          description: Indicates whether the series is active
        default_series:
          type: boolean
          default: false
          description: Indicates whether this is the user's default series (only one can be)
        created_at:
          type: string
          format: date-time
          description: Creation date
        next_number:
          type: integer
          format: int64
          readOnly: true
          description: Next invoice number that will be assigned for this series
        updated_at:
          type: string
          format: date-time
          description: Last update date
    TaxRegime:
      type: object
      required:
        - code
        - name
        - description
        - tax_rates
        - applies_equivalence_surcharge
        - regime_keys
      properties:
        code:
          type: string
          description: Tax regime code
          enum:
            - IVA
            - IGIC
            - IPSI
            - OTHER
          example: IVA
        name:
          type: string
          description: Tax regime name
          example: IVA
        description:
          type: string
          description: Detailed description of the tax regime
          example: Value Added Tax
        tax_rates:
          type: array
          items:
            $ref: '#/components/schemas/TaxPercentage'
          description: Available percentages in this regime
        applies_equivalence_surcharge:
          type: boolean
          description: Whether this regime applies equivalence surcharge
        regime_keys:
          type: array
          items:
            $ref: '#/components/schemas/VeriFactuRegimeKey'
          description: Valid VeriFactu regime keys for this tax type
    VeriFactuRegimeKey:
      type: object
      required:
        - code
        - description
      properties:
        code:
          type: string
          description: VeriFactu regime code
          pattern: ^[0-9]{2}$
          example: '01'
        description:
          type: string
          description: Regime key description
          example: General regime operation
    TaxPercentage:
      type: object
      required:
        - percentage
        - description
        - active
      properties:
        percentage:
          type: number
          description: Tax percentage
          example: 21
        description:
          type: string
          description: Tax type description
          example: IVA (21%)
        active:
          type: boolean
          description: Indicates whether the type is active
          default: true
        associated_equivalence_surcharge:
          type: number
          description: Associated equivalence surcharge percentage (only for VAT)
          minimum: 0
          example: 5.2
          nullable: true
    VatType:
      type: object
      required:
        - percentage
        - description
        - verifactu_code
        - active
      properties:
        percentage:
          type: number
          description: VAT percentage
          example: 21
        description:
          type: string
          description: VAT type description
          example: IVA General (21%)
        verifactu_code:
          type: string
          description: VAT type code according to VeriFactu regulations
          example: S1
        active:
          type: boolean
          description: Whether the VAT type is active
          default: true
    IrpfType:
      type: object
      required:
        - percentage
        - description
        - active
      properties:
        percentage:
          type: number
          description: IRPF withholding percentage
          example: 15
        description:
          type: string
          description: IRPF type description
          example: IRPF Profesional (15%)
        active:
          type: boolean
          description: Whether the IRPF type is active
          default: true
    EquivalenceSurcharge:
      type: object
      required:
        - percentage
        - associated_vat
        - description
        - active
      properties:
        percentage:
          type: number
          description: Equivalence surcharge percentage
          example: 5.2
        associated_vat:
          type: number
          description: VAT percentage to which this surcharge is associated
          example: 21
        description:
          type: string
          description: Equivalence surcharge description
          example: RE 5.2% (IVA 21%)
        active:
          type: boolean
          description: Whether the surcharge is active
          default: true
    CreateSeriesRequest:
      type: object
      required:
        - name
        - code
        - format
        - counter_reset
      properties:
        document_type:
          $ref: '#/components/schemas/DocumentType'
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: Main Series
          description: Descriptive name of the series
        code:
          $ref: '#/components/schemas/SeriesCode'
        description:
          type: string
          maxLength: 1000
          nullable: true
          example: Series for standard invoices
          description: Optional series description
        format:
          $ref: '#/components/schemas/SeriesFormat'
        counter_reset:
          $ref: '#/components/schemas/CounterReset'
        initial_number:
          type: integer
          format: int64
          minimum: 1
          maximum: 999999
          default: 1
          example: 1
          description: |
            Initial number for this series counter.
            Useful when migrating from another system and wanting to continue existing numbering.
            For example, if the last invoices were 2024-0150, you can set initial_number=151.
            Default value is 1.
        active:
          type: boolean
          default: true
          description: Whether the series is active
        default_series:
          type: boolean
          default: false
          description: Whether this is the default series
    UpdateSeriesRequest:
      type: object
      description: |
        Request to update an invoice series.

        **IMPORTANT:** All fields are optional, but can only be modified
        when the series **has NO associated invoices**. If the series already has issued invoices,
        no fields can be updated to maintain numbering integrity.
      properties:
        document_type:
          $ref: '#/components/schemas/DocumentType'
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: Main Series
          description: Descriptive name of the series
        code:
          $ref: '#/components/schemas/SeriesCode'
        description:
          type: string
          maxLength: 1000
          nullable: true
          example: Series for standard invoices
          description: Optional series description
        format:
          $ref: '#/components/schemas/SeriesFormat'
        counter_reset:
          $ref: '#/components/schemas/CounterReset'
        initial_number:
          type: integer
          format: int64
          minimum: 1
          maximum: 999999
          example: 54
          description: |
            Initial number for this series counter.
            Useful for adjusting numbering or resetting the counter.
            For example, if you want the next invoice to be 2025-0054, set initial_number=54.
        active:
          type: boolean
          description: |
            Whether the series is active.

            **Restriction:** A default series cannot be deactivated
            (another must be set as default first).
        default_series:
          type: boolean
          description: |
            Whether this is the default series.

            **Restriction:** An inactive series cannot be marked as default.
    UpdateTaxConfigurationRequest:
      type: object
      properties:
        default_main_tax:
          allOf:
            - $ref: '#/components/schemas/TaxInfo'
          description: |
            Default main tax configuration.
            If provided, completely replaces the current configuration.
        apply_equivalence_surcharge:
          type: boolean
          default: false
          description: Whether the freelancer is under the equivalence surcharge regime
        default_equivalence_surcharge:
          $ref: '#/components/schemas/EquivalenceSurchargePercentage'
        apply_irpf:
          type: boolean
          default: true
          description: Whether IRPF withholding should be applied
        default_irpf_rate:
          $ref: '#/components/schemas/IrpfPercentage'
        irpf_exempt:
          type: boolean
          default: false
          description: Whether the freelancer is exempt from IRPF withholding
        default_payment_method:
          type: string
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PaymentMethod'
          description: |
            Default payment method for new invoices.
            If NONE is selected, no payment information will be shown on the invoice.
        payment_term_days:
          type: integer
          nullable: true
          minimum: 0
          maximum: 365
          description: Default payment term in days (0-365)
    ValidateNifRequest:
      type: object
      required:
        - nif
      properties:
        nif:
          allOf:
            - $ref: '#/components/schemas/NIF'
            - description: |
                NIF/CIF to validate against the AEAT census.
                Must be 9 alphanumeric characters.
        legal_name:
          type: string
          nullable: true
          minLength: 1
          maxLength: 255
          description: |
            Last name and first name (individual) or business name (legal entity).

            **Important**:
            - REQUIRED for individuals (NIFs starting with a number)
            - OPTIONAL for legal entities (NIFs starting with a letter)

            AEAT validates that the provided name matches the one registered in the census.
            If it doesn't match, validation will return INVALID.
          example: JUAN PEREZ GARCIA
    ValidateNifResponse:
      type: object
      required:
        - valid
        - status
        - message
      properties:
        valid:
          type: boolean
          description: |
            true if the NIF is validated against the AEAT census (status = VALID).
            false otherwise.
          example: true
        status:
          allOf:
            - $ref: '#/components/schemas/NifValidationStatus'
            - description: Validation status
        legal_name:
          type: string
          nullable: true
          description: |
            Legal name obtained from the AEAT census.
            Only available if status = VALID.
          example: JUAN PEREZ GARCIA
        message:
          type: string
          description: |
            Descriptive message of the validation result.

            Examples:
            - VALID: "NIF successfully validated against the AEAT census"
            - INVALID: "NIF does not exist in the AEAT census"
            - PENDING: "NIF validation is pending. The system will automatically validate it when the service is available."
            - ERROR: "Error validating NIF: VeriFactu API timeout"
          example: NIF successfully validated against the AEAT census
        validated_at:
          type: string
          format: date-time
          nullable: true
          description: |
            Timestamp of when the NIF was successfully validated.
            Only available if status = VALID.
          example: '2025-01-15T10:30:00Z'
    InvoiceTemplateOption:
      type: object
      required:
        - code
        - name
        - description
        - features
      properties:
        code:
          type: string
          enum:
            - MODERN_TABLE
            - PROFESSIONAL_SERVICE
          description: Unique template code
          example: MODERN_TABLE
        name:
          type: string
          description: Descriptive template name
          example: Modern Table
        description:
          type: string
          description: Detailed template description
          example: Design with structured table, ideal for standard products and services
        preview_url:
          type: string
          nullable: true
          description: URL to a template preview image
          example: /assets/previews/modern-table.png
        features:
          type: array
          description: List of template features
          items:
            type: string
          example:
            - Structured table with columns
            - Quantity, unit price and totals
            - Clean and minimal design
    ColorOption:
      type: object
      required:
        - hex
        - name
        - category
      properties:
        hex:
          type: string
          pattern: ^#[0-9A-Fa-f]{6}$
          description: Color in hexadecimal format
          example: '#fc481d'
        name:
          type: string
          description: Descriptive color name
          example: BeeL Orange
        category:
          type: string
          enum:
            - DEFAULT
            - PROFESSIONAL
            - CREATIVE
            - CLASSIC
          description: |
            Color category:
            - DEFAULT: BeeL brand colors
            - PROFESSIONAL: Professional tones (grays, dark blues)
            - CREATIVE: Striking colors (violet, pink, amber)
            - CLASSIC: Classic colors (blue, red, green, purple)
          example: DEFAULT
    InvoiceCustomizationOptionsResponse:
      type: object
      required:
        - template_types
        - suggested_colors
      properties:
        template_types:
          type: array
          description: Available template types
          items:
            $ref: '#/components/schemas/InvoiceTemplateOption'
        suggested_colors:
          type: array
          description: Suggested color palette
          items:
            $ref: '#/components/schemas/ColorOption'
    TaxType:
      type: string
      enum:
        - IVA
        - IGIC
        - IPSI
        - OTHER
      description: |
        Tax type by territory:
        - IVA: Iberian Peninsula and Balearic Islands (0%, 4%, 10%, 21%)
        - IGIC: Canary Islands (0%, 3%, 5%, 7%, 9.5%, 15%, 20%)
        - IPSI: Ceuta and Melilla (0.5%, 1%, 2%, 4%, 8%, 10%)
        - OTHER: Configurable 0%-100%
      example: IVA
    RegimeKey:
      type: string
      enum:
        - '01'
        - '02'
        - '03'
        - '04'
        - '05'
        - '06'
        - '07'
        - '08'
        - '09'
        - '10'
        - '11'
        - '14'
        - '15'
        - '17'
        - '18'
        - '19'
        - '20'
      default: '01'
      description: |
        Regime key according to VeriFactu regulations:
        - 01: General regime operation
        - 02: Export
        - 03: Used goods, art, antiques
        - 04: Investment gold
        - 05: Travel agencies
        - 06: Group of entities
        - 07: Cash basis
        - 08: IPSI/IVA/IGIC operations
        - 09: Mediating agencies
        - 10: Third-party collections
        - 11: Local rental
        - 14: VAT pending in certifications
        - 15: VAT pending successive tract
        - 17: OSS and IOSS
        - 18: Equivalence surcharge
        - 19: REAGYP
        - 20: Simplified regime
      example: '01'
    TaxInfo:
      type: object
      required:
        - type
        - percentage
      properties:
        type:
          $ref: '#/components/schemas/TaxType'
        percentage:
          type: number
          minimum: 0
          maximum: 100
          description: Tax percentage
          example: 21
        regime_key:
          $ref: '#/components/schemas/RegimeKey'
      description: |
        Complete tax information with cross-validations:
        - IVA: only percentages 0, 4, 10, 21
        - IGIC: only percentages 0, 3, 5, 7, 9.5, 15, 20
        - IPSI: only percentages 0.5, 1, 2, 4, 8, 10
        - OTHER: any percentage between 0 and 100

        Exception: when regime_key = "17" (OSS/IOSS) the invoice applies the destination
        country VAT instead of the Spanish one, so any percentage in the EU range [0, 27]
        is accepted regardless of the tax type set.
      example:
        type: IVA
        percentage: 21
        regime_key: '01'
    IgicPercentage:
      type: number
      description: |
        Applied IGIC percentage.
        Valid value validation is performed in the backend according to the configured tax regime.
        Common IGIC values (Canary Islands):
        - 0%: Exempt
        - 3%: Reduced
        - 5%: Special reduced (energy drinks)
        - 7%: Standard
        - 9.5%: Increased
        - 15%: Higher increased
        - 20%: Special
      minimum: 0
      maximum: 100
      example: 7
    IpsiPercentage:
      type: number
      description: |
        Applied IPSI percentage.
        Valid value validation is performed in the backend according to the configured tax regime.
        Common IPSI values (Ceuta and Melilla):
        - 0.5%: Super-reduced (social housing, games, advertising)
        - 1%: Reduced (taxis, bars, electricity)
        - 2%: Intermediate (restaurants, hospitality)
        - 4%: Medium (renovations, real estate)
        - 8%: High (telecommunications, electronic services)
        - 10%: Standard (construction)
      minimum: 0
      maximum: 100
      example: 10
    OtherTaxPercentage:
      type: number
      minimum: 0
      maximum: 100
      description: |
        Customizable percentage for OTHER.
        Allows any value between 0% and 100%.
      example: 5
    CustomerValidationUnifiedResult:
      type: object
      required:
        - metadata
        - customers_validation
        - statistics
      description: |
        Unified format for customer validation results.
        Used for bulk validation (JSON), CSV import and Holded Excel import.

        Combines the functionality of CrearClientesBulk200Response, CsvImportPreviewResult
        and HoldedImportResult into a consistent structure that simplifies the frontend.
      properties:
        metadata:
          $ref: '#/components/schemas/CustomerValidationMetadata'
        customers_validation:
          type: array
          items:
            $ref: '#/components/schemas/CustomerValidationItem'
          description: List of processed customers with their validation status
        statistics:
          $ref: '#/components/schemas/CustomerValidationStatistics'
    CustomerValidationMetadata:
      type: object
      required:
        - total_customers
        - is_dry_run
        - processing_time_ms
        - source_type
      description: |
        Metadata about the validation operation.
        Compatible with bulk JSON, CSV import and Holded Excel import.
      properties:
        total_customers:
          type: integer
          description: Total number of customers submitted
          example: 10
        is_dry_run:
          type: boolean
          description: Whether it was validation (true) or import (false)
          example: true
        processing_time_ms:
          type: integer
          description: Processing time in milliseconds
          example: 450
        source_type:
          type: string
          enum:
            - BULK_JSON
            - CSV_IMPORT
            - HOLDED_EXCEL
          description: |
            Data source type:
            - BULK_JSON: Data sent as JSON to bulk endpoint
            - CSV_IMPORT: Data parsed from CSV file
            - HOLDED_EXCEL: Data parsed from Holded Excel file
          example: CSV_IMPORT
        filename:
          type: string
          nullable: true
          description: File name (CSV or Excel depending on source_type)
          example: clientes_enero.csv
        file_size_bytes:
          type: integer
          nullable: true
          description: File size in bytes
          example: 1048576
        total_rows:
          type: integer
          nullable: true
          description: Total file rows excluding headers
          example: 250
    CustomerValidationItem:
      type: object
      required:
        - index
        - customer
        - status
        - errors
        - warnings
      description: |
        Individual customer validation result.
        Works for bulk JSON, CSV import and Holded Excel import.
      properties:
        index:
          type: integer
          description: |
            Customer index in the submitted list (0-based for bulk, 1-based for files).
            For CSV/Excel, represents the row number in the file.
          example: 0
        customer:
          allOf:
            - $ref: '#/components/schemas/Customer'
          description: |
            Parsed and validated customer.
            Can be null if there were critical parsing errors.
        status:
          type: string
          enum:
            - VALID
            - WARNING
            - ERROR
            - DUPLICATE
            - NIF_INVALID
          description: |
            Customer validation status:
            - VALID: Completely valid customer
            - WARNING: Valid customer but with non-critical warnings
            - ERROR: Invalid customer, cannot be processed
            - DUPLICATE: Duplicate customer (in batch/CSV or database)
            - NIF_INVALID: NIF not valid according to AEAT census (VeriFactu)
          example: VALID
        errors:
          type: array
          description: List of errors found (block import)
          items:
            $ref: '#/components/schemas/CustomerValidationError'
        warnings:
          type: array
          description: List of warnings (do not block import)
          items:
            $ref: '#/components/schemas/CustomerValidationWarning'
        row_number:
          type: integer
          nullable: true
          description: Row number in the file (CSV or Excel depending on source_type)
          example: 15
    CustomerValidationError:
      type: object
      required:
        - field
        - value
        - message
      description: Field-specific validation error
      properties:
        field:
          type: string
          description: Field that failed validation
          example: nif
        value:
          type: string
          description: Value that caused the error
          example: 12345678X
        message:
          type: string
          description: Error explanatory message
          example: NIF not found in AEAT census
    CustomerValidationWarning:
      type: object
      required:
        - field
        - message
      description: Validation warning (does not block import)
      properties:
        field:
          type: string
          description: Field with warning
          example: email
        message:
          type: string
          description: Warning message
          example: Email not provided, NIF will be used for invoicing
    CustomerValidationStatistics:
      type: object
      required:
        - total_processed
        - valid
        - with_warnings
        - with_errors
        - duplicates
        - invalid_nifs
        - imported
        - success_rate
      description: |
        Aggregated validation statistics.
        Compatible with all source types (bulk JSON, CSV and Holded Excel).
      properties:
        total_processed:
          type: integer
          description: Total customers processed
          example: 10
        valid:
          type: integer
          description: Completely valid customers (no warnings or errors)
          example: 6
        with_warnings:
          type: integer
          description: Valid customers but with warnings
          example: 2
        with_errors:
          type: integer
          description: Customers with errors (not importable)
          example: 2
        duplicates:
          type: integer
          description: Customers with duplicate NIFs
          example: 1
        invalid_nifs:
          type: integer
          description: Customers with NIFs not valid in AEAT
          example: 1
        imported:
          type: integer
          description: Customers actually imported (only if is_dry_run=false)
          example: 0
        success_rate:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: Success rate (valid + with_warnings) / total_processed
          example: 0.8
        importable:
          type: integer
          description: Total customers that can be imported (valid + with_warnings)
          example: 8
        not_importable:
          type: integer
          description: Total customers that CANNOT be imported (with_errors + duplicates + invalid_nifs)
          example: 2
    RecurringInvoiceResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        frequency:
          type: string
          enum:
            - MONTHLY
        day_of_month:
          type: integer
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date
          nullable: true
        next_generation:
          type: string
          format: date
        preview_days:
          type: integer
          description: Days before emission date to create a draft for review. 0 means immediate emission.
          example: 3
        status:
          type: string
          enum:
            - ACTIVE
            - PAUSED
            - COMPLETED
        series_id:
          type: string
          format: uuid
        series_code:
          type: string
        invoice_type:
          type: string
          enum:
            - STANDARD
            - SIMPLIFIED
        customer_id:
          type: string
          format: uuid
          nullable: true
        recipient_fiscal_name:
          type: string
          nullable: true
        recipient_nif:
          type: string
          nullable: true
        lines:
          type: array
          items:
            $ref: '#/components/schemas/InvoiceLineTemplateResponse'
        payment_method:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
        verifactu_enabled:
          type: boolean
        send_automatically:
          type: boolean
        email_configuration:
          $ref: '#/components/schemas/RecurringEmailConfigResponse'
        generated_invoices:
          type: integer
        last_generation:
          type: string
          format: date-time
          nullable: true
        source_invoice_id:
          type: string
          format: uuid
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    InvoiceLineTemplateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        order:
          type: integer
        description:
          type: string
        quantity:
          type: number
        unit:
          type: string
          nullable: true
        unit_price:
          type: number
        discount_percentage:
          type: number
          nullable: true
        tax_type:
          type: string
        vat_rate:
          type: number
        regime_key:
          type: string
        equivalence_surcharge_rate:
          type: number
          nullable: true
        irpf_rate:
          type: number
          nullable: true
    RecurringEmailConfigResponse:
      type: object
      nullable: true
      properties:
        recipients:
          type: array
          items:
            type: string
        cc:
          type: array
          items:
            type: string
        subject:
          type: string
          nullable: true
        message:
          type: string
          nullable: true
    UpdateRecurringInvoiceRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        day_of_month:
          type: integer
          minimum: 1
          maximum: 31
        start_date:
          type: string
          format: date
        preview_days:
          type: integer
          minimum: 0
          maximum: 30
          description: Days before emission date to create a draft for review. 0 means immediate emission.
        end_date:
          type: string
          format: date
          nullable: true
        series_id:
          type: string
          format: uuid
        customer_id:
          type: string
          format: uuid
          nullable: true
        lines:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/RecurringLineRequest'
        payment_method:
          allOf:
            - $ref: '#/components/schemas/PaymentMethod'
          nullable: true
        payment_iban:
          type: string
          nullable: true
        payment_swift:
          type: string
          nullable: true
        payment_term_days:
          type: integer
          nullable: true
        notes:
          type: string
          nullable: true
        verifactu_enabled:
          type: boolean
        send_automatically:
          type: boolean
        email_configuration:
          $ref: '#/components/schemas/RecurringEmailConfigRequest'
    CreateRecurringInvoiceRequest:
      type: object
      required:
        - name
        - day_of_month
        - start_date
        - series_id
        - invoice_type
        - lines
      properties:
        name:
          type: string
          maxLength: 255
        day_of_month:
          type: integer
          minimum: 1
          maximum: 31
        start_date:
          type: string
          format: date
        preview_days:
          type: integer
          minimum: 0
          maximum: 30
          default: 0
          description: Days before emission date to create a draft for review. 0 means immediate emission.
        end_date:
          type: string
          format: date
          nullable: true
        series_id:
          type: string
          format: uuid
        invoice_type:
          type: string
          enum:
            - STANDARD
            - SIMPLIFIED
        customer_id:
          type: string
          format: uuid
          nullable: true
        lines:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/RecurringLineRequest'
        payment_method:
          allOf:
            - $ref: '#/components/schemas/PaymentMethod'
          nullable: true
        payment_iban:
          type: string
          nullable: true
        payment_swift:
          type: string
          nullable: true
        payment_term_days:
          type: integer
          nullable: true
        notes:
          type: string
          nullable: true
        verifactu_enabled:
          type: boolean
          default: false
        send_automatically:
          type: boolean
          default: false
        email_configuration:
          $ref: '#/components/schemas/RecurringEmailConfigRequest'
    CreateRecurringFromInvoiceRequest:
      type: object
      required:
        - name
        - day_of_month
        - start_date
      properties:
        name:
          type: string
          maxLength: 255
        day_of_month:
          type: integer
          minimum: 1
          maximum: 31
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date
          nullable: true
        verifactu_enabled:
          type: boolean
          default: false
        send_automatically:
          type: boolean
          default: false
        email_configuration:
          $ref: '#/components/schemas/RecurringEmailConfigRequest'
    RecurringLineRequest:
      type: object
      description: |
        Recurring-invoice line. Unlike invoice lines (which nest tax data under a `main_tax` object),
        recurring lines use flat tax fields: `vat_rate`, `tax_type`, `regime_key`,
        `equivalence_surcharge_rate` and `irpf_rate`. Do not send a `main_tax` object here.
      required:
        - description
        - quantity
        - unit_price
        - vat_rate
      properties:
        description:
          type: string
          maxLength: 500
        quantity:
          type: number
          minimum: 0.01
        unit:
          type: string
          maxLength: 20
        unit_price:
          type: number
          minimum: 0
        discount_percentage:
          type: number
          minimum: 0
          maximum: 100
        tax_type:
          type: string
          default: IVA
        vat_rate:
          type: number
        regime_key:
          type: string
          default: '01'
        equivalence_surcharge_rate:
          type: number
          nullable: true
        irpf_rate:
          type: number
          nullable: true
    RecurringEmailConfigRequest:
      type: object
      nullable: true
      properties:
        recipients:
          type: array
          items:
            type: string
        cc:
          type: array
          items:
            type: string
        subject:
          type: string
          nullable: true
        message:
          type: string
          nullable: true
    GenerationHistoryResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        invoice_id:
          type: string
          format: uuid
        generated_at:
          type: string
          format: date-time
        scheduled_date:
          type: string
          format: date
    CreateCompanyRequest:
      type: object
      required:
        - nif
        - legal_name
        - entity_type
        - address_street
        - address_postal_code
        - address_city
        - address_province
      properties:
        nif:
          type: string
          description: NIF/CIF of the business
          example: B12345678
        legal_name:
          type: string
          description: Legal/fiscal name
          example: Mi Empresa SL
        entity_type:
          type: string
          enum:
            - INDIVIDUAL
            - LEGAL_ENTITY
          description: Type of entity
        address_street:
          type: string
          example: Calle Mayor
        address_number:
          type: string
          example: '10'
        address_postal_code:
          type: string
          example: '28001'
        address_city:
          type: string
          example: Madrid
        address_province:
          type: string
          example: Madrid
        address_country:
          type: string
          default: ES
        legal_form:
          type: string
          description: Required for LEGAL_ENTITY
        representative_name:
          type: string
          description: Required for LEGAL_ENTITY
        representative_nif:
          type: string
          description: Required for LEGAL_ENTITY
        business_display_name:
          type: string
          description: Optional display name
        tax_type:
          type: string
          default: IVA
        tax_percentage:
          type: number
          default: 21
        irpf_percentage:
          type: number
          default: 15
        environment:
          type: string
          enum:
            - TEST
            - PROD
          default: TEST
          description: |
            VeriFactu environment for this company.
            * `TEST` — Sandbox NIF, invoices reach VeriFactu test (default).
            * `PROD` — Production NIF; requires the AEAT representation
              model to be signed (POST /v1/representations/{nif}/sign)
              before real invoices can be issued.
        success_url:
          type: string
          description: |
            Only relevant when `environment=PROD` and the parent account has no
            card on file (`SANDBOX_ONLY`). Redirect target after the user
            captures a payment method in the Stripe Checkout setup session.
            May embed the Stripe `{CHECKOUT_SESSION_ID}` template (which Stripe
            substitutes on redirect), so this is a plain string — not a strict
            `uri` — to confirm via `POST /v1/companies/verify-creation`.
          example: https://app.beel.es/settings/empresas?company_creation_session={CHECKOUT_SESSION_ID}
        cancel_url:
          type: string
          description: |
            Only relevant when `environment=PROD` and the parent account is
            `SANDBOX_ONLY`. Redirect target if the user cancels the checkout.
            No company is created in that case.
          example: https://app.beel.es/settings/empresas
    UpdateCompanyRequest:
      type: object
      description: Editable fields of a company. NIF, legal name, and entity type are immutable.
      properties:
        business_display_name:
          type: string
          description: Friendly display name for the company
        address_street:
          type: string
        address_number:
          type: string
        address_postal_code:
          type: string
        address_city:
          type: string
        address_province:
          type: string
        address_country:
          type: string
    CompanyData:
      type: object
      properties:
        id:
          type: string
          format: uuid
        nif:
          type: string
        legal_name:
          type: string
        business_display_name:
          type: string
        entity_type:
          type: string
          enum:
            - INDIVIDUAL
            - LEGAL_ENTITY
        is_primary:
          type: boolean
        verifactu_status:
          type: string
          enum:
            - NOT_CONFIGURED
            - PENDING
            - ACTIVE
            - ERROR
        can_issue_production_invoices:
          type: boolean
        environment:
          type: string
          enum:
            - TEST
            - PROD
          description: |
            VeriFactu environment for this company. `TEST` means invoices
            reach VeriFactu test; `PROD` means real invoices once the AEAT
            representation model is signed for this NIF.
        account_state:
          type: string
          enum:
            - SANDBOX_ONLY
            - PRODUCTION_ACTIVE
          description: |
            Account-level state derived from the owner's `production_activated_at`.
            `SANDBOX_ONLY` means no card captured yet — the promote wizard must
            stay in the Stripe step. `PRODUCTION_ACTIVE` means a Stripe subscription
            is live so the wizard can skip to PDF signing on resume.
        created_at:
          type: string
          format: date-time
    CompanyResponse201:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/CompanyData'
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    ListCompanies200Response:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/CompanyData'
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    RepresentationActionResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            status:
              type: string
            message:
              type: string
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    RepresentationDownloadResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            download_url:
              type: string
              format: uri
            expires_in_seconds:
              type: integer
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    RepresentationStatusResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            status:
              type: string
              enum:
                - NOT_STARTED
                - PDF_GENERATED
                - SUBMITTED
                - ACTIVE
                - ERROR
                - CANCELLED
            message:
              type: string
            updated_at:
              type: string
              format: date-time
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    CreateCompanyApiKeyRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Descriptive name for the API key
        permissions:
          type: array
          items:
            type: string
          description: Scopes for the key (cannot exceed parent key scopes)
        expires_in_days:
          type: integer
          description: Expiration in days (optional)
    CompanyApiKeyData:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        prefix:
          type: string
        environment:
          type: string
          enum:
            - PRODUCTION
            - SANDBOX
        permissions:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
    CreateCompanyApiKey201Response:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            key:
              type: string
              description: Plain text key (only shown once)
            prefix:
              type: string
            environment:
              type: string
            permissions:
              type: array
              items:
                type: string
            expires_at:
              type: string
              format: date-time
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    ListCompanyApiKeys200Response:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/CompanyApiKeyData'
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    ExemptionReason:
      type: string
      enum:
        - EXENTA_ART_20
        - EXENTA_ART_21
        - EXENTA_ART_22
        - EXENTA_ART_24
        - EXENTA_ART_25
        - EXENTA_ART_26
        - EXENTA_ART_140
        - NO_SUJETA_ART_7_9
        - NO_SUJETA_LOCALIZACION
        - ISP_ART_84_2_A
        - ISP_ART_84_2_E
        - ISP_ART_84_2_F
        - REGIMEN_ART_129
        - REGIMEN_ART_135
        - REGIMEN_ART_141
        - REGIMEN_ART_154
        - REGIMEN_ART_163_DECIES
        - OTRO
      description: |
        Tax exemption reason code per Spanish VAT Law (Ley 37/1992 LIVA).
        VeriFactu mapping: EXENTA_ART_20→E1, EXENTA_ART_21→E2, EXENTA_ART_22→E3,
        EXENTA_ART_24→E4, EXENTA_ART_25→E5, rest→E6. ISP→S2, NO_SUJETA→N1/N2.
        When OTRO, a custom text must be provided in exemption_reason_text.
      example: EXENTA_ART_20
    FieldDeserializationError:
      type: object
      required:
        - field
        - invalid_value
      properties:
        field:
          type: string
          description: JSON field name that caused the error (matches the API contract name)
          example: due_date
        invalid_value:
          type: string
          description: The value that was rejected
          example: 2026-03-04fds
        expected_format:
          type: string
          nullable: true
          description: Expected format hint (e.g. YYYY-MM-DD for dates)
          example: YYYY-MM-DD
        allowed_values:
          type: string
          nullable: true
          description: Comma-separated list of allowed values (for enum fields)
          example: STANDARD, SIMPLIFIED, CORRECTIVE
    ExemptionReasonCatalogEntry:
      type: object
      required:
        - code
        - label
        - description
        - category
        - classification_type
      properties:
        code:
          type: string
          description: Enum value to send in invoice line exemption_reason field
          example: EXENTA_ART_20
        label:
          type: string
          description: i18n key for the label (resolve via translations)
          example: invoice.exemption.EXENTA_ART_20
        description:
          type: string
          description: i18n key for the description
          example: invoice.exemption.EXENTA_ART_20
        category:
          type: string
          description: Category grouping for UI display
          example: OPERACIONES EXENTAS
        classification_type:
          type: string
          enum:
            - EXENTA
            - NO_SUJETA
            - NO_SUJETA_LOCALIZACION
            - SUJETA_NO_EXENTA_ISP
            - SUJETA_NO_EXENTA
          description: |
            Tax classification type that determines VeriFactu behavior:
            - EXENTA: Exempt operation (E1-E6), no tax fields
            - NO_SUJETA: Not subject to tax (N1), no tax fields
            - NO_SUJETA_LOCALIZACION: Not subject by localization rules (N2), no tax fields
            - SUJETA_NO_EXENTA_ISP: Reverse charge (S2), tax rate forced to 0%
            - SUJETA_NO_EXENTA: Normal taxed (S1), requires tax rate
    DocumentType:
      type: string
      enum:
        - SIN_ASIGNAR
        - FACTURA_ORDINARIA
        - FACTURA_SIMPLIFICADA
        - FACTURA_RECTIFICATIVA
      description: |
        Document type associated with a series:
        - SIN_ASIGNAR: Legacy series, compatible with any invoice type
        - FACTURA_ORDINARIA: Standard invoice
        - FACTURA_SIMPLIFICADA: Simplified invoice
        - FACTURA_RECTIFICATIVA: Rectificative invoice
    DocumentTypeDefaultStatus:
      type: object
      required:
        - document_type
        - exists
      description: |
        Status of the default invoice series for a single `DocumentType` in the
        request's environment (resolved via auth context, no input needed).

        When `exists` is `false`, `serie_id` and `code` are absent. The integration
        setup screen reads this list to surface missing defaults at config time
        instead of failing webhooks at runtime with `MISSING_DEFAULT_SERIES`.
      properties:
        document_type:
          $ref: '#/components/schemas/DocumentType'
        exists:
          type: boolean
          description: Whether the user has a default series for this document type.
        serie_id:
          type: string
          format: uuid
          nullable: true
          description: ID of the configured default series (only when exists=true).
        code:
          type: string
          nullable: true
          description: Code of the configured default series (only when exists=true).
    CheckoutRequiredResponse:
      description: |
        Returned with HTTP 402 when a production NIF cannot be created yet
        because the owner account has no card on file. Carries the Stripe
        Checkout setup-session URL in `error.details.checkout_url`.
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          properties:
            error:
              allOf:
                - $ref: '#/components/schemas/ErrorDetail'
                - type: object
                  properties:
                    details:
                      type: object
                      required:
                        - checkout_url
                      properties:
                        checkout_url:
                          type: string
                          format: uri
                          description: Stripe Checkout setup-session URL.
      example:
        success: false
        error:
          code: CHECKOUT_REQUIRED
          message: A payment method is required before creating a production NIF.
          details:
            checkout_url: https://checkout.stripe.com/c/pay/cs_test_a1b2c3...
        meta:
          timestamp: '2026-05-29T10:30:00Z'
          request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    CompanyStatsData:
      type: object
      required:
        - company_id
        - invoice_count
      properties:
        company_id:
          type: string
          format: uuid
        invoice_count:
          type: integer
          format: int64
          description: Total non-deleted invoices issued by this company.
          example: 42
        last_invoice_at:
          type: string
          format: date-time
          nullable: true
          description: Date of the most recent non-deleted invoice (ISO 8601), or null if none.
          example: '2026-05-12T00:00:00Z'
    ListCompanyStats200Response:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/CompanyStatsData'
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    VerifyCreationRequest:
      type: object
      required:
        - session_id
      properties:
        session_id:
          type: string
          description: |
            Stripe Checkout Session ID returned in the `success_url` as the
            `{CHECKOUT_SESSION_ID}` template variable.
          example: cs_test_a1b2c3...
    VerifyCreationResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            status:
              type: string
              enum:
                - CREATED
                - PENDING
              description: |
                `CREATED` when the card was captured and the production company
                was materialized; `PENDING` when the session is not yet paid.
            company:
              allOf:
                - $ref: '#/components/schemas/CompanyData'
              nullable: true
              description: The created company. `null` while `status=PENDING`.
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    WebhookSubscription:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
        active:
          type: boolean
        last_used_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    WebhookEventTypeEnum:
      type: string
      description: |
        Available webhook event types:
        - `invoice.emitted` — Invoice emitted and finalized
        - `invoice.email.sent` — Invoice sent by email
        - `invoice.cancelled` — Invoice cancelled
        - `verifactu.status.updated` — VeriFactu status changed (PENDING, ACCEPTED, REJECTED)
      enum:
        - invoice.emitted
        - invoice.email.sent
        - invoice.cancelled
        - verifactu.status.updated
    CreateWebhookSubscriptionRequest:
      type: object
      required:
        - url
        - events
      properties:
        url:
          type: string
          format: uri
          description: HTTPS endpoint URL that will receive webhook POST requests.
          example: https://yourapp.com/webhooks/beel
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventTypeEnum'
          description: List of event types to subscribe to.
          example:
            - invoice.emitted
            - verifactu.status.updated
    WebhookSubscriptionWithSecret:
      allOf:
        - $ref: '#/components/schemas/WebhookSubscription'
        - type: object
          properties:
            secret:
              type: string
              description: |
                HMAC-SHA256 signing secret. **Only returned on creation and secret rotation.**
                Store it securely — it cannot be retrieved again.
              example: whsec_a3f5b2e1c9d8f7e6b5a4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2
    WebhookEventDataInvoiceEmitted:
      type: object
      description: Data payload for `invoice.emitted` events.
      additionalProperties: false
      required:
        - invoice_id
        - invoice_number
      properties:
        invoice_id:
          type: string
          format: uuid
        invoice_number:
          type: string
          example: 2025/0001
        customer_email:
          type: string
          nullable: true
        customer_name:
          type: string
          nullable: true
    WebhookEventDataInvoiceEmailSent:
      type: object
      description: Data payload for `invoice.email.sent` events.
      additionalProperties: false
      required:
        - invoice_id
        - all_recipients
        - sent_at
      properties:
        invoice_id:
          type: string
          format: uuid
        invoice_number:
          type: string
          nullable: true
          example: 2025/0001
        all_recipients:
          type: array
          items:
            type: string
          description: All email recipients (TO + CC).
        sent_at:
          type: string
          format: date-time
    WebhookEventDataInvoiceCancelled:
      type: object
      description: Data payload for `invoice.cancelled` events.
      additionalProperties: false
      required:
        - invoice_id
        - invoice_number
      properties:
        invoice_id:
          type: string
          format: uuid
        invoice_number:
          type: string
          example: 2025/0001
        cancellation_reason:
          type: string
          nullable: true
    WebhookEventDataVeriFactuStatusUpdated:
      type: object
      description: Data payload for `verifactu.status.updated` events.
      additionalProperties: false
      required:
        - invoice_id
        - verifactu_registration_id
        - previous_status
        - new_status
      properties:
        invoice_id:
          type: string
          format: uuid
        invoice_number:
          type: string
          nullable: true
          example: 2025-001
        verifactu_registration_id:
          type: string
          format: uuid
        previous_status:
          type: string
          enum:
            - PENDING
            - ACCEPTED
            - REJECTED
        new_status:
          type: string
          enum:
            - PENDING
            - ACCEPTED
            - REJECTED
        qr_url:
          type: string
          nullable: true
          example: https://sede.agenciatributaria.gob.es/Sede/verifactu?id=ABC123
        qr_base64:
          type: string
          nullable: true
          description: QR code as base64-encoded PNG for embedding in PDFs.
        invoice_hash:
          type: string
          nullable: true
          example: B11F3A015173AD99075E2720F61E2DE1FF08CBFEDD85C6F73C77AD835301B2A3
        error_code:
          type: string
          nullable: true
        error_message:
          type: string
          nullable: true
    WebhookEvent:
      type: object
      description: Payload delivered to your endpoint on every webhook event.
      properties:
        id:
          type: string
          description: Unique webhook event ID (UUID).
          example: 3f7a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c
        type:
          $ref: '#/components/schemas/WebhookEventTypeEnum'
        created_at:
          type: string
          format: date-time
        api_version:
          type: string
          example: 2025-01
        livemode:
          type: boolean
          description: '`false` for SANDBOX API keys.'
        test:
          type: boolean
          nullable: true
          description: '`true` only for test deliveries triggered from the dashboard.'
        data:
          oneOf:
            - $ref: '#/components/schemas/WebhookEventDataInvoiceEmitted'
            - $ref: '#/components/schemas/WebhookEventDataInvoiceEmailSent'
            - $ref: '#/components/schemas/WebhookEventDataInvoiceCancelled'
            - $ref: '#/components/schemas/WebhookEventDataVeriFactuStatusUpdated'
    UpdateWebhookSubscriptionRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
          nullable: true
          description: New HTTPS endpoint URL.
          example: https://yourapp.com/webhooks/beel
        events:
          type: array
          nullable: true
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventTypeEnum'
          description: New list of event types to subscribe to.
        active:
          type: boolean
          nullable: true
          description: Enable or disable the webhook subscription.
    WebhookDeliveryLog:
      type: object
      properties:
        id:
          type: string
          format: uuid
        subscription_id:
          type: string
          format: uuid
        webhook_event_id:
          type: string
          format: uuid
          description: Groups all delivery attempts for the same logical event.
        event_type:
          type: string
          example: verifactu.status.updated
        attempt_number:
          type: integer
          description: Delivery attempt number (1 = first, 2 = first retry, etc.)
        http_status:
          type: integer
          nullable: true
          description: HTTP response status code. Null if connection failed.
          example: 200
        response_body:
          type: string
          nullable: true
        duration_ms:
          type: integer
          nullable: true
          description: Delivery duration in milliseconds.
        success:
          type: boolean
        error_message:
          type: string
          nullable: true
          description: Error description for failed deliveries (timeout, connection error, etc.)
        payload:
          type: string
          nullable: true
          description: The JSON payload that was sent (or attempted) to your endpoint.
        request_headers:
          type: object
          nullable: true
          description: |
            HTTP headers sent to your endpoint with this delivery attempt.
            Includes `BeeL-Signature`, `BeeL-Event`, `BeeL-Event-Id`, `BeeL-Delivery-Id`, and `Idempotency-Key`.
          additionalProperties:
            type: string
          example:
            BeeL-Signature: t=1741362026,v1=3c4f7a2e1b9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f
            BeeL-Event: verifactu.status.updated
            BeeL-Event-Id: 8ee6b023-c4e5-482e-93ca-dc66da2f9cb5
            BeeL-Delivery-Id: b3873959-67d8-4a11-87ba-b39cc2d395fe
            Idempotency-Key: 8ee6b023-c4e5-482e-93ca-dc66da2f9cb5
        delivered_at:
          type: string
          format: date-time
    TaxRegime-2:
      type: string
      enum:
        - IVA
        - IGIC
        - IPSI
        - OTHER
      description: |
        Applicable tax regime:
        - IVA: Value Added Tax (Iberian Peninsula and Balearic Islands)
        - IGIC: Canary Islands General Indirect Tax (Canary Islands)
        - IPSI: Production, Services and Import Tax (Ceuta and Melilla)
        - OTHER: Special regimes (exemptions, etc.)
      example: IVA
    VatPercentage:
      type: number
      enum:
        - 0
        - 4
        - 5
        - 10
        - 21
      description: |
        VAT percentage.
        Allowed values: 0 (exempt), 4 (super-reduced), 5 (RD-ley 11/2022 temporal reduced),
        10 (reduced), 21 (standard).
        Valid values are validated according to the configured tax regime.
      example: 21
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: beel_sk_*
      description: |
        API Key authentication.

        **Format:** `Authorization: Bearer beel_sk_<key>`

        **Scopes:** API Keys use the same scopes as OAuth2 tokens. Each key is created with
        specific scopes that limit which endpoints it can access. The required scope for each
        endpoint is documented in the operation's security section under `OAuth2`.

        **Obtaining Keys:** API Keys are managed from the BeeL dashboard

        **Security:** API Keys are secret credentials. Do not share them or store them in source code
    OAuth2:
      type: oauth2
      description: |
        OAuth2 authentication via authorization_code flow with PKCE.

        **Discovery:** `https://app.beel.es/.well-known/oauth-authorization-server`

        **Flow:** User logs in on BeeL, consents scopes, and the client receives an access token (JWT).
        The access token is used as Bearer token in the Authorization header.
      flows:
        authorizationCode:
          authorizationUrl: https://app.beel.es/api/oauth2/authorize
          tokenUrl: https://app.beel.es/api/oauth2/token
          refreshUrl: https://app.beel.es/api/oauth2/token
          scopes:
            invoices:read: Leer facturas
            invoices:write: Crear, editar, eliminar facturas
            customers:read: Leer clientes
            customers:write: Crear, editar, eliminar clientes
            products:read: Leer productos
            products:write: Crear, editar, eliminar productos
            configuration:read: Leer configuracion (impuestos, series, VeriFactu)
            configuration:write: Modificar configuracion
            series:read: Leer series de facturacion
            series:write: Crear, editar, eliminar series
            taxes:read: Leer tipos de impuestos
            nif:validate: Validar NIFs contra AEAT
            business_profiles:read: Leer perfiles de negocio
            business_profiles:write: Gestionar perfiles de negocio
    SessionCookie:
      type: apiKey
      in: cookie
      name: BEEL_SESSION
      description: |
        Session Cookie authentication (for browser-based access).

        **Note:** This method is primarily used by the web application.
        For programmatic access, use API Keys or OAuth2 instead.
  examples:
    list-invoices-success:
      summary: List invoices (200)
      description: Invoice list with pagination metadata
      value:
        success: true
        data:
          invoices:
            - id: f47ac10b-58cc-4372-a567-0e02b2c3d479
              invoice_number: A/2025/0042
              series:
                id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                code: A
              number: 42
              type: STANDARD
              status: ISSUED
              issue_date: '2025-01-20'
              due_date: '2025-02-20'
              issuer:
                legal_name: Mi Empresa SL
                nif: B12345678
                address:
                  street: Calle Principal
                  number: '10'
                  postal_code: '28001'
                  city: Madrid
                  province: Madrid
                  country: España
              recipient:
                customer_id: 123e4567-e89b-12d3-a456-426614174000
                legal_name: Cliente Ejemplo SL
                nif: B87654321
                address:
                  street: Calle Secundaria
                  number: '20'
                  postal_code: '08001'
                  city: Barcelona
                  province: Barcelona
                  country: España
              lines:
                - description: Consulting services
                  quantity: 10
                  unit: hours
                  unit_price: 150
                  taxable_base: 1500
                  main_tax:
                    type: IVA
                    percentage: 21
                    regime_key: '01'
                  line_total: 1815
              totals:
                taxable_base: 1500
                total_vat: 315
                total_irpf: 0
                total_equivalence_surcharge: 0
                vat_breakdown:
                  - type: 21
                    base: 1500
                    amount: 315
                invoice_total: 1815
              pdf_download_url: /v1/invoices/f47ac10b-58cc-4372-a567-0e02b2c3d479/pdf
              created_at: '2025-01-20T09:00:00Z'
              updated_at: '2025-01-20T09:30:00Z'
            - id: 550e8400-e29b-41d4-a716-446655440002
              invoice_number: A/2025/0041
              series:
                id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                code: A
              number: 41
              type: STANDARD
              status: PAID
              issue_date: '2025-01-15'
              due_date: '2025-02-15'
              payment_date: '2025-01-18'
              sent_at: '2025-01-16T10:00:00Z'
              paid_at: '2025-01-18T14:00:00Z'
              issuer:
                legal_name: Mi Empresa SL
                nif: B12345678
                address:
                  street: Calle Principal
                  number: '10'
                  postal_code: '28001'
                  city: Madrid
                  province: Madrid
                  country: España
              recipient:
                customer_id: 456e7890-e12b-34d5-a678-901234567890
                legal_name: Otro Cliente SA
                nif: A12345678
                address:
                  street: Avenida Central
                  number: '100'
                  postal_code: '46001'
                  city: Valencia
                  province: Valencia
                  country: España
              lines:
                - description: Software development
                  quantity: 50
                  unit: hours
                  unit_price: 50
                  taxable_base: 2500
                  main_tax:
                    type: IVA
                    percentage: 21
                    regime_key: '01'
                  line_total: 3025
              totals:
                taxable_base: 2500
                total_vat: 525
                total_irpf: 0
                total_equivalence_surcharge: 0
                vat_breakdown:
                  - type: 21
                    base: 2500
                    amount: 525
                invoice_total: 3025
              pdf_download_url: /v1/invoices/550e8400-e29b-41d4-a716-446655440002/pdf
              created_at: '2025-01-15T10:00:00Z'
              updated_at: '2025-01-18T14:00:00Z'
            - id: 550e8400-e29b-41d4-a716-446655440003
              invoice_number: A/2025/0040
              series:
                id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                code: A
              number: 40
              type: STANDARD
              status: OVERDUE
              issue_date: '2024-12-10'
              due_date: '2025-01-10'
              issuer:
                legal_name: Mi Empresa SL
                nif: B12345678
                address:
                  street: Calle Principal
                  number: '10'
                  postal_code: '28001'
                  city: Madrid
                  province: Madrid
                  country: España
              recipient:
                customer_id: 789e0123-e45b-67d8-a901-234567890123
                legal_name: Cliente Moroso SL
                nif: B98765432
                address:
                  street: Plaza Mayor
                  number: '5'
                  postal_code: '41001'
                  city: Sevilla
                  province: Sevilla
                  country: España
              lines:
                - description: Web maintenance
                  quantity: 1
                  unit: unit
                  unit_price: 800
                  taxable_base: 800
                  main_tax:
                    type: IVA
                    percentage: 21
                    regime_key: '01'
                  line_total: 968
              totals:
                taxable_base: 800
                total_vat: 168
                total_irpf: 0
                total_equivalence_surcharge: 0
                vat_breakdown:
                  - type: 21
                    base: 800
                    amount: 168
                invoice_total: 968
              pdf_download_url: /v1/invoices/550e8400-e29b-41d4-a716-446655440003/pdf
              created_at: '2024-12-10T11:00:00Z'
              updated_at: '2024-12-10T11:00:00Z'
          pagination:
            current_page: 1
            items_per_page: 20
            total_items: 3
            total_pages: 1
            has_next: false
            has_previous: false
        meta:
          timestamp: '2025-01-20T12:00:00Z'
          request_id: 550e8400-e29b-41d4-a716-446655440000
    create-basic:
      summary: Basic ordinary invoice
      description: Registered customer, 21% VAT, single service line
      value:
        type: STANDARD
        recipient:
          customer_id: 123e4567-e89b-12d3-a456-426614174000
        lines:
          - description: Corporate website development
            quantity: 40
            unit: hours
            unit_price: 37.5
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
        payment_info:
          method: BANK_TRANSFER
          iban: ES9121000418450200051332
          payment_term_days: 30
        notes: Payment via bank transfer
    create-with-adhoc-customer:
      summary: Invoice with ad-hoc customer
      description: Unregistered customer, inline data, multiple line items
      value:
        type: STANDARD
        recipient:
          legal_name: Comercial Martínez SL
          nif: B87654321
          email: contacto@comercialmartinez.es
          phone: '+34912345678'
          address:
            street: Avenida de la Constitución
            number: '45'
            floor: 3º B
            postal_code: '41001'
            city: Sevilla
            province: Sevilla
            country: España
        lines:
          - description: Business strategic consulting
            quantity: 8
            unit: hours
            unit_price: 125
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
          - description: Executive report preparation
            quantity: 1
            unit: document
            unit_price: 500
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
        payment_info:
          method: BANK_TRANSFER
          iban: ES9121000418450200051332
          payment_term_days: 30
        notes: |
          Invoice for consulting services for the month of January.
          Includes 8 hours of consulting and report preparation.
    create-issue-directly:
      summary: Create, issue directly, and send by email
      description: |
        Skip draft state. Creates invoice directly as ISSUED with:
        - Definitive invoice number assigned
        - PDF generated (sync or async based on options.wait_for_pdf)
        - Email sent automatically if options.send_automatically=true
      value:
        type: STANDARD
        recipient:
          customer_id: 123e4567-e89b-12d3-a456-426614174000
        lines:
          - description: Monthly web application maintenance
            quantity: 1
            unit: month
            unit_price: 850
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
            irpf_rate: 15
        payment_info:
          method: DIRECT_DEBIT
          iban: ES9121000418450200051332
        options:
          issue_directly: true
          wait_for_pdf: true
          send_automatically: true
    create-with-custom-email:
      summary: Create invoice with custom email recipients
      description: |
        Issues invoice and sends to multiple recipients with:
        - Custom recipient list (overrides recipient.email)
        - CC to accounting department
        - Personalized subject and message
      value:
        type: STANDARD
        due_date: '2025-02-26'
        recipient:
          legal_name: Acme Corporation SL
          nif: B12345678
          email: info@acme.es
          address:
            street: Calle Gran Vía
            number: '42'
            floor: 5º
            postal_code: '28013'
            city: Madrid
            province: Madrid
            country: España
        lines:
          - description: Mobile application development - Phase 1
            quantity: 80
            unit: hours
            unit_price: 65
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
            irpf_rate: 15
          - description: Application UX/UI design
            quantity: 20
            unit: hours
            unit_price: 55
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
            irpf_rate: 15
        payment_info:
          method: BANK_TRANSFER
          iban: ES9121000418450200051332
          payment_term_days: 30
        notes: Project APP-2025. Includes source code and technical documentation.
        metadata:
          project_code: APP-2025-001
          purchase_order: PO-ACME-2025-042
        options:
          issue_directly: true
          wait_for_pdf: true
          send_automatically: true
          email_config:
            recipients:
              - facturas@acme.es
              - pedro.garcia@acme.es
              - maria.lopez@acme.es
            cc:
              - contabilidad@miempresa.com
              - gestor@asesoria.es
            subject: Invoice project APP-2025 - Mobile application development
            message: |
              Dear Acme Corporation team,

              Please find attached the invoice corresponding to the first phase
              of the mobile application development as agreed.

              Payment is due within 30 days. For any questions,
              please do not hesitate to contact us.

              Kind regards.
    simplified-with-nif:
      summary: Simplified with NIF ≤3000€
      description: Simplified invoice with NIF, max 3000€ total. No full address required
      value:
        type: SIMPLIFIED
        recipient:
          legal_name: María García López
          nif: 12345678A
        lines:
          - description: Security system installation
            quantity: 1
            unit: installation
            unit_price: 1800
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '20'
          - description: Surveillance cameras - 4 units
            quantity: 4
            unit: unit
            unit_price: 150
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '20'
        payment_info:
          method: CARD
    under-3000:
      summary: Simplified invoice under 3000 EUR
      description: Ticket invoice for consumer without NIF, total under 3000 EUR limit
      value:
        type: SIMPLIFIED
        recipient: {}
        lines:
          - description: Menú del día
            quantity: 2
            unit: unit
            unit_price: 14.5
            main_tax:
              type: IVA
              percentage: 10
              regime_key: '01'
        options:
          issue_directly: true
    spanish-b2b:
      summary: Standard B2B invoice - Spanish business customer
      description: Invoice to ACCIONA SA with full address and 30-day payment term
      value:
        type: STANDARD
        due_date: '2026-06-19'
        recipient:
          legal_name: ACCIONA SA
          nif: A08001851
          address:
            street: Avenida de Europa
            number: '18'
            postal_code: '28108'
            city: Alcobendas
            province: Madrid
            country: España
            country_code: ES
        lines:
          - description: Consultoría de arquitectura — Sprint mayo
            quantity: 40
            unit: hours
            unit_price: 75
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
        payment_info:
          method: BANK_TRANSFER
          iban: ES9121000418450200051332
          payment_term_days: 30
        options:
          issue_directly: true
    multiple-vat-rates:
      summary: Standard invoice with multiple VAT rates
      description: Three lines with 21%, 10%, and 4% VAT rates mixed
      value:
        type: STANDARD
        recipient:
          legal_name: ACCIONA SA
          nif: A08001851
          address:
            street: Calle Cava Baja
            number: '35'
            postal_code: '28005'
            city: Madrid
            province: Madrid
            country: España
            country_code: ES
        lines:
          - description: Vino reserva (caja 6 botellas)
            quantity: 4
            unit: box
            unit_price: 90
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
          - description: Servicio de catering — menú degustación
            quantity: 25
            unit: menu
            unit_price: 32
            main_tax:
              type: IVA
              percentage: 10
              regime_key: '01'
          - description: Pan artesano
            quantity: 50
            unit: unit
            unit_price: 1.2
            main_tax:
              type: IVA
              percentage: 4
              regime_key: '01'
        options:
          issue_directly: true
    irpf-withholding:
      summary: Standard invoice with IRPF withholding (professional services)
      description: Professional services with 15% IRPF tax retention
      value:
        type: STANDARD
        recipient:
          legal_name: ACCIONA SA
          nif: A08001851
          address:
            street: Avenida de Europa
            number: '18'
            postal_code: '28108'
            city: Alcobendas
            province: Madrid
            country: España
            country_code: ES
        lines:
          - description: Servicios de abogacía — asesoramiento mercantil
            quantity: 12
            unit: hours
            unit_price: 120
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
            irpf_rate: 15
        options:
          issue_directly: true
    recargo-equivalencia:
      summary: Standard invoice with equivalence surcharge (RECAREGO)
      description: Retail invoice with 21% IVA + 5.2% equivalence surcharge (regime_key 18)
      value:
        type: STANDARD
        recipient:
          legal_name: ACCIONA SA
          nif: A08001851
          address:
            street: Calle del Comercio
            number: '56'
            postal_code: '08001'
            city: Barcelona
            province: Barcelona
            country: España
            country_code: ES
        lines:
          - description: Lote de auriculares inalámbricos para reventa
            quantity: 30
            unit: unit
            unit_price: 45
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '18'
            equivalence_surcharge_rate: 5.2
        options:
          issue_directly: true
    b2b-intra-eu-goods-e5:
      summary: B2B intra-EU goods (E5) - exempt Art. 25
      description: EU-to-EU goods sale to German business with NIF_IVA, 0% IVA exempt
      value:
        type: STANDARD
        recipient:
          legal_name: Berliner Industrie GmbH
          alternative_id:
            type: NIF_IVA
            number: DE123456789
            country_code: DE
          address:
            street: Friedrichstraße
            number: '200'
            postal_code: '10117'
            city: Berlin
            province: Berlin
            country: Alemania
            country_code: DE
        lines:
          - description: Componentes electrónicos — pedido EU-2026-0042
            quantity: 500
            unit: unit
            unit_price: 8.4
            main_tax:
              type: IVA
              percentage: 0
              regime_key: '01'
            exemption_reason: EXENTA_ART_25
        options:
          issue_directly: true
    b2b-intra-eu-services-n2:
      summary: B2B intra-EU services (N2) - no sujeta localization
      description: EU-to-EU services to German business, not subject to Spanish VAT
      value:
        type: STANDARD
        recipient:
          legal_name: Berliner Industrie GmbH
          alternative_id:
            type: NIF_IVA
            number: DE123456789
            country_code: DE
          address:
            street: Friedrichstraße
            number: '200'
            postal_code: '10117'
            city: Berlin
            province: Berlin
            country: Alemania
            country_code: DE
        lines:
          - description: Consultoría de arquitectura cloud
            quantity: 60
            unit: hours
            unit_price: 110
            main_tax:
              type: IVA
              percentage: 0
              regime_key: '01'
            exemption_reason: NO_SUJETA_LOCALIZACION
        options:
          issue_directly: true
    b2c-oss-over-threshold:
      summary: B2C OSS (One-Stop-Shop) over threshold
      description: Cross-border B2C sales over 10000 EUR annual threshold, regime 17 OSS
      value:
        type: STANDARD
        recipient:
          legal_name: Anna Schmidt
          alternative_id:
            type: PASSPORT
            number: C0HJ4P9DT
            country_code: DE
          address:
            street: Müllerstraße
            number: '47'
            postal_code: '80469'
            city: München
            province: Bayern
            country: Alemania
            country_code: DE
        lines:
          - description: Suscripción anual plataforma SaaS
            quantity: 1
            unit: year
            unit_price: 480
            main_tax:
              type: IVA
              percentage: 0
              regime_key: '17'
            exemption_reason: NO_SUJETA_LOCALIZACION
        options:
          issue_directly: true
    b2c-under-oss-threshold:
      summary: B2C under OSS threshold
      description: Cross-border B2C sale below 10000 EUR annual threshold, standard 21% Spanish IVA
      value:
        type: STANDARD
        recipient:
          legal_name: Anna Schmidt
          alternative_id:
            type: PASSPORT
            number: C0HJ4P9DT
            country_code: DE
          address:
            street: Müllerstraße
            number: '47'
            postal_code: '80469'
            city: München
            province: Bayern
            country: Alemania
            country_code: DE
        lines:
          - description: Curso online — fotografía digital
            quantity: 1
            unit: course
            unit_price: 149
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
        options:
          issue_directly: true
    export-non-eu-goods-e2:
      summary: Export of goods to non-EU (E2) - exempt Art. 21
      description: Non-EU goods export with 0% IVA exempt, regime_key 02
      value:
        type: STANDARD
        recipient:
          legal_name: Northeast Imports Inc.
          alternative_id:
            type: PASSPORT
            number: '551234567'
            country_code: US
          address:
            street: 5th Avenue
            number: '725'
            postal_code: '10022'
            city: New York
            province: NY
            country: Estados Unidos
            country_code: US
        lines:
          - description: Aceite de oliva virgen extra — palet 480 botellas
            quantity: 1
            unit: pallet
            unit_price: 3850
            main_tax:
              type: IVA
              percentage: 0
              regime_key: '02'
            exemption_reason: EXENTA_ART_21
        options:
          issue_directly: true
    services-non-eu-n2:
      summary: Services to non-EU (N2) - no sujeta
      description: Non-EU services not subject to Spanish VAT
      value:
        type: STANDARD
        recipient:
          legal_name: Northeast Imports Inc.
          alternative_id:
            type: PASSPORT
            number: '551234567'
            country_code: US
          address:
            street: 5th Avenue
            number: '725'
            postal_code: '10022'
            city: New York
            province: NY
            country: Estados Unidos
            country_code: US
        lines:
          - description: Diseño de identidad de marca
            quantity: 1
            unit: project
            unit_price: 6500
            main_tax:
              type: IVA
              percentage: 0
              regime_key: '01'
            exemption_reason: NO_SUJETA_LOCALIZACION
        options:
          issue_directly: true
    exempt-art-20-medical:
      summary: Exempt operation (E1) - educational/medical Art. 20
      description: Educational services exempt under Art. 20 LIVA
      value:
        type: STANDARD
        recipient:
          legal_name: Academia Cervantes SL
          nif: A08001851
          address:
            street: Calle Princesa
            number: '27'
            postal_code: '28008'
            city: Madrid
            province: Madrid
            country: España
            country_code: ES
        lines:
          - description: Clases particulares de matemáticas — trimestre primavera
            quantity: 36
            unit: hours
            unit_price: 28
            main_tax:
              type: IVA
              percentage: 0
              regime_key: '01'
            exemption_reason: EXENTA_ART_20
            exemption_reason_text: Operación exenta de IVA según Art. 20.Uno.10º LIVA (enseñanza)
        options:
          issue_directly: true
    igic-canary-islands:
      summary: IGIC (Canary Islands) 7% instead of IVA
      description: Regional tax for Canary Islands operations
      value:
        type: STANDARD
        recipient:
          legal_name: Importaciones Atlántico SL
          nif: A08001851
          address:
            street: Calle León y Castillo
            number: '200'
            postal_code: '35004'
            city: Las Palmas de Gran Canaria
            province: Las Palmas
            country: España
            country_code: ES
        lines:
          - description: Mobiliario de oficina — sillas ergonómicas
            quantity: 10
            unit: unit
            unit_price: 180
            main_tax:
              type: IGIC
              percentage: 7
              regime_key: '01'
        options:
          issue_directly: true
    not-subject-art-7-9:
      summary: Not subject (N1) - Art. 7/9 LIVA
      description: Internal transfers or promotional samples not subject to VAT
      value:
        type: STANDARD
        recipient:
          legal_name: Distribuciones Hermanas Pérez SL
          nif: A08001851
          address:
            street: Polígono Industrial San Isidro
            number: '12'
            postal_code: '46980'
            city: Paterna
            province: Valencia
            country: España
            country_code: ES
        lines:
          - description: Muestras comerciales sin valor — catálogo 2026
            quantity: 50
            unit: unit
            unit_price: 12
            main_tax:
              type: IVA
              percentage: 0
              regime_key: '01'
            exemption_reason: NO_SUJETA_ART_7_9
            exemption_reason_text: Entrega de muestras gratuitas (Art. 7.4º LIVA)
        options:
          issue_directly: true
    isp-reverse-charge-s2:
      summary: ISP reverse charge (S2) - Art. 84.2.f LIVA
      description: Construction services with buyer self-assessing VAT
      value:
        type: STANDARD
        recipient:
          legal_name: Promociones Inmobiliarias del Mediterráneo SA
          nif: A46789012
          address:
            street: Gran Vía Marqués del Turia
            number: '47'
            postal_code: '46005'
            city: Valencia
            province: Valencia
            country: España
            country_code: ES
        lines:
          - description: Ejecución de obra — instalación eléctrica edificio Torre Norte
            quantity: 1
            unit: project
            unit_price: 42000
            main_tax:
              type: IVA
              percentage: 0
              regime_key: '01'
            exemption_reason: ISP_ART_84_2_F
            exemption_reason_text: Inversión del sujeto pasivo — Art. 84.Uno.2º.f LIVA (ejecución de obra)
        options:
          issue_directly: true
    rebu-used-goods:
      summary: REBU special regime (used goods, art, antiques)
      description: Used goods with margin-based taxation - base calculated on difference between sale and cost
      value:
        type: STANDARD
        recipient:
          legal_name: Galería de Arte Velázquez SL
          nif: A08001851
          address:
            street: Calle de Jorge Juan
            number: '12'
            postal_code: '28001'
            city: Madrid
            province: Madrid
            country: España
            country_code: ES
        lines:
          - description: 'Pintura óleo siglo XIX — REBU (margen): venta 3 000 € / coste 2 000 €'
            quantity: 1
            unit: unit
            unit_price: 826.45
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '03'
        notes: Régimen especial de bienes usados (Art. 135 LIVA). Base imponible calculada sobre el margen.
        options:
          issue_directly: true
    created-success:
      summary: Invoice created (201)
      description: Invoice created successfully in DRAFT state with draft number and totals
      value:
        success: true
        data:
          id: f47ac10b-58cc-4372-a567-0e02b2c3d479
          invoice_number: null
          number: null
          type: STANDARD
          status: DRAFT
          issue_date: '2025-01-20'
          issuer:
            legal_name: Tu Empresa SL
            nif: B12345678
            address:
              street: Calle Ejemplo
              number: '123'
              postal_code: '28001'
              city: Madrid
              province: Madrid
              country: España
          series:
            id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
            code: A
          recipient:
            customer_id: 123e4567-e89b-12d3-a456-426614174000
            legal_name: Cliente Ejemplo SL
            nif: B87654321
            email: cliente@ejemplo.com
            address:
              street: Avenida Cliente
              number: '456'
              postal_code: '28013'
              city: Madrid
              province: Madrid
              country: España
          lines:
            - description: Corporate website development
              quantity: 40
              unit: hours
              unit_price: 37.5
              discount_percentage: 0
              taxable_base: 1500
              main_tax:
                type: IVA
                percentage: 21
                regime_key: '01'
              line_total: 1815
          totals:
            taxable_base: 1500
            total_vat: 315
            total_irpf: 0
            total_equivalence_surcharge: 0
            vat_breakdown:
              - type: 21
                base: 1500
                amount: 315
            invoice_total: 1815
          payment_info:
            method: BANK_TRANSFER
            payment_term_days: 30
          notes: Payment via bank transfer
          verifactu:
            enabled: false
          created_at: '2025-01-20T10:30:00Z'
          updated_at: '2025-01-20T10:30:00Z'
        meta:
          timestamp: '2025-01-20T10:30:00Z'
          request_id: c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f
    conflict-409:
      summary: Conflict (409)
      description: Duplicate resource exists (NIF, email, invoice number, series code)
      value:
        success: false
        error:
          code: CONFLICT
          message: Resource already exists
          details:
            conflict_type: DUPLICATE_NIF
            field: nif
            value: B12345678
            existing_resource_id: 123e4567-e89b-12d3-a456-426614174000
            message: A customer with NIF B12345678 already exists
        meta:
          timestamp: '2025-01-20T10:00:00Z'
          request_id: d4d4d4d4-0004-4000-a000-000000000004
    validation-422:
      summary: Validation error (422)
      description: Request data fails validation (missing fields, invalid format, business rules)
      value:
        success: false
        error:
          code: VALIDATION_ERROR
          message: Request validation failed
          details:
            errors:
              - field: nif
                message: Invalid NIF format. Expected 9 characters (8 digits + letter or letter + 8 digits)
                value: B123INVALID
              - field: lineas
                message: At least one line item is required
                value: []
              - field: receptor.direccion.codigo_postal
                message: Invalid postal code format. Expected 5 digits
                value: '280'
              - field: lineas[0].precio_unitario
                message: Unit price must be greater than or equal to 0
                value: -10.5
        meta:
          timestamp: '2025-01-20T10:00:00Z'
          request_id: c3c3c3c3-0003-4000-a000-000000000003
    get-invoice-success:
      summary: Get invoice (200)
      description: Single invoice retrieved with full details including VeriFactu signature and PDF URL
      value:
        success: true
        data:
          id: f47ac10b-58cc-4372-a567-0e02b2c3d479
          invoice_number: A/2025/0042
          series:
            id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
            code: A
          number: 42
          type: STANDARD
          status: ISSUED
          issue_date: '2025-01-20'
          due_date: '2025-02-20'
          issuer:
            legal_name: Tu Empresa SL
            nif: B12345678
            address:
              street: Calle Ejemplo
              number: '123'
              postal_code: '28001'
              city: Madrid
              province: Madrid
              country: España
            email: info@tuempresa.es
            phone: '+34912345678'
          recipient:
            customer_id: 123e4567-e89b-12d3-a456-426614174000
            legal_name: Cliente Ejemplo SL
            nif: B87654321
            email: cliente@ejemplo.com
            address:
              street: Avenida Cliente
              number: '456'
              postal_code: '28013'
              city: Madrid
              province: Madrid
              country: España
          lines:
            - description: Corporate website development
              quantity: 40
              unit: hours
              unit_price: 37.5
              discount_percentage: 0
              taxable_base: 1500
              main_tax:
                type: IVA
                percentage: 21
                regime_key: '01'
              irpf_rate: 15
              line_total: 1590
          totals:
            taxable_base: 1500
            total_vat: 315
            total_irpf: 225
            total_equivalence_surcharge: 0
            vat_breakdown:
              - type: 21
                base: 1500
                amount: 315
            irpf_breakdown:
              - type: 15
                base: 1500
                amount: 225
            invoice_total: 1590
          payment_info:
            method: BANK_TRANSFER
            iban: ES9121000418450200051332
            payment_term_days: 30
          notes: Payment via bank transfer
          verifactu:
            enabled: true
            invoice_hash: a7f3c9e2b1d4f8a6c3e9b2d5f1a8c4e7b9d2f5a1c8e4b7d3f9a2c6e1b5d8f4a7
            chaining_hash: c9e2b1d4f8a6c3e9b2d5f1a8c4e7b9d2f5a1c8e4b7d3f9a2c6e1b5d8f4a7b3c1
            qr_url: https://verifactu.agenciatributaria.gob.es/v?id=a7f3c9e2b1d4f8a6
            registration_date: '2025-01-20T10:35:00Z'
            submission_status: ACCEPTED
          pdf_download_url: /v1/invoices/f47ac10b-58cc-4372-a567-0e02b2c3d479/pdf
          created_at: '2025-01-20T10:30:00Z'
          updated_at: '2025-01-20T10:35:00Z'
        meta:
          timestamp: '2025-01-20T11:00:00Z'
          request_id: f4a5b6c7-d8e9-4f0a-1b2c-3d4e5f6a7b8c
    generated-invoice:
      summary: Stripe-generated invoice (200)
      description: |
        A SIMPLIFIED invoice auto-generated from a Stripe Connect charge. Note the
        `metadata` block: BeeL writes system keys you can filter on — most usefully
        `external_customer_id` (the Stripe customer) and `external_payment_id`. Use
        `GET /v1/invoices?metadata[external_customer_id]=cus_…` to list a customer's
        full history. See the "Filtering by metadata" guide for the full key list.
      value:
        success: true
        data:
          id: 9d2fd4c2-29a1-4f65-90f7-78854adf69bc
          invoice_number: A/2026/0204
          series:
            id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
            code: A
          number: 204
          type: SIMPLIFIED
          status: PAID
          issue_date: '2026-06-02'
          due_date: '2026-06-02'
          issuer:
            legal_name: Tu Empresa SL
            nif: B12345678
            address:
              street: Calle Ejemplo
              number: '123'
              postal_code: '28001'
              city: Madrid
              province: Madrid
              country: España
            email: info@tuempresa.es
            phone: '+34912345678'
          recipient:
            legal_name: Aleksei Borisov
            email: asbfox@gmail.com
          lines:
            - description: Online subscription
              quantity: 1
              unit: service
              unit_price: 81.82
              discount_percentage: 0
              taxable_base: 81.82
              main_tax:
                type: IVA
                percentage: 21
                regime_key: '01'
              irpf_rate: 0
              line_total: 99
          totals:
            taxable_base: 81.82
            total_vat: 17.18
            total_irpf: 0
            total_equivalence_surcharge: 0
            vat_breakdown:
              - type: 21
                base: 81.82
                amount: 17.18
            irpf_breakdown: []
            invoice_total: 99
          payment_info:
            method: CARD
          metadata:
            external_customer_id: cus_ULGk8bzIr88aag
            external_payment_id: pi_3Tdpm4RzLnzHAY341P3TsOfk
            payment_intent_id: pi_3Tdpm4RzLnzHAY341P3TsOfk
            charge_id: ch_3Tdpm4RzLnzHAY341upZN1op
            payment_provider: STRIPE_CONNECT
            stripe_fiscal_mode: INCLUSIVE
            original_amount: '99.00'
            original_currency: eur
            order_id: '1593'
            site_url: https://superpole.es
          verifactu:
            enabled: true
            invoice_hash: a7f3c9e2b1d4f8a6c3e9b2d5f1a8c4e7b9d2f5a1c8e4b7d3f9a2c6e1b5d8f4a7
            chaining_hash: c9e2b1d4f8a6c3e9b2d5f1a8c4e7b9d2f5a1c8e4b7d3f9a2c6e1b5d8f4a7b3c1
            qr_url: https://verifactu.agenciatributaria.gob.es/v?id=a7f3c9e2b1d4f8a6
            registration_date: '2026-06-02T10:35:00Z'
            submission_status: ACCEPTED
          pdf_download_url: /v1/invoices/9d2fd4c2-29a1-4f65-90f7-78854adf69bc/pdf
          created_at: '2026-06-02T10:30:00Z'
          updated_at: '2026-06-02T10:35:00Z'
        meta:
          timestamp: '2026-06-02T11:00:00Z'
          request_id: f4a5b6c7-d8e9-4f0a-1b2c-3d4e5f6a7b8c
    update-invoice:
      summary: Update draft invoice
      description: Update invoice in DRAFT state. Modify dates, lines, recipient, payment method
      value:
        lines:
          - description: Corporate website development - Updated
            quantity: 45
            unit: hours
            unit_price: 40
            discount_percentage: 5
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
          - description: Web maintenance - 3 months
            quantity: 3
            unit: month
            unit_price: 150
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
        payment_info:
          method: DIRECT_DEBIT
          iban: ES9121000418450200051332
        notes: Invoice updated with new maintenance line. Payment via direct debit.
    void-issued-by-error:
      summary: Void invoice - issued by error
      description: Voiding operation when invoice should not exist (duplicate, wrong customer)
      value:
        reason: Factura emitida por error sobre un cliente equivocado (operación nunca realizada)
        void_date: '2026-05-20'
    corrective-total-r1:
      summary: Total corrective R1 - Legal error
      description: R1 code for legal errors. Fully cancels original invoice (status VOIDED)
      value:
        rectification_type: TOTAL
        rectification_code: R1
        reason: Cancellation of invoice issued due to a legally founded error under Art. 80 Uno LIVA. The transaction was not completed due to project cancellation before commencement.
        notes: Original invoice F/2025/0042 fully cancelled. Customer notified.
    corrective-total-r4:
      summary: Total corrective R4 - Other reasons
      description: R4 code (most common). General cancellations, customer requests, etc
      value:
        rectification_type: TOTAL
        rectification_code: R4
        reason: Invoice cancellation at customer's request. The service was cancelled before delivery and full cancellation of the invoice has been agreed.
        notes: Invoice cancelled at customer's request. No outstanding amount pending collection.
    corrective-partial-r2:
      summary: Partial corrective R2 - Bankruptcy
      description: R2 code for bankruptcy proceedings. Partial correction with amount adjustment
      value:
        rectification_type: PARTIAL
        rectification_code: R2
        reason: Partial correction of invoice due to customer bankruptcy proceedings under Art. 80 Tres LIVA. Amount adjusted to the agreement reached with the insolvency administrator.
        lines:
          - description: Adjustment for bankruptcy proceedings - 40% amount reduction
            quantity: -16
            unit: hours
            unit_price: 37.5
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
        notes: Correction approved by insolvency administrator. Case 123/2024 Commercial Court No. 2 Madrid.
    corrective-partial-r3:
      summary: Partial corrective R3 - Bad debts
      description: R3 code for bad debts. VAT recovery on unpaid invoices after 6 months
      value:
        rectification_type: PARTIAL
        rectification_code: R3
        reason: Partial correction for bad debt under Art. 80 Cuatro LIVA. More than 6 months have elapsed since the invoice due date and collection efforts via notarial demands have been documented without result.
        lines:
          - description: Bad debt adjustment - Full cancellation of outstanding amount
            quantity: -40
            unit: hours
            unit_price: 37.5
            discount_percentage: 0
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
        notes: Collection efforts documented via notarial demands (Protocol 456/2024). Original invoice due on 15/03/2024.
    r1-partial-discount:
      summary: R1 corrective (partial) - post-issuance discount
      description: Partial rectification with negative quantity for discount adjustment
      value:
        rectification_type: PARTIAL
        rectification_code: R1
        reason: Descuento comercial post-emisión acordado con el cliente (5 horas no facturables)
        lines:
          - description: Ajuste por horas no facturables — Sprint mayo
            quantity: -5
            unit: hours
            unit_price: 75
            main_tax:
              type: IVA
              percentage: 21
              regime_key: '01'
        options:
          issue_directly: true
    r5-total-simplified:
      summary: R5 corrective (total) - simplified to full invoice
      description: Total rectification of simplified invoice - no lines needed, auto-negated
      value:
        rectification_type: TOTAL
        rectification_code: R5
        reason: Cliente solicita factura completa (F1) con sus datos fiscales tras emitir tique simplificado
    list-customers-success:
      summary: List customers (200)
      description: Customer list with pagination
      value:
        success: true
        data:
          customers:
            - id: 123e4567-e89b-12d3-a456-426614174000
              legal_name: Tech Solutions SL
              nif: B12345678
              email: admin@techsolutions.com
              phone: '+34912345678'
              address:
                street: Calle Mayor
                number: '15'
                postal_code: '28013'
                city: Madrid
                province: Madrid
                country: España
              active: true
              created_at: '2024-06-15T10:30:00Z'
              updated_at: '2025-01-10T14:00:00Z'
            - id: 789abc12-34de-56f7-8901-234567890abc
              legal_name: María García López
              nif: 12345678Z
              email: maria.garcia@email.com
              phone: '+34654321987'
              address:
                street: Avenida de la Constitución
                number: '25'
                floor: 3º
                door: B
                postal_code: '41001'
                city: Sevilla
                province: Sevilla
                country: España
              active: true
              created_at: '2024-08-20T09:00:00Z'
              updated_at: '2024-12-01T11:30:00Z'
            - id: 456def78-90ab-12cd-3456-78ef01234567
              legal_name: Distribuidora Levante SA
              nif: A22222222
              email: contabilidad@distribuidoralevante.es
              phone: '+34963456789'
              address:
                street: Polígono Industrial Norte
                number: '7'
                postal_code: '46007'
                city: Valencia
                province: Valencia
                country: España
              active: false
              created_at: '2024-03-10T16:45:00Z'
              updated_at: '2024-11-20T08:00:00Z'
          pagination:
            current_page: 1
            items_per_page: 20
            total_items: 3
            total_pages: 1
            has_next: false
            has_previous: false
        meta:
          timestamp: '2025-01-20T12:00:00Z'
          request_id: 550e8400-e29b-41d4-a716-446655440000
    company-customer:
      summary: Spanish company (SL/SA)
      description: 'Spanish company with CIF format: Letter + 8 digits (e.g., B12345678)'
      value:
        legal_name: Tech Solutions SL
        nif: B12345678
        email: admin@techsolutions.com
        phone: '+34912345678'
        address:
          street: Calle Mayor
          number: '123'
          floor: 2º B
          postal_code: '28013'
          city: Madrid
          province: Madrid
          country: España
        notes: Cliente prioritario - Pago puntual
    freelancer-customer:
      summary: Self-employed individual
      description: 'Self-employed with personal NIF. Format: 8 digits + letter (e.g., 12345678Z)'
      value:
        legal_name: María García López
        nif: 12345678Z
        email: maria.garcia@email.com
        phone: '+34654321987'
        address:
          street: Avenida de la Constitución
          number: '45'
          floor: 3º A
          postal_code: '41001'
          city: Sevilla
          province: Sevilla
          country: España
        notes: Diseñadora gráfica freelance
    eu_customer:
      summary: EU Customer with VAT
      value:
        legal_name: Tech Europe GmbH
        alternative_id:
          type: '02'
          number: DE123456789
          country_code: DE
        email: invoices@techeurope.de
        phone: '+49301234567'
        address:
          street: Hauptstraße
          number: '456'
          postal_code: '10115'
          city: Berlin
          province: Berlin
          country: Alemania
        notes: Cliente intracomunitario - Aplicar inversión del sujeto pasivo
    us_customer:
      summary: US Customer with EIN
      value:
        legal_name: American Tech Corp
        alternative_id:
          type: '06'
          number: 12-3456789
          country_code: US
        email: billing@americantech.com
        phone: '+14155551234'
        address:
          street: Main Street
          number: '123'
          floor: Suite 500
          postal_code: '94102'
          city: San Francisco
          province: California
          country: Estados Unidos
        notes: Cliente extracomunitario - Factura de exportación
    spanish-business:
      summary: Spanish business customer (ACCIONA)
      description: Customer creation for Spanish company with full address and NIF
      value:
        legal_name: ACCIONA SA
        nif: A08001851
        address:
          street: Avenida de Europa
          number: '18'
          postal_code: '28108'
          city: Alcobendas
          province: Madrid
          country: España
          country_code: ES
    created-success-2:
      summary: Customer created (201)
      description: Customer created successfully with generated ID and initial statistics
      value:
        success: true
        data:
          id: 123e4567-e89b-12d3-a456-426614174000
          legal_name: Tech Solutions SL
          nif: B12345678
          email: admin@techsolutions.com
          phone: '+34912345678'
          address:
            street: Calle Mayor
            number: '123'
            floor: 2º B
            postal_code: '28013'
            city: Madrid
            province: Madrid
            country: España
          notes: Cliente prioritario - Pago puntual
          active: true
          created_at: '2025-01-20T10:00:00Z'
          updated_at: '2025-01-20T10:00:00Z'
        meta:
          timestamp: '2025-01-20T10:00:00Z'
          request_id: c1c1c1c1-0001-4000-a000-000000000001
    get-customer-success:
      summary: Get customer (200)
      description: Single customer retrieved with full details
      value:
        success: true
        data:
          id: 123e4567-e89b-12d3-a456-426614174000
          legal_name: Tech Solutions SL
          nif: B12345678
          email: admin@techsolutions.com
          phone: '+34912345678'
          address:
            street: Calle Mayor
            number: '123'
            floor: 2º B
            postal_code: '28013'
            city: Madrid
            province: Madrid
            country: España
          notes: Cliente prioritario - Pago puntual
          active: true
          created_at: '2024-03-15T09:00:00Z'
          updated_at: '2025-01-20T10:00:00Z'
        meta:
          timestamp: '2025-01-20T11:00:00Z'
          request_id: b6c7d8e9-f0a1-4b2c-3d4e-5f6a7b8c9d0e
  responses:
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: UNAUTHORIZED
              message: Authentication required
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: INTERNAL_ERROR
              message: Internal server error
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    InvalidJsonFormat:
      description: |
        Invalid JSON format — a field has a wrong type or format (e.g. invalid date, unknown enum value, malformed UUID).
        The `details` object contains `field`, `invalid_value`, and optionally `expected_format` or `allowed_values`.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/ErrorResponse'
              - type: object
                properties:
                  error:
                    allOf:
                      - $ref: '#/components/schemas/ErrorDetail'
                      - type: object
                        properties:
                          details:
                            $ref: '#/components/schemas/FieldDeserializationError'
          example:
            success: false
            error:
              code: INVALID_JSON_FORMAT
              message: 'The field ''due_date'' has an invalid date format: ''2026-03-04fds''. Expected format: YYYY-MM-DD.'
              details:
                field: due_date
                invalid_value: 2026-03-04fds
                expected_format: YYYY-MM-DD
            meta:
              timestamp: '2026-03-05T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: NOT_FOUND
              message: Resource not found
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: VALIDATION_ERROR
              message: Validation error
              details:
                field_name: Field is required
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: FORBIDDEN
              message: You do not have permission to access this resource
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: BAD_REQUEST
              message: Invalid request
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    UnprocessableEntity:
      description: Unprocessable entity - validation or business rule error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: UNPROCESSABLE_ENTITY
              message: Data cannot be processed
              details:
                field: Specific error description
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    Conflict:
      description: Resource conflict (e.g. duplicate)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: CONFLICT
              message: Resource already exists
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
    PayloadTooLarge:
      description: Payload too large - file or request body exceeds the maximum allowed size
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: FILE_TOO_LARGE
              message: The file exceeds the maximum allowed size
            meta:
              timestamp: '2025-01-15T10:30:00Z'
              request_id: 4bf92f3577b34da6a3ce929d0e0e4736
x-tagGroups:
  - name: Invoices
    x-icon: FileText
    tags:
      - Invoices
      - InvoiceLifecycle
      - InvoiceDelivery
  - name: Customers
    x-icon: Users
    tags:
      - Customers
      - Customer Import
      - Customer Templates
  - name: Products
    x-icon: Package
    tags:
      - Products
  - name: Invoice Series
    x-icon: List
    tags:
      - InvoiceSeries
  - name: Tax Configuration
    x-icon: Calculator
    tags:
      - ConfigurationTax
  - name: VeriFactu
    x-icon: ShieldCheck
    tags:
      - ConfigurationVeriFactu
  - name: Preferences
    x-icon: Settings
    tags:
      - ConfigurationPreferences
  - name: NIF Validation
    x-icon: Search
    tags:
      - NIF
  - name: Companies
    x-icon: Building2
    tags:
      - PublicCompanies
      - PublicCompanyRepresentations
      - PublicCompanyApiKeys
  - name: Recurring Invoices
    x-icon: Repeat
    tags:
      - RecurringInvoices
  - name: Webhooks
    x-icon: Webhook
    tags:
      - Webhooks
