openapi: 3.0.3
info:
  title: 'accounting API'
  description: 'Public REST API for managing clients, invoices, and recurring forecasts. Authenticated with Sanctum personal access tokens, scoped to a single team.'
  version: 1.0.0
servers:
  -
    url: 'https://vorgio.app'
tags:
  -
    name: 'Checkout intents'
    description: "\nTwo-step checkout for the JS widget (sub-plan 11 §6):\n\n  1. Shop's server: `POST /v1/checkout-intents` (Bearer token,\n     `checkouts:write`). Stores the payload, returns id + one-shot\n     `client_secret`.\n  2. Browser (`vorgio.js`): `POST /v1/checkout-intents/{id}/confirm` with\n     the `client_secret`. No Sanctum auth — the secret IS the credential.\n     The same {@see CreateCheckout} composition that backs `/v1/checkouts`\n     runs, so the contract surface for shop integrators stays one shape."
  -
    name: Checkouts
    description: "\nHigh-level integration endpoint for third-party shops. One call creates or\nupdates the customer (`client.external_id` keyed per team), issues a\ngapless-numbered invoice, and queues the invoice email. The shop receives\n`invoice.sent` and `invoice.paid` webhook events for fulfilment."
  -
    name: Clients
    description: "\nManage the clients (customers) inside your team."
  -
    name: Invoices
    description: "\nCreate, send, and manage invoices and offers."
  -
    name: Recurring
    description: "\nForecasts of upcoming recurring-invoice generations.\n\nThe full forecast computation lands in sub-plan 07; this controller exposes\nwhatever is already on `invoices.next_invoice_at` so consumers can integrate\nagainst the route contract today."
components:
  securitySchemes:
    default:
      type: http
      scheme: bearer
      description: 'Tokens are minted from the team settings page. Each token carries a list of scoped abilities (`clients:read`, `invoices:write`, …) and is bound to a single team — it cannot access data in other teams.'
security:
  -
    default: []
paths:
  /api/v1/checkout-intents:
    post:
      summary: 'Create a checkout intent.'
      operationId: createACheckoutIntent
      description: "Server-to-server. Validates the same body shape as `POST /v1/checkouts`\n(plus an optional `return_url`) and stores it for the browser to\nconfirm."
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: Required.
          schema:
            type: string
      responses: {  }
      tags:
        - 'Checkout intents'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                client:
                  type: object
                  description: ''
                  example: []
                  properties:
                    name:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: z
                    name_addition:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: m
                      nullable: true
                    address:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: i
                    address_addition:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: 'y'
                      nullable: true
                    zip:
                      type: string
                      description: 'value darf nicht länger als 20 Zeichen sein.'
                      example: vdljnikhwaykcmyu
                    city:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: w
                    country:
                      type: string
                      description: 'value muss 2 Zeichen lang sein.'
                      example: pw
                    email:
                      type: string
                      description: 'value muss eine gültige E-Mail-Adresse sein. value darf nicht länger als 255 Zeichen sein.'
                      example: emelie.baumbach@example.net
                      nullable: true
                    email_cc:
                      type: array
                      description: 'value muss eine gültige E-Mail-Adresse sein. value darf nicht länger als 255 Zeichen sein.'
                      example:
                        - s
                      items:
                        type: string
                    email_text:
                      type: string
                      description: ''
                      example: architecto
                      nullable: true
                    language:
                      type: string
                      description: ''
                      example: en
                      enum:
                        - de
                        - en
                    rate:
                      type: integer
                      description: 'value muss mindestens 0 sein.'
                      example: 39
                    vat:
                      type: number
                      description: 'value muss mindestens 0 sein. value darf nicht größer als 100 sein.'
                      example: 7
                    default_position_mode:
                      type: string
                      description: ''
                      example: hourly
                      enum:
                        - hourly
                        - fixed
                    invoice_note:
                      type: string
                      description: ''
                      example: architecto
                      nullable: true
                    iban:
                      type: string
                      description: 'value darf nicht länger als 34 Zeichen sein.'
                      example: 'n'
                      nullable: true
                    bic:
                      type: string
                      description: 'value darf nicht länger als 11 Zeichen sein.'
                      example: gzmiyv
                      nullable: true
                    vat_id:
                      type: string
                      description: 'value darf nicht länger als 64 Zeichen sein.'
                      example: d
                      nullable: true
                    buyer_reference:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: l
                      nullable: true
                    e_invoice_format:
                      type: string
                      description: ''
                      example: none
                      enum:
                        - none
                        - zugferd
                        - xrechnung
                    external_id:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: j
                      nullable: true
                  required:
                    - name
                    - address
                    - zip
                    - city
                    - country
                    - language
                    - rate
                    - vat
                    - default_position_mode
                invoice:
                  type: object
                  description: ''
                  example: []
                  properties:
                    positions:
                      type: array
                      description: 'value muss mindestens 1 Elemente enthalten.'
                      example:
                        - []
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            description: 'value muss eine gültige UUID sein.'
                            example: 977e5426-8d13-3824-86aa-b092f8ae52c5
                          date:
                            type: string
                            description: 'value ist kein gültiges Datum.'
                            example: '2026-05-18T10:08:43'
                          mode:
                            type: string
                            description: ''
                            example: fixed
                            enum:
                              - hourly
                              - fixed
                          description:
                            type: string
                            description: 'value darf nicht länger als 1000 Zeichen sein.'
                            example: 'Fugiat sunt nihil accusantium harum mollitia.'
                          hours:
                            type: number
                            description: ''
                            example: 4326.41688
                            nullable: true
                          amount_cents:
                            type: integer
                            description: ''
                            example: 16
                            nullable: true
                        required:
                          - id
                          - date
                          - mode
                          - description
                    tax_rate:
                      type: number
                      description: 'value muss mindestens 0 sein. value darf nicht größer als 100 sein.'
                      example: 17
                    billing_date:
                      type: string
                      description: 'value ist kein gültiges Datum.'
                      example: '2026-05-18T10:08:43'
                      nullable: true
                    billing_period:
                      type: string
                      description: 'value muss 7 Zeichen lang sein.'
                      example: ikhwayk
                      nullable: true
                    due_offset_days:
                      type: integer
                      description: 'value muss mindestens 0 sein. value darf nicht größer als 255 sein.'
                      example: 8
                    due_at:
                      type: string
                      description: 'value ist kein gültiges Datum.'
                      example: '2026-05-18T10:08:43'
                      nullable: true
                    subject:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: m
                      nullable: true
                    salutation:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: 'y'
                      nullable: true
                    description:
                      type: string
                      description: ''
                      example: 'Eius et animi quos velit et.'
                      nullable: true
                    note:
                      type: string
                      description: ''
                      example: architecto
                      nullable: true
                    every:
                      type: string
                      description: ''
                      example: monthly
                      enum:
                        - daily
                        - weekly
                        - biweekly
                        - monthly
                        - bimonthly
                        - quarterly
                        - semiannual
                        - yearly
                      nullable: true
                    next_invoice_at:
                      type: string
                      description: 'This field is required when <code>invoice.every</code> is present. value ist kein gültiges Datum.'
                      example: '2026-05-18T10:08:43'
                      nullable: true
                  required:
                    - positions
                    - tax_rate
                    - due_offset_days
                send:
                  type: object
                  description: ''
                  example: null
                  properties:
                    subject:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: b
                      nullable: true
                    body:
                      type: string
                      description: 'value darf nicht länger als 50000 Zeichen sein.'
                      example: 'n'
                      nullable: true
                    cc:
                      type: array
                      description: 'value muss eine gültige E-Mail-Adresse sein. value darf nicht länger als 255 Zeichen sein.'
                      example:
                        - g
                      items:
                        type: string
                metadata:
                  type: object
                  description: ''
                  example: null
                  properties: {  }
                  nullable: true
                return_url:
                  type: string
                  description: 'Must be a valid URL. value darf nicht länger als 2048 Zeichen sein.'
                  example: 'http://bailey.com/'
                  nullable: true
              required:
                - client
                - invoice
  '/api/v1/checkout-intents/{id}/confirm':
    post:
      summary: 'Confirm a checkout intent.'
      operationId: confirmACheckoutIntent
      description: "Public endpoint — authenticated only by the `client_secret` body\nfield. One-shot: a second confirm returns 409. Composes the same\nclient + invoice + send flow as `/v1/checkouts`."
      parameters: []
      responses: {  }
      tags:
        - 'Checkout intents'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                client_secret:
                  type: string
                  description: 'value darf nicht länger als 128 Zeichen sein.'
                  example: b
              required:
                - client_secret
    parameters:
      -
        in: path
        name: id
        description: 'The ID of the checkout intent.'
        example: architecto
        required: true
        schema:
          type: string
  /api/v1/checkouts:
    post:
      summary: 'Create a checkout'
      operationId: createACheckout
      description: "Wraps client find-or-create + invoice creation + invoice email send\ninto one idempotent call."
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: Required.
          schema:
            type: string
      responses: {  }
      tags:
        - Checkouts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                client:
                  type: object
                  description: "Client (customer) details. Looked up\n  by `external_id` within the current team; created if not found,\n  updated in place if found."
                  example: []
                  properties:
                    name:
                      type: string
                      description: ''
                      example: architecto
                    name_addition:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: m
                      nullable: true
                    address:
                      type: string
                      description: ''
                      example: architecto
                    address_addition:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: 'y'
                      nullable: true
                    zip:
                      type: string
                      description: ''
                      example: architecto
                    city:
                      type: string
                      description: ''
                      example: architecto
                    country:
                      type: string
                      description: 'ISO 3166-1 alpha-2.'
                      example: architecto
                    email:
                      type: string
                      description: 'Required to send the invoice email.'
                      example: gbailey@example.net
                      nullable: true
                    email_cc:
                      type: array
                      description: 'value muss eine gültige E-Mail-Adresse sein. value darf nicht länger als 255 Zeichen sein.'
                      example:
                        - s
                      items:
                        type: string
                    email_text:
                      type: string
                      description: ''
                      example: architecto
                      nullable: true
                    language:
                      type: string
                      description: ''
                      example: en
                      enum:
                        - de
                        - en
                    rate:
                      type: integer
                      description: 'Hourly rate in cents (0 if N/A).'
                      example: 16
                    vat:
                      type: number
                      description: ''
                      example: 4326.41688
                    default_position_mode:
                      type: string
                      description: ''
                      example: fixed
                      enum:
                        - hourly
                        - fixed
                    invoice_note:
                      type: string
                      description: ''
                      example: architecto
                      nullable: true
                    iban:
                      type: string
                      description: 'value darf nicht länger als 34 Zeichen sein.'
                      example: 'n'
                      nullable: true
                    bic:
                      type: string
                      description: 'value darf nicht länger als 11 Zeichen sein.'
                      example: gzmiyv
                      nullable: true
                    vat_id:
                      type: string
                      description: 'value darf nicht länger als 64 Zeichen sein.'
                      example: d
                      nullable: true
                    buyer_reference:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: l
                      nullable: true
                    e_invoice_format:
                      type: string
                      description: ''
                      example: xrechnung
                      enum:
                        - none
                        - zugferd
                        - xrechnung
                    external_id:
                      type: string
                      description: "Stable identifier from the shop\n  (e.g. `wc_customer_42`). Strongly recommended — without it every\n  checkout creates a new client."
                      example: architecto
                      nullable: true
                  required:
                    - name
                    - address
                    - zip
                    - city
                    - country
                    - language
                    - rate
                    - vat
                    - default_position_mode
                invoice:
                  type: object
                  description: ''
                  example: []
                  properties:
                    positions:
                      type: array
                      description: 'At least one row.'
                      example:
                        - []
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            description: UUID.
                            example: architecto
                          date:
                            type: date
                            description: ''
                            example: architecto
                          mode:
                            type: string
                            description: ''
                            example: hourly
                            enum:
                              - hourly
                              - fixed
                          description:
                            type: string
                            description: ''
                            example: 'Eius et animi quos velit et.'
                          hours:
                            type: number
                            description: 'Required when mode=hourly.'
                            example: 4326.41688
                            nullable: true
                          amount_cents:
                            type: integer
                            description: 'Required when mode=fixed.'
                            example: 16
                            nullable: true
                        required:
                          - id
                          - date
                          - mode
                          - description
                    tax_rate:
                      type: number
                      description: 'VAT % (0–100).'
                      example: 4326.41688
                    billing_date:
                      type: string
                      description: 'value ist kein gültiges Datum.'
                      example: '2026-05-18T10:08:43'
                      nullable: true
                    billing_period:
                      type: string
                      description: 'value muss 7 Zeichen lang sein.'
                      example: ikhwayk
                      nullable: true
                    due_offset_days:
                      type: integer
                      description: ''
                      example: 16
                    due_at:
                      type: string
                      description: 'value ist kein gültiges Datum.'
                      example: '2026-05-18T10:08:43'
                      nullable: true
                    subject:
                      type: string
                      description: ''
                      example: architecto
                      nullable: true
                    salutation:
                      type: string
                      description: 'value darf nicht länger als 255 Zeichen sein.'
                      example: 'y'
                      nullable: true
                    description:
                      type: string
                      description: ''
                      example: 'Eius et animi quos velit et.'
                      nullable: true
                    note:
                      type: string
                      description: ''
                      example: architecto
                      nullable: true
                    every:
                      type: string
                      description: ''
                      example: quarterly
                      enum:
                        - daily
                        - weekly
                        - biweekly
                        - monthly
                        - bimonthly
                        - quarterly
                        - semiannual
                        - yearly
                      nullable: true
                    next_invoice_at:
                      type: string
                      description: 'This field is required when <code>invoice.every</code> is present. value ist kein gültiges Datum.'
                      example: '2026-05-18T10:08:43'
                      nullable: true
                  required:
                    - positions
                    - tax_rate
                    - due_offset_days
                send:
                  type: object
                  description: ''
                  example: []
                  properties:
                    subject:
                      type: string
                      description: 'Mail subject.'
                      example: architecto
                      nullable: true
                    body:
                      type: string
                      description: 'Mail body (template vars allowed).'
                      example: architecto
                      nullable: true
                    cc:
                      type: array
                      description: 'Optional CC list.'
                      example:
                        - architecto
                      items:
                        type: string
                  required:
                    - subject
                    - body
                metadata:
                  type: object
                  description: "Echoed verbatim on every webhook payload —\n  the right place to round-trip your shop's order ID."
                  example: []
                  properties: {  }
                  nullable: true
              required:
                - client
                - invoice
                - send
  /api/v1/clients:
    get:
      summary: 'List clients'
      operationId: listClients
      description: "Returns the team's clients, cursor-paginated."
      parameters:
        -
          in: query
          name: limit
          description: 'Page size (max 100).'
          example: 25
          required: false
          schema:
            type: integer
            description: 'Page size (max 100).'
            example: 25
        -
          in: query
          name: cursor
          description: 'Opaque cursor returned in `meta.next_cursor`. Example:'
          example: architecto
          required: false
          schema:
            type: string
            description: 'Opaque cursor returned in `meta.next_cursor`. Example:'
            example: architecto
      responses:
        401:
          description: ''
          content:
            application/problem+json:
              schema:
                type: object
                example:
                  type: 'https://accounting.example/problems/unauthenticated'
                  title: Unauthenticated
                  status: 401
                  detail: Unauthenticated.
                properties:
                  type:
                    type: string
                    example: 'https://accounting.example/problems/unauthenticated'
                  title:
                    type: string
                    example: Unauthenticated
                  status:
                    type: integer
                    example: 401
                  detail:
                    type: string
                    example: Unauthenticated.
      tags:
        - Clients
    post:
      summary: 'Create a client'
      operationId: createAClient
      description: ''
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: 'Required. Unique per logical request; replays return the original response with `Idempotency-Replay: true`.'
          schema:
            type: string
      responses:
        201:
          description: created
          content:
            application/json:
              schema:
                type: object
                example:
                  data:
                    id: 0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f3e
                    name: 'Acme GmbH'
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        example: 0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f3e
                      name:
                        type: string
                        example: 'Acme GmbH'
      tags:
        - Clients
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                team_id:
                  type: integer
                  description: 'The <code>id</code> of an existing record in the teams table.'
                  example: 16
                name:
                  type: string
                  description: 'Display name of the client.'
                  example: 'Acme GmbH'
                name_addition:
                  type: string
                  description: 'Second line of the company name.'
                  example: 'Berlin Office'
                  nullable: true
                address:
                  type: string
                  description: 'Street address.'
                  example: 'Musterstraße 1'
                address_addition:
                  type: string
                  description: 'Optional address line 2.'
                  example: architecto
                  nullable: true
                zip:
                  type: string
                  description: 'Postal code.'
                  example: '10115'
                city:
                  type: string
                  description: City.
                  example: Berlin
                country:
                  type: string
                  description: 'ISO 3166-1 alpha-2 country code.'
                  example: DE
                email:
                  type: string
                  description: 'Primary billing email.'
                  example: billing@acme.example
                  nullable: true
                email_cc:
                  type: array
                  description: 'Optional CC list.'
                  example:
                    - accounting@acme.example
                  items:
                    type: string
                email_text:
                  type: string
                  description: 'Default cover text used when emailing invoices to this client.'
                  example: architecto
                  nullable: true
                language:
                  type: string
                  description: 'UI / mail language for this client.'
                  example: de
                  enum:
                    - de
                    - en
                rate:
                  type: integer
                  description: 'Hourly rate in euro cents.'
                  example: 12000
                vat:
                  type: number
                  description: 'VAT percentage (0–100).'
                  example: 19.0
                default_position_mode:
                  type: string
                  description: 'Default mode for new invoice rows.'
                  example: hourly
                  enum:
                    - hourly
                    - fixed
                invoice_note:
                  type: string
                  description: 'Default note appended to invoices for this client.'
                  example: architecto
                  nullable: true
                iban:
                  type: string
                  description: 'SEPA IBAN (max 34 chars).'
                  example: architecto
                  nullable: true
                bic:
                  type: string
                  description: 'SEPA BIC (max 11 chars).'
                  example: architecto
                  nullable: true
                vat_id:
                  type: string
                  description: 'Buyer VAT identification number.'
                  example: architecto
                  nullable: true
                buyer_reference:
                  type: string
                  description: 'Required for XRechnung — usually the Leitweg-ID.'
                  example: 04011000-12345-34
                  nullable: true
                e_invoice_format:
                  type: string
                  description: 'Default e-invoice format.'
                  example: xrechnung
                  enum:
                    - none
                    - zugferd
                    - xrechnung
                external_id:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: j
                  nullable: true
              required:
                - team_id
                - name
                - address
                - zip
                - city
                - country
                - language
                - rate
                - vat
                - default_position_mode
  '/api/v1/clients/{id}':
    get:
      summary: 'Get a client'
      operationId: getAClient
      description: ''
      parameters: []
      responses:
        401:
          description: ''
          content:
            application/problem+json:
              schema:
                type: object
                example:
                  type: 'https://accounting.example/problems/unauthenticated'
                  title: Unauthenticated
                  status: 401
                  detail: Unauthenticated.
                properties:
                  type:
                    type: string
                    example: 'https://accounting.example/problems/unauthenticated'
                  title:
                    type: string
                    example: Unauthenticated
                  status:
                    type: integer
                    example: 401
                  detail:
                    type: string
                    example: Unauthenticated.
      tags:
        - Clients
    patch:
      summary: 'Update a client'
      operationId: updateAClient
      description: 'Same body shape as create. Pass only the fields you want to change.'
      parameters: []
      responses: {  }
      tags:
        - Clients
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                team_id:
                  type: integer
                  description: 'The <code>id</code> of an existing record in the teams table.'
                  example: 16
                name:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: 'n'
                name_addition:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: g
                  nullable: true
                address:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: z
                address_addition:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: m
                  nullable: true
                zip:
                  type: string
                  description: 'value darf nicht länger als 20 Zeichen sein.'
                  example: iyvdljnikhwaykcm
                city:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: 'y'
                country:
                  type: string
                  description: 'value muss 2 Zeichen lang sein.'
                  example: uw
                email:
                  type: string
                  description: 'value muss eine gültige E-Mail-Adresse sein. value darf nicht länger als 255 Zeichen sein.'
                  example: theo.hauck@example.com
                  nullable: true
                email_cc:
                  type: array
                  description: 'value muss eine gültige E-Mail-Adresse sein. value darf nicht länger als 255 Zeichen sein.'
                  example:
                    - w
                  items:
                    type: string
                email_text:
                  type: string
                  description: ''
                  example: architecto
                  nullable: true
                language:
                  type: string
                  description: ''
                  example: en
                  enum:
                    - de
                    - en
                rate:
                  type: integer
                  description: 'value muss mindestens 0 sein.'
                  example: 39
                vat:
                  type: number
                  description: 'value muss mindestens 0 sein. value darf nicht größer als 100 sein.'
                  example: 7
                default_position_mode:
                  type: string
                  description: ''
                  example: fixed
                  enum:
                    - hourly
                    - fixed
                invoice_note:
                  type: string
                  description: ''
                  example: architecto
                  nullable: true
                iban:
                  type: string
                  description: 'value darf nicht länger als 34 Zeichen sein.'
                  example: 'n'
                  nullable: true
                bic:
                  type: string
                  description: 'value darf nicht länger als 11 Zeichen sein.'
                  example: gzmiyv
                  nullable: true
                vat_id:
                  type: string
                  description: 'value darf nicht länger als 64 Zeichen sein.'
                  example: d
                  nullable: true
                buyer_reference:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: l
                  nullable: true
                e_invoice_format:
                  type: string
                  description: ''
                  example: xrechnung
                  enum:
                    - none
                    - zugferd
                    - xrechnung
                external_id:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: j
                  nullable: true
              required:
                - team_id
                - name
                - address
                - zip
                - city
                - country
                - language
                - rate
                - vat
                - default_position_mode
    delete:
      summary: 'Delete a client'
      operationId: deleteAClient
      description: 'Soft-delete. Refused with 409 if the client has any invoice still inside its 10-year legal-retention window.'
      parameters: []
      responses:
        204:
          description: deleted
          content:
            text/plain:
              schema:
                type: string
                example: ''
        409:
          description: 'retained invoices exist'
          content:
            application/json:
              schema:
                type: object
                example:
                  type: 'https://accounting.example/problems/client-has-retained-invoices'
                  title: 'Client has retained invoices'
                  status: 409
                properties:
                  type:
                    type: string
                    example: 'https://accounting.example/problems/client-has-retained-invoices'
                  title:
                    type: string
                    example: 'Client has retained invoices'
                  status:
                    type: integer
                    example: 409
      tags:
        - Clients
    parameters:
      -
        in: path
        name: id
        description: 'The ID of the client.'
        example: 014b17a6-c8e3-45c2-a2ff-78b1449daa26
        required: true
        schema:
          type: string
      -
        in: path
        name: client
        description: 'Client UUID.'
        example: architecto
        required: true
        schema:
          type: string
  /api/v1/invoices:
    get:
      summary: 'List invoices'
      operationId: listInvoices
      description: "Returns the team's invoices, cursor-paginated. Drafts are hidden by default — pass `?include=drafts` (or `?include_drafts=1`) to include them."
      parameters:
        -
          in: query
          name: limit
          description: 'Page size (max 100).'
          example: 25
          required: false
          schema:
            type: integer
            description: 'Page size (max 100).'
            example: 25
        -
          in: query
          name: cursor
          description: 'Opaque cursor returned in `meta.next_cursor`. Example:'
          example: architecto
          required: false
          schema:
            type: string
            description: 'Opaque cursor returned in `meta.next_cursor`. Example:'
            example: architecto
        -
          in: query
          name: include
          description: 'Use `drafts` to include draft invoices.'
          example: drafts
          required: false
          schema:
            type: string
            description: 'Use `drafts` to include draft invoices.'
            example: drafts
      responses:
        401:
          description: ''
          content:
            application/problem+json:
              schema:
                type: object
                example:
                  type: 'https://accounting.example/problems/unauthenticated'
                  title: Unauthenticated
                  status: 401
                  detail: Unauthenticated.
                properties:
                  type:
                    type: string
                    example: 'https://accounting.example/problems/unauthenticated'
                  title:
                    type: string
                    example: Unauthenticated
                  status:
                    type: integer
                    example: 401
                  detail:
                    type: string
                    example: Unauthenticated.
      tags:
        - Invoices
    post:
      summary: 'Create an invoice'
      operationId: createAnInvoice
      description: ''
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: Required.
          schema:
            type: string
      responses: {  }
      tags:
        - Invoices
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                team_id:
                  type: integer
                  description: 'The <code>id</code> of an existing record in the teams table.'
                  example: 16
                client_id:
                  type: string
                  description: 'UUID of the client to bill.'
                  example: architecto
                type:
                  type: string
                  description: 'Document type.'
                  example: invoice
                  enum:
                    - invoice
                    - offer
                tax_rate:
                  type: number
                  description: 'VAT percentage (0–100).'
                  example: 19.0
                billing_date:
                  type: date
                  description: 'Billing date (`YYYY-MM-DD`). Defaults to today server-side.'
                  example: '2026-05-07'
                  nullable: true
                billing_period:
                  type: string
                  description: 'value muss 7 Zeichen lang sein.'
                  example: miyvdlj
                  nullable: true
                due_offset_days:
                  type: integer
                  description: 'Days from `billing_date` to `due_at`.'
                  example: 14
                due_at:
                  type: string
                  description: 'value ist kein gültiges Datum.'
                  example: '2026-05-18T10:08:43'
                  nullable: true
                paid_at:
                  type: string
                  description: 'value ist kein gültiges Datum.'
                  example: '2026-05-18T10:08:43'
                  nullable: true
                every:
                  type: string
                  description: 'Recurrence interval. Forces `type=invoice`.'
                  example: daily
                  enum:
                    - daily
                    - weekly
                    - biweekly
                    - monthly
                    - bimonthly
                    - quarterly
                    - semiannual
                    - 'yearly. Example:'
                  nullable: true
                next_invoice_at:
                  type: date
                  description: 'Required when `every` is set. Example:'
                  example: architecto
                  nullable: true
                is_draft:
                  type: boolean
                  description: 'Save as draft (no number issued yet). Cannot be combined with `every`.'
                  example: false
                subject:
                  type: string
                  description: 'Optional subject line.'
                  example: architecto
                  nullable: true
                salutation:
                  type: string
                  description: 'Optional salutation.'
                  example: architecto
                  nullable: true
                description:
                  type: string
                  description: 'Long-form description rendered above the position table.'
                  example: 'Eius et animi quos velit et.'
                  nullable: true
                note:
                  type: string
                  description: 'Footer note rendered below the totals.'
                  example: architecto
                  nullable: true
                metadata:
                  type: object
                  description: ''
                  example: null
                  properties: {  }
                  nullable: true
                positions:
                  type: array
                  description: 'At least one row.'
                  example:
                    - []
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        description: 'UUID identifying the row.'
                        example: architecto
                      date:
                        type: date
                        description: 'Position date.'
                        example: architecto
                      mode:
                        type: string
                        description: 'Pricing mode.'
                        example: hourly
                        enum:
                          - hourly
                          - fixed
                      description:
                        type: string
                        description: 'Description rendered on the PDF.'
                        example: 'Eius et animi quos velit et.'
                      hours:
                        type: number
                        description: 'Required when `mode=hourly`; prohibited when `mode=fixed`.'
                        example: 4326.41688
                        nullable: true
                      amount_cents:
                        type: integer
                        description: 'Required when `mode=fixed`; prohibited when `mode=hourly`.'
                        example: 16
                        nullable: true
                    required:
                      - id
                      - date
                      - mode
                      - description
              required:
                - team_id
                - client_id
                - type
                - tax_rate
                - positions
  '/api/v1/invoices/{id}':
    get:
      summary: 'Get an invoice'
      operationId: getAnInvoice
      description: ''
      parameters: []
      responses:
        401:
          description: ''
          content:
            application/problem+json:
              schema:
                type: object
                example:
                  type: 'https://accounting.example/problems/unauthenticated'
                  title: Unauthenticated
                  status: 401
                  detail: Unauthenticated.
                properties:
                  type:
                    type: string
                    example: 'https://accounting.example/problems/unauthenticated'
                  title:
                    type: string
                    example: Unauthenticated
                  status:
                    type: integer
                    example: 401
                  detail:
                    type: string
                    example: Unauthenticated.
      tags:
        - Invoices
    patch:
      summary: 'Update an invoice'
      operationId: updateAnInvoice
      description: 'Same body shape as create. A draft invoice gets its gapless number assigned the moment `is_draft` flips to `false`.'
      parameters: []
      responses: {  }
      tags:
        - Invoices
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                team_id:
                  type: integer
                  description: 'The <code>id</code> of an existing record in the teams table.'
                  example: 16
                client_id:
                  type: string
                  description: 'value muss eine gültige UUID sein. The <code>id</code> of an existing record in the clients table.'
                  example: a4855dc5-0acb-33c3-b921-f4291f719ca0
                type:
                  type: string
                  description: ''
                  example: invoice
                  enum:
                    - invoice
                    - offer
                tax_rate:
                  type: number
                  description: 'value muss mindestens 0 sein. value darf nicht größer als 100 sein.'
                  example: 16
                billing_date:
                  type: string
                  description: 'value ist kein gültiges Datum.'
                  example: '2026-05-18T10:08:43'
                  nullable: true
                billing_period:
                  type: string
                  description: 'value muss 7 Zeichen lang sein.'
                  example: miyvdlj
                  nullable: true
                due_offset_days:
                  type: integer
                  description: 'value muss mindestens 0 sein. value darf nicht größer als 255 sein.'
                  example: 17
                due_at:
                  type: string
                  description: 'value ist kein gültiges Datum.'
                  example: '2026-05-18T10:08:43'
                  nullable: true
                paid_at:
                  type: string
                  description: 'value ist kein gültiges Datum.'
                  example: '2026-05-18T10:08:43'
                  nullable: true
                every:
                  type: string
                  description: ''
                  example: monthly
                  enum:
                    - daily
                    - weekly
                    - biweekly
                    - monthly
                    - bimonthly
                    - quarterly
                    - semiannual
                    - yearly
                  nullable: true
                next_invoice_at:
                  type: string
                  description: 'This field is required when <code>every</code> is present. value ist kein gültiges Datum. value muss ein Datum nach oder gleich dem <code>billing_date</code> sein.'
                  example: '2052-06-10'
                  nullable: true
                is_draft:
                  type: boolean
                  description: ''
                  example: true
                subject:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: 'n'
                  nullable: true
                salutation:
                  type: string
                  description: 'value darf nicht länger als 255 Zeichen sein.'
                  example: g
                  nullable: true
                description:
                  type: string
                  description: ''
                  example: 'Eius et animi quos velit et.'
                  nullable: true
                note:
                  type: string
                  description: ''
                  example: architecto
                  nullable: true
                metadata:
                  type: object
                  description: ''
                  example: null
                  properties: {  }
                  nullable: true
                positions:
                  type: array
                  description: 'value muss mindestens 1 Elemente enthalten.'
                  example:
                    - []
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        description: 'value muss eine gültige UUID sein.'
                        example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed
                      date:
                        type: string
                        description: 'value ist kein gültiges Datum.'
                        example: '2026-05-18T10:08:43'
                      mode:
                        type: string
                        description: ''
                        example: hourly
                        enum:
                          - hourly
                          - fixed
                      description:
                        type: string
                        description: 'value darf nicht länger als 1000 Zeichen sein.'
                        example: 'Animi quos velit et fugiat.'
                      hours:
                        type: number
                        description: ''
                        example: 4326.41688
                        nullable: true
                      amount_cents:
                        type: integer
                        description: ''
                        example: 16
                        nullable: true
                    required:
                      - id
                      - date
                      - mode
                      - description
              required:
                - team_id
                - client_id
                - type
                - tax_rate
                - due_offset_days
                - positions
    delete:
      summary: 'Delete an invoice'
      operationId: deleteAnInvoice
      description: "Soft-delete, restricted to true drafts. Finalised / sent invoices must\nbe reversed via Stornorechnung — `POST /v1/invoices/{id}/cancel`.\nRecurring templates must be stopped via\n`POST /v1/invoices/{id}/stop-recurring`."
      parameters: []
      responses:
        204:
          description: deleted
          content:
            text/plain:
              schema:
                type: string
                example: ''
        422:
          description: ''
          content:
            text/plain:
              schema:
                oneOf:
                  -
                    description: 'finalised invoice — use /cancel'
                    type: string
                    example: ''
                  -
                    description: 'recurring template — use /stop-recurring'
                    type: string
                    example: ''
      tags:
        - Invoices
    parameters:
      -
        in: path
        name: id
        description: 'The ID of the invoice.'
        example: 019e167c-cbf7-73fe-94e0-c99b6907e435
        required: true
        schema:
          type: string
      -
        in: path
        name: invoice
        description: 'Invoice UUID.'
        example: architecto
        required: true
        schema:
          type: string
  '/api/v1/invoices/{invoice_id}/pdf':
    get:
      summary: 'Download the invoice PDF'
      operationId: downloadTheInvoicePDF
      description: "Returns the rendered PDF as `application/pdf`. The response carries an `ETag` keyed on `(invoice.id, invoice.updated_at)`. Send `If-None-Match: <etag>` to get a 304 when the PDF hasn't changed. The PDF is regenerated on cache miss and stored on the `pdf-cache` disk for 7 days."
      parameters: []
      responses:
        200:
          description: 'pdf bytes'
          content:
            text/plain:
              schema:
                type: string
                example: ''
        304:
          description: 'not modified'
          content:
            text/plain:
              schema:
                type: string
                example: ''
      tags:
        - Invoices
    parameters:
      -
        in: path
        name: invoice_id
        description: 'The ID of the invoice.'
        example: 019e167c-cbf7-73fe-94e0-c99b6907e435
        required: true
        schema:
          type: string
      -
        in: path
        name: invoice
        description: 'Invoice UUID.'
        example: architecto
        required: true
        schema:
          type: string
  '/api/v1/invoices/{invoice_id}/send':
    post:
      summary: 'Send an invoice by email'
      operationId: sendAnInvoiceByEmail
      description: "Queues a `SendInvoiceMail` job. The mail is rendered in the **client's** language (`clients.language`), not the caller's. Returns `202 Accepted` immediately with a `mail_event_id` that can be correlated with the `mail_events` table once the worker has run."
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: Required.
          schema:
            type: string
      responses:
        202:
          description: queued
          content:
            application/json:
              schema:
                type: object
                example:
                  mail_event_id: 0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f3e
                  status: queued
                properties:
                  mail_event_id:
                    type: string
                    example: 0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f3e
                  status:
                    type: string
                    example: queued
      tags:
        - Invoices
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                cc:
                  type: array
                  description: 'Optional CC recipients.'
                  example:
                    - architecto
                  items:
                    type: string
                subject:
                  type: string
                  description: 'Mail subject.'
                  example: 'Your invoice INV-2026-0042'
                body:
                  type: string
                  description: 'Mail body (plain text).'
                  example: 'Please find attached…'
                attachments:
                  type: array
                  description: 'Optional extra attachments. Max 25 MB each. Allowed types: pdf, png, jpg, jpeg, doc, docx, xlsx.'
                  items:
                    type: string
                    format: binary
              required:
                - subject
                - body
    parameters:
      -
        in: path
        name: invoice_id
        description: 'The ID of the invoice.'
        example: 019e167c-cbf7-73fe-94e0-c99b6907e435
        required: true
        schema:
          type: string
      -
        in: path
        name: invoice
        description: 'Invoice UUID.'
        example: architecto
        required: true
        schema:
          type: string
  '/api/v1/invoices/{invoice_id}/mark-paid':
    post:
      summary: 'Mark an invoice as paid'
      operationId: markAnInvoiceAsPaid
      description: ''
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: Required.
          schema:
            type: string
      responses: {  }
      tags:
        - Invoices
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                paid_at:
                  type: date
                  description: 'Optional payment date. Defaults to today.'
                  example: '2026-05-07'
                  nullable: true
    parameters:
      -
        in: path
        name: invoice_id
        description: 'The ID of the invoice.'
        example: 019e167c-cbf7-73fe-94e0-c99b6907e435
        required: true
        schema:
          type: string
      -
        in: path
        name: invoice
        description: 'Invoice UUID.'
        example: architecto
        required: true
        schema:
          type: string
  '/api/v1/invoices/{invoice_id}/stop-recurring':
    post:
      summary: 'Stop a recurring invoice template'
      operationId: stopARecurringInvoiceTemplate
      description: "Sets `every` to `null` so the scheduler stops generating further\nchildren, and stamps `recurring_stopped_at`. The template stays visible\nwith its full history. Idempotent: a second call returns the same\nresource."
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: Required.
          schema:
            type: string
      responses: {  }
      tags:
        - Invoices
    parameters:
      -
        in: path
        name: invoice_id
        description: 'The ID of the invoice.'
        example: 019e167c-cbf7-73fe-94e0-c99b6907e435
        required: true
        schema:
          type: string
      -
        in: path
        name: invoice
        description: 'Invoice UUID.'
        example: architecto
        required: true
        schema:
          type: string
  '/api/v1/invoices/{invoice_id}/change-cycle':
    post:
      summary: 'Change the cadence of a recurring invoice template'
      operationId: changeTheCadenceOfARecurringInvoiceTemplate
      description: "Mutates `every` (required) and `next_invoice_at` (optional — derived\nfrom current `next_invoice_at` + new cadence when omitted). Rejected\nwith 422 if `every` is currently `null` (the template was stopped —\nstart a new one instead). Idempotent: a second call with the same\npayload returns the same resource."
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: Required.
          schema:
            type: string
      responses: {  }
      tags:
        - Invoices
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                every:
                  type: string
                  description: 'New recurrence interval.'
                  example: yearly
                  enum:
                    - daily
                    - weekly
                    - biweekly
                    - monthly
                    - bimonthly
                    - quarterly
                    - semiannual
                    - yearly
                next_invoice_at:
                  type: date
                  description: 'Optional override for the next run. Defaults to `next_invoice_at` advanced by the new cadence.'
                  example: '2026-06-15'
                  nullable: true
              required:
                - every
    parameters:
      -
        in: path
        name: invoice_id
        description: 'The ID of the invoice.'
        example: 019e167c-cbf7-73fe-94e0-c99b6907e435
        required: true
        schema:
          type: string
      -
        in: path
        name: invoice
        description: 'Invoice UUID.'
        example: architecto
        required: true
        schema:
          type: string
  '/api/v1/invoices/{invoice_id}/cancel':
    post:
      summary: 'Cancel an invoice via Stornorechnung'
      operationId: cancelAnInvoiceViaStornorechnung
      description: "Issues a Stornorechnung (negative-amount reversing invoice) that\nlegally cancels the original under UStG §14c / GoBD. Returns the\nnew Stornorechnung resource (not the original). Idempotent via the\n`Idempotency-Key` header: a second call with the same key replays\nthe same response. Rejected with 422 if the invoice is not in a\ncancellable state (draft, offer, already a Storno, already cancelled)."
      parameters:
        -
          in: header
          name: Idempotency-Key
          description: ''
          example: Required.
          schema:
            type: string
      responses: {  }
      tags:
        - Invoices
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: "Optional free-text cancellation reason, stored on the Stornorechnung's metadata."
                  example: 'customer downgraded'
    parameters:
      -
        in: path
        name: invoice_id
        description: 'The ID of the invoice.'
        example: 019e167c-cbf7-73fe-94e0-c99b6907e435
        required: true
        schema:
          type: string
      -
        in: path
        name: invoice
        description: 'Invoice UUID.'
        example: architecto
        required: true
        schema:
          type: string
  /api/v1/recurring:
    get:
      summary: '30-day recurring forecast'
      operationId: 30DayRecurringForecast
      description: 'Returns recurring invoices whose `next_invoice_at` falls within the next 30 days, ordered by date.'
      parameters: []
      responses:
        401:
          description: ''
          content:
            application/problem+json:
              schema:
                type: object
                example:
                  type: 'https://accounting.example/problems/unauthenticated'
                  title: Unauthenticated
                  status: 401
                  detail: Unauthenticated.
                properties:
                  type:
                    type: string
                    example: 'https://accounting.example/problems/unauthenticated'
                  title:
                    type: string
                    example: Unauthenticated
                  status:
                    type: integer
                    example: 401
                  detail:
                    type: string
                    example: Unauthenticated.
      tags:
        - Recurring
