openapi: 3.1.0
info:
  title: Public API
  version: 1.0.0
  description: |
    Official API documentation for integrations.
    This API allows you to interact with the platform programmatically.

    ## Getting Started

    New to the API? Check out our [Getting Started Guide](https://app.alunta.com/docs/getting-started) for authentication, examples, and best practices.

    ## Webhook delivery and retries

    Webhooks are dispatched asynchronously after the relevant event is committed. Your endpoint should respond with a `2xx` status code within **3 seconds** to be considered successful.

    Failed deliveries are attempted up to **8 times** in total (the first attempt plus 7 retries) with the following schedule between attempts (cumulative window 44 hours and 36 minutes):

    | Attempt | Delay before retry |
    | ------- | ------------------ |
    | 2       | 1 minute           |
    | 3       | 5 minutes          |
    | 4       | 30 minutes         |
    | 5       | 2 hours            |
    | 6       | 6 hours            |
    | 7       | 12 hours           |
    | 8       | 24 hours           |

    Any non-2xx response, request timeout, or connection error counts as a failure and triggers the next retry. After the final attempt the delivery is marked failed - you can replay it using `POST /webhooks/replay/{uuid}`.

    Webhook delivery is **at least once**, so handlers must be idempotent. `invoice.paid`, `subscription.payment_succeeded`, `subscription.started`, and `subscription.ended` include a stable UUID in `data.event_id`; use that value as the deduplication key when the same local transition is delivered again.

    Every payload includes `team_uuid`, `event`, `data`, `timestamp` and `test_mode` at the root. Resource identifiers in `data` (customer, invoice, plan, subscription, refund, ...) are always UUIDs - we never expose internal database ids.

    ## Team payment state

    When a free Team exceeds its included usage and does not choose a paid Alunta plan within 14 days, management access is restricted. Authenticated API operations then return `402 Payment Required` with the exact `account_closes_at` timestamp. New operations close one calendar month later. Existing data and end-customer subscriptions are retained.
servers:
  - url: https://app.alunta.com/api/v1
    description: Development server
paths:
  /ping:
    get:
      summary: Ping endpoint
      description: A simple health check endpoint that returns a pong message
      operationId: ping
      tags:
        - Health
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PingResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /me:
    get:
      summary: Get authenticated team context
      description: |
        Returns the team UUID, team name, granted OAuth scopes, base currency and timezone for the access token.
        Use this immediately after the OAuth handshake to discover which team the token is scoped to.
        Do not derive the team from the JWT `sub` claim - `sub` is the user id, not the team id.
      operationId: getMe
      tags:
        - Health
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Authenticated team context
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /team:
    get:
      summary: Get the authenticated team
      description: Retrieve the full profile of the team the access token is scoped to, including contact and address details.
      operationId: getTeam
      tags:
        - Team
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Team profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Team'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
    put:
      summary: Update the authenticated team
      description: |
        Update the profile of the team the access token is scoped to. Only the fields provided in the request body will be updated (partial update).

        `vat_number` is the team's VAT/CVR registration number.
      operationId: updateTeam
      tags:
        - Team
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTeamRequest'
            examples:
              updateContact:
                summary: Update contact details
                value:
                  email: "billing@acme.dk"
                  phone: "+4512345678"
              updateAddress:
                summary: Update address
                value:
                  address: "Strandvejen 1"
                  zip: "2900"
                  city: "Hellerup"
              updateAll:
                summary: Update all supported fields
                value:
                  name: "Acme ApS"
                  email: "billing@acme.dk"
                  phone: "+4512345678"
                  vat_number: "DK12345678"
                  address: "Strandvejen 1"
                  zip: "2900"
                  city: "Hellerup"
                  invoice_due_days: 14
              updateInvoiceDueDays:
                summary: Update payment due-date setting
                value:
                  invoice_due_days: 30
      responses:
        '200':
          description: Team updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Team'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /checkout-sessions:
    post:
      summary: Create checkout session
      description: |
        Create a new checkout session that generates a unique URL for customers to complete their purchase.

        Checkout sessions support two types via the `type` field (default: `subscription`):

        - **`subscription`** (default): The customer is signing up for a recurring subscription. Requires `plan_id` and `external_customer_id`.
        - **`one_off_invoice`**: Generates a payment link for a one-off invoice. The invoice is only created and sent once the end customer commits — either by paying online (card gateway) or by choosing to receive an invoice (bank transfer). Until that point no invoice number is consumed and no accounting sync is triggered, so abandoned flows leave nothing behind. Requires `metadata.lines` and `metadata.currency`; `plan_id` must not be supplied.

        Checkout sessions allow you to:
        - Pre-select a specific plan for the customer (subscription only)
        - Pre-select a preferred renewal interval via metadata (e.g., monthly, quarterly, annual)
        - Pre-select a quantity for license-based plans; the customer can still change it in checkout
        - Pre-fill customer information via metadata
        - Attach custom metadata that is forwarded to the resulting subscription
        - Set custom redirect URLs (back_url and success_url)
        - Track external customer IDs for integration with your platform
        - Send a one-off invoice payment link to a customer before the invoice itself is issued (one_off_invoice only)

        The generated checkout URL expires after 24 hours. Once a customer completes checkout, the session status is updated to "completed" and the `external_customer_id` is included in the `checkout.completed` webhook payload.

        ## One-off invoice sessions

        When `type` is `one_off_invoice`, the session represents a payment link for an invoice that has not yet been created. The end customer chooses how to settle the invoice on the public checkout page — every payment provider that is enabled on the team is offered, including bank transfer (`invoice`) when no card gateway is connected.

        Only when the customer commits does the system:

        1. Create (or find) the end customer
        2. Create the invoice and its lines
        3. Register the payment (card gateway only — bank transfer leaves the invoice unpaid until reconciled)

        This ensures abandoned payment links never consume an invoice number or trigger accounting synchronisation.

        ### Card gateway vs bank transfer

        - **Card gateway** (OnPay, Stripe, QuickPay): customer pays online; both `invoice.created` and `invoice.paid` fire on completion.
        - **Bank transfer** (`invoice` provider): the customer confirms they want to receive an invoice; `invoice.created` fires immediately, and `invoice.paid` fires later once the payment is reconciled (manual, file import or accounting sync).

        ### Restricting which payment providers are offered

        By default the public checkout page surfaces every payment provider enabled on the team. Pass `payment_providers` (array of provider codes) on the request to restrict the offered set per session — useful when an integrator wants a particular link to be card-only, bank-transfer-only, or any explicit subset. When the field is omitted, all team-enabled providers are offered.

        For `one_off_invoice` sessions, `metadata` must include:
        - `lines` (array, required, min 1): Invoice line items. Each line requires `text`, `amount`, and `price`.
          - `text` (string): Line description.
          - `amount` (integer, >= 1): Quantity.
          - `price` (integer): Unit price in minor units (e.g. øre/cents) excluding VAT.
          - `accounting_product_number` (string, optional): External product identifier from your accounting integration. For Dinero this is the `ProductGuid` (UUID) from `GET https://api.dinero.dk/v1/{orgId}/products`; for e-conomic the `productNumber`; for Billy the `id`. e-conomic products must be active, not barred. Not a chart-of-accounts number - to control which income account a line books on, set the sales account directly on the product in the accounting system.
        - `currency` (string, required): 3-letter ISO currency code. Must be one of the team's supported currencies.
        - `note` (string, optional): Note appended to the invoice (max 2000 characters).

        `plan_id` must not be supplied for `one_off_invoice` sessions. `external_customer_id` is optional; when provided it is used to find-or-create the end customer at payment time.

        ## Pre-filling Form Fields

        You can pre-fill checkout form fields and configure checkout behavior by including them in the `metadata` object:
        - `preferred_interval` (integer): Preferred renewal interval (in months) to auto-select. Common values are 1 (Monthly), 3 (Quarterly), 6 (Semi-annually), 12 (Annually). Must be a valid interval for the selected plan.
        - `usage_parameters` (object): For tiered plans only. A map of `parameter_key` => non-negative integer value used to place the new subscription at the correct initial tier when the customer completes checkout. Without this map the subscription always starts at the lowest tier; with it the subscription lands directly on the tier whose conditions match the supplied values, and the values are persisted as the subscription's first tier-parameter readings. Unknown keys, negative values, and non-integer values are silently dropped. The values are consumed at checkout time and not echoed back on the resulting subscription's `metadata` field — read them back via the tier-parameter values on the subscription instead. Example: `{"seats": 25, "api_calls": 10000}`.
        - `starting_tier_uuid` (UUID): For single tiered-plan subscriptions only. Sets the lowest tier the new subscription may start on. Real `usage_parameters` can place it on a higher tier, but never below this tier. The tier must belong to `plan_id`. This value is consumed by checkout and is not forwarded as custom subscription metadata.
        - `is_company` (boolean or string): Set customer type ("company" or "individual")
        - `name`: Company or person name
        - `email`: Email address
        - `phone`: Phone number
        - `country`: 2-letter country code (e.g., "dk", "us")
        - `address`: Street address
        - `zip`: Postal code
        - `city`: City name
        - `reg_number`: VAT/registration number

        ## Custom Trial Periods

        You can configure a custom trial period for this specific checkout session using these metadata fields:
        - `trial_enabled` (boolean): Set to `true` to enable a trial period, or `false` to explicitly disable trials (even if the plan has a default trial)
        - `trial_type` (string): Type of trial duration - either "days" or "months"
        - `trial_value` (integer): Duration of the trial (1-365 for days, 1-12 for months)

        **Trial Priority:** Checkout session trial settings take priority over the plan's default trial configuration. This allows you to:
        - Add a custom trial to a plan that has no default trial
        - Override the default trial duration for specific customers
        - Disable trials for specific checkout sessions even when the plan has a default trial

        ## Intro offer

        If the plan has a default intro offer, a `subscription` checkout session inherits it automatically (there is no metadata field to control it). The first chargeable period is discounted, so the customer's first payment is collected at the reduced intro price (not the normal price), then the normal price applies after the intro period. When the checkout also has a trial, nothing is collected upfront and the intro applies on the first chargeable period after the trial. In the rare case where the discount rounds the first charge to zero, the session stores the card without an upfront charge. The intro never applies to usage-based plans.

        ## Custom Metadata

        Any additional keys in the `metadata` object that are not reserved are echoed back at the root of the `checkout.completed` webhook so you can correlate the completed checkout with state in your own system. This applies to both `subscription` and `one_off_invoice` sessions.

        Reserved keys (filtered out before forwarding) are:
        - Customer pre-fill: `is_company`, `name`, `email`, `phone`, `country`, `address`, `zip`, `city`, `reg_number`
        - Trial configuration: `trial_enabled`, `trial_type`, `trial_value`
        - Subscription only: `preferred_interval`, `usage_parameters`, `starting_tier_uuid`
        - One-off invoice only: `currency`, `lines`, `note`, `heading`, `charge_vat`

        For `subscription` sessions, custom metadata is also stored on the resulting subscription's `metadata` field and can be read back via the subscriptions API. Example use cases:
        - `booking_intent_id`: Link a paid one-off invoice back to a pending booking in your system
        - `founding_tier`: Track which tier a customer signed up for
        - `billing_cycle`: Record the intended billing cycle
        - `referral_code`: Track referral sources
        - `campaign_id`: Link subscriptions to marketing campaigns

        Metadata fields take priority over URL query parameters.
      operationId: createCheckoutSession
      tags:
        - Checkout
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCheckoutSessionRequest'
            examples:
              minimal:
                summary: Minimal request
                value:
                  external_customer_id: "customer_12345"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
              withPrefilledData:
                summary: Request with pre-filled customer data
                description: Example showing how to pre-fill checkout form fields using metadata for a company
                value:
                  external_customer_id: "customer_12345"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  quantity: 25
                  back_url: "https://your-app.com/checkout/back"
                  success_url: "https://your-app.com/checkout/success"
                  metadata:
                    preferred_interval: 12
                    is_company: true
                    name: "Acme Corporation"
                    reg_number: "12345678"
                    email: "contact@acme.com"
                    phone: "12345678"
                    country: "dk"
                    address: "Main Street 123"
                    zip: "2100"
                    city: "Copenhagen"
              withPrefilledIndividualData:
                summary: Request with pre-filled individual data
                description: Example showing how to pre-fill checkout form fields for an individual customer
                value:
                  external_customer_id: "customer_67890"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  metadata:
                    is_company: false
                    name: "John Doe"
                    email: "john.doe@example.com"
                    phone: "98765432"
                    country: "dk"
                    address: "Park Avenue 456"
                    zip: "2200"
                    city: "Aarhus"
              withCustomTrial:
                summary: Request with custom trial period
                description: Example showing how to configure a custom 30-day trial for this checkout session
                value:
                  external_customer_id: "customer_12345"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  metadata:
                    name: "Trial Customer"
                    email: "trial@example.com"
                    country: "dk"
                    trial_enabled: true
                    trial_type: "days"
                    trial_value: 30
              withSubscriptionMetadata:
                summary: Request with custom subscription metadata
                description: Example showing how to attach custom metadata that will be stored on the resulting subscription
                value:
                  external_customer_id: "customer_12345"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  metadata:
                    name: "Acme Corporation"
                    email: "contact@acme.com"
                    country: "dk"
                    founding_tier: "gold"
                    billing_cycle: "annual"
                    referral_code: "PARTNER_ABC"
              withTieredUsageParameters:
                summary: Request with pre-seeded usage data for a tiered plan
                description: |
                  Example showing how to pre-seed usage parameter values so a tiered subscription is placed at the correct initial tier on signup. Without `usage_parameters` the subscription would start at the plan's lowest tier and only be promoted later when usage is reported.
                value:
                  external_customer_id: "customer_12345"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  metadata:
                    name: "Acme Corporation"
                    email: "contact@acme.com"
                    country: "dk"
                    usage_parameters:
                      seats: 25
                      api_calls: 10000
              withStartingTier:
                summary: Tiered subscription with a minimum starting tier
                description: The subscription starts on this tier or a higher tier when the supplied real usage requires it.
                value:
                  external_customer_id: "customer_12345"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  metadata:
                    starting_tier_uuid: "7f9f8a5a-784f-4fe8-8f25-5a673c9550f3"
                    usage_parameters:
                      customers: 4
                      mrr: 12000
              withDisabledTrial:
                summary: Request with trial disabled
                description: Example showing how to disable the trial even if the plan has a default trial
                value:
                  external_customer_id: "customer_12345"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  metadata:
                    trial_enabled: false
              complete:
                summary: Complete request with all fields
                value:
                  external_customer_id: "customer_12345"
                  plan_id: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  quantity: 25
                  back_url: "https://your-app.com/checkout/back"
                  success_url: "https://your-app.com/checkout/success"
                  metadata:
                    preferred_interval: 12
                    is_company: true
                    name: "Acme Corporation"
                    reg_number: "12345678"
                    email: "contact@acme.com"
                    phone: "12345678"
                    country: "dk"
                    address: "Main Street 123"
                    zip: "2100"
                    city: "Copenhagen"
                    trial_enabled: true
                    trial_type: "days"
                    trial_value: 14
              oneOffInvoiceMinimal:
                summary: Minimal one-off invoice payment link
                description: Generate a payment link for a one-off invoice with a single line. The invoice is only created if the customer pays.
                value:
                  type: "one_off_invoice"
                  metadata:
                    currency: "DKK"
                    lines:
                      - text: "Consulting services"
                        amount: 1
                        price: 150000
              oneOffInvoiceWithPrefilledCustomer:
                summary: One-off invoice with pre-filled customer data
                description: Generate a payment link for a specific existing end customer with multiple lines and a note.
                value:
                  type: "one_off_invoice"
                  external_customer_id: "customer_12345"
                  metadata:
                    currency: "DKK"
                    is_company: true
                    name: "Acme Corporation"
                    reg_number: "12345678"
                    email: "billing@acme.com"
                    country: "dk"
                    address: "Main Street 123"
                    zip: "2100"
                    city: "Copenhagen"
                    note: "Thanks for your business!"
                    lines:
                      - text: "Onboarding"
                        amount: 1
                        price: 500000
                        accounting_product_number: "a1b2c3d4-5678-90ab-cdef-1234567890ab"
                      - text: "Monthly retainer"
                        amount: 3
                        price: 200000
                        accounting_product_number: "f9e8d7c6-1234-5678-90ab-cdef12345678"
              oneOffInvoiceBankTransferOnly:
                summary: One-off invoice restricted to bank transfer
                description: Force the public checkout to offer only bank transfer (no card gateway), regardless of which gateways the team has connected.
                value:
                  type: "one_off_invoice"
                  payment_providers: ["invoice"]
                  metadata:
                    currency: "DKK"
                    lines:
                      - text: "Festival booking"
                        amount: 1
                        price: 75000
      responses:
        '201':
          description: Checkout session created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CheckoutSession'
              example:
                data:
                  id: "550e8400-e29b-41d4-a716-446655440000"
                  checkout_url: "https://app.alunta.com/checkout/550e8400-e29b-41d4-a716-446655440000"
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Plan not found.
  /subscriptions:
    get:
      summary: List subscriptions
      description: |
        Retrieve a paginated list of all subscriptions (memberships) for the authenticated user's team.

        Results are ordered by creation date (newest first) and include subscription details such as pricing, interval, dates, status, plan information, and customer information.

        **Understanding Subscription Attributes:**

        - **Pricing:** The `standard_price` is charged once per `interval` period. For example, a subscription with `interval: 12` (annual) and `standard_price: 120000` means the customer pays 120000 currency units once per year.

        - **Status:** The `status` field is computed automatically based on `start_date` and `end_date`. A set `end_date` takes precedence over a future `start_date`:
          - `pending`: Subscription hasn't started yet (`start_date` is in the future and no `end_date` is set)
          - `active`: Subscription is currently active and billing (`start_date` has passed, no `end_date`)
          - `under_cancellation`: Subscription is cancelled but not ended yet (`end_date` is today or in the future)
          - `cancelled`: Subscription has ended (`end_date` is before today)

        - **Dates:** Both `start_date` and `end_date` are returned as ISO 8601 date-time strings. The `start_date` determines when billing begins, and `end_date` (if set) determines when the subscription will stop.
      operationId: listSubscriptions
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          description: "Number of subscriptions to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
          example: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: status
          in: query
          description: |
            Filter subscriptions by status. Only subscriptions matching the specified status will be returned.

            **Valid values:**
            - `pending` - Subscriptions that haven't started yet (`start_date` is in the future and no `end_date` is set)
            - `active` - Currently active subscriptions (`start_date` has passed, no `end_date`)
            - `under_cancellation` - Cancelled subscriptions that have not ended yet (`end_date` is today or in the future)
            - `cancelled` - Subscriptions that have ended (`end_date` is before today)

            If not provided, all subscriptions are returned regardless of status.
          required: false
          schema:
            type: string
            enum: [pending, active, under_cancellation, cancelled]
          example: "active"
        - name: customer_uuid
          in: query
          description: |
            Filter subscriptions by customer UUID. Only subscriptions belonging to the specified customer will be returned.

            Returns 422 if the customer UUID does not exist.
          required: false
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
        - name: external_customer_id
          in: query
          description: |
            Filter subscriptions by external customer ID. Only subscriptions matching the specified `external_customer_id` will be returned.

            This is the same identifier you provided when creating the checkout session. Use this to find all subscriptions belonging to a specific customer in your system without paginating through all results.
          required: false
          schema:
            type: string
          example: "customer_12345"
        - name: external_subscription_id
          in: query
          description: |
            Filter subscriptions by external subscription ID. Only subscriptions matching the specified `external_subscription_id` will be returned.

            This is the identifier you provided when creating the subscription via the API. Use this to look up a specific subscription by your own system's identifier.
          required: false
          schema:
            type: string
          example: "sub_abc123"
        - name: plan_uuid
          in: query
          description: |
            Filter subscriptions by plan UUID. Only subscriptions belonging to the specified plan will be returned.
          required: false
          schema:
            type: string
            format: uuid
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
        - name: include_bundle_children
          in: query
          description: |
            Include subscriptions that were originally purchased via a bundle. Defaults to `false` for backwards compatibility; bundle children are otherwise available through the subscription bundle purchase endpoints.
          required: false
          schema:
            type: boolean
          example: true
      responses:
        '200':
          description: Successful response with paginated subscriptions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSubscriptionsResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error - Invalid request parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                message: "The given data was invalid."
                errors:
                  status: ["The selected status is invalid."]
                  per_page: ["The per page must be between 1 and 100."]
    post:
      summary: Create a subscription
      description: |
        Create a new subscription linking a customer to a plan.

        The subscription uses the pricing, interval, and currency from the renewal interval you reference via `renewal_interval_uuid`. The start date defaults to today if not provided.

        For a `license_based` plan, you may override the plan's per-license price with `unit_price` and set the initial license count with `quantity`. Both values apply only to license-based plans. When omitted, `quantity` defaults to `1` and `unit_price` defaults to the selected renewal interval's price. Monetary values use the smallest currency unit, for example `89700` for 897.00 DKK. The resulting subscription's `standard_price` is `unit_price * quantity`.

        ## Trial period

        Pass `trial_end_date` to start the subscription with a free trial running from `start_date` through that date (inclusive). Nothing is invoiced or charged during the trial; regular billing starts the day after the trial ends. An explicit `trial_end_date` overrides the plan's default trial configuration - omit the field to fall back to the plan's default trial (if any). Not available for usage-based plans. The trial can later be adjusted via `PUT /subscriptions/{uuid}/trial`.

        ## Intro discount

        If the plan has a default intro offer configured, the new subscription automatically inherits it - the first N months are discounted, after which the normal price applies (the same behaviour as in self-service checkout). The discount covers N months of value prorated across the actual billing periods, so it works on both anniversary (FLEXIBLE) and calendar-aligned (FIXED) plans even when the intro window ends mid-period. There is no request field for this; it follows the plan. When the subscription also has a trial, the intro discount starts on the first chargeable period after the trial. The resulting discount is readable via `GET /subscriptions/{uuid}/discounts`. Not applied to usage-based plans or to bundle children.

        ## Linking back to your own system

        Two optional fields let you stamp the subscription with identifiers from your own system so you can reconcile later without relying on webhooks:

        - `external_customer_id` — your customer identifier (e.g. the CRM id for the end user).
        - `external_subscription_id` — your subscription identifier (e.g. your internal id for this specific subscription).

        Both are returned on subsequent `GET /subscriptions` responses and are usable as filters on `GET /subscriptions?external_customer_id=…` and `GET /subscriptions?external_subscription_id=…`.
      operationId: createSubscription
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionRequest'
            examples:
              minimal:
                summary: Minimal request
                value:
                  plan_uuid: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  customer_uuid: "456e7890-e89b-12d3-a456-426614174001"
                  renewal_interval_uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              withExternalIds:
                summary: Request with external identifiers
                description: Include `external_customer_id` and `external_subscription_id` to map the subscription back to records in your own system.
                value:
                  plan_uuid: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  customer_uuid: "456e7890-e89b-12d3-a456-426614174001"
                  renewal_interval_uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  external_customer_id: "customer_12345"
                  external_subscription_id: "sub_abc123"
              withTrial:
                summary: Request with a trial period
                description: The subscription starts with a free trial from `start_date` through `trial_end_date`; billing begins the day after.
                value:
                  plan_uuid: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  customer_uuid: "456e7890-e89b-12d3-a456-426614174001"
                  renewal_interval_uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  start_date: "2026-05-01"
                  trial_end_date: "2026-05-14"
              licenseBasedCustomPrice:
                summary: License-based subscription with a customer-specific unit price
                value:
                  plan_uuid: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  customer_uuid: "456e7890-e89b-12d3-a456-426614174001"
                  renewal_interval_uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  quantity: 7
                  unit_price: 89700
              complete:
                summary: Complete request with all optional fields
                value:
                  plan_uuid: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
                  customer_uuid: "456e7890-e89b-12d3-a456-426614174001"
                  renewal_interval_uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  external_customer_id: "customer_12345"
                  external_subscription_id: "sub_abc123"
                  name: "Andersenvej 12, 2. th."
                  start_date: "2026-05-01"
                  trial_end_date: "2026-05-14"
      responses:
        '201':
          description: Subscription created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan or customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error, plan has no pricing configured, or a trial was requested on a usage-based plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /subscriptions/{uuid}:
    get:
      summary: Get a single subscription
      description: Retrieve a single subscription by UUID for the authenticated user's team.
      operationId: getSubscription
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription to retrieve
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      responses:
        '200':
          description: Successful response with subscription details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
    delete:
      summary: Delete a subscription
      description: |
        Permanently delete a subscription by UUID. This removes the subscription and all of its history (transactions, scheduled changes, events, trials, pauses, proration adjustments, tier values).

        A subscription cannot be deleted once any invoices have been created for it. Cancel it instead via the [Cancel endpoint](#tag/Subscriptions/operation/cancelSubscription).
      operationId: deleteSubscription
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription to delete
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      responses:
        '204':
          description: Subscription deleted successfully
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Subscription has invoices and cannot be deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "This subscription cannot be deleted because invoices have already been issued for it - deleting it would orphan invoice records that are required for accounting. To stop further billing, cancel the subscription instead via POST /v1/subscriptions/{uuid}/cancel."
  /subscriptions/{uuid}/custom-fields:
    put:
      summary: Set a subscription's custom fields
      description: |
        Set the custom field values on a subscription, as an object keyed by each field's public `key`.

        Custom fields are defined per plan under the custom fields module. Requires the module to be enabled. Unknown keys are rejected with `422`, and each value is validated against the field's type. Pass an empty string or `null` to clear a field. Only the keys you include are changed.

        While a plan change is scheduled, ordinary and automatically carried fields remain editable. A field that has an explicit target value in the scheduled plan change cannot be changed through this endpoint until the plan change is updated, cancelled, applied, or has failed; that conflict returns `422` with a standard error response.
      operationId: updateSubscriptionCustomFields
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - custom_fields
              properties:
                custom_fields:
                  type: object
                  description: Custom field values keyed by the plan's custom field key. Send every value as a string, including integer and date fields (e.g. "42", "2026-06-01").
                  additionalProperties:
                    type: string
                    nullable: true
                  example:
                    license-key: "LIC-ABC-123"
      responses:
        '200':
          description: Successful response with the updated subscription
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation failed, or an edited field has an explicit value in a scheduled plan change
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/Error'
              examples:
                validationFailed:
                  summary: Invalid custom field value
                  value:
                    message: "The given data was invalid."
                    errors:
                      custom_fields.license-key:
                        - "The custom_fields.license-key field format is invalid."
                scheduledValueConflict:
                  summary: The scheduled plan change has an explicit value for the field
                  value:
                    message: "One or more custom fields have explicit values in the scheduled plan change. Update or cancel the plan change before editing those fields."
  /subscriptions/{uuid}/cancel:
    post:
      summary: Cancel a subscription
      description: |
        Cancel a subscription. By default, cancellation normally takes effect at the end of the current billing period - the subscription's `end_date` begins as the day before the next renewal date and the subscription remains active (status `under_cancellation`) until that date. If a later ordinary, non-installment period has already been invoiced or charged, or has a card capture pending, the end date is moved to the end of that protected period.

        This non-expansive operation remains available while a Team's management access is payment-locked, so future billing can still be stopped. It returns 402 after the Team reaches terminal closure.

        Pass `immediate: true` in the request body to request cancellation from today. The same protection still applies, so `end_date` is today only when no later ordinary, non-installment period is protected; otherwise it is the end of the latest protected period.

        Installment periods settle slice by slice. A settled installment slice is preserved and remains invoiceable, but it does not keep the subscription active through the whole parent period.

        Pending prorated charges or credits are settled on a final invoice during cancellation by default, so they cannot be left without a future billing period. Pass `pending_proration_handling: waive` only when those adjustments should be deliberately discarded without invoicing.

        **Important:** End-of-period cancellation works on active subscriptions, and on pending subscriptions that already have issued invoice lines and therefore cannot be deleted safely. Pending subscriptions without invoices should be deleted instead. Attempting to cancel a subscription that is already cancelled or under cancellation will return a 422 error.

        Subscriptions that were purchased as part of a bundle have their own lifecycle after purchase and can be cancelled individually.

        To undo a cancellation before the end date, use the [Resume endpoint](#tag/Subscriptions/operation/resumeSubscription).
      operationId: cancelSubscription
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription to cancel
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                immediate:
                  type: boolean
                  description: |
                    When `true`, cancellation is requested from today. When `false` or omitted, it normally takes effect at the end of the current billing period. In both cases, a later ordinary, non-installment period that is already invoiced, charged or awaiting card capture pushes the end date to the end of that period. Settled installment slices are preserved separately and do not protect their whole parent period.
                  default: false
                  example: false
                pending_proration_handling:
                  type: string
                  enum: [invoice, waive]
                  description: |
                    How to settle pending prorated charges or credits. `invoice` creates a final invoice during cancellation; `waive` permanently removes the adjustments without invoicing them.
                  default: invoice
                  example: invoice
      responses:
        '200':
          description: Subscription cancelled successfully. Returns the updated subscription with `end_date` set.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
        '422':
          description: Subscription cannot be cancelled (e.g., already cancelled)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "This subscription is already cancelled."
  /subscriptions/{uuid}/resume:
    post:
      summary: Resume a cancelled subscription
      description: |
        Resume a subscription that is currently under cancellation by removing its `end_date`.

        This effectively undoes a previous cancellation. The subscription's `end_date` is cleared and the status returns to `active`, meaning it will continue to renew indefinitely.

        **Important:** This endpoint only works on subscriptions that are `under_cancellation` (have an `end_date` set in the future). Attempting to resume a subscription that:
        - Is already active (no `end_date`) will return a 422 error
        - Has already ended (`end_date` is in the past) will return a 422 error
      operationId: resumeSubscription
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription to resume
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      responses:
        '200':
          description: Subscription resumed successfully. Returns the updated subscription with `end_date` cleared.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
        '422':
          description: Subscription cannot be resumed (e.g., not cancelled or already ended)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notCancelled:
                  summary: Subscription is not cancelled
                  value:
                    message: "This subscription is not cancelled."
                alreadyEnded:
                  summary: Subscription has already ended
                  value:
                    message: "This subscription has already ended and cannot be resumed."
  /subscriptions/{uuid}/trial:
    put:
      summary: Update a subscription's trial period
      description: |
        Update the end date of a subscription's trial period.

        This endpoint modifies the trial end date, deletes any existing trial transaction, and regenerates the subscription's transactions to reflect the new trial duration.

        **Important:**
        - The subscription must have an existing trial period. If no trial exists, a 422 error is returned.
        - The new `end_date` must be after the trial's start date.
        - If the trial's transaction has been invoiced or charged (i.e., locked), the trial cannot be edited and a 422 error is returned.
      operationId: updateSubscriptionTrial
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription whose trial to update
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - end_date
              properties:
                end_date:
                  type: string
                  format: date
                  description: The new trial end date in `Y-m-d` format. Must be after the trial's start date.
                  example: "2026-04-15"
      responses:
        '200':
          description: Trial updated successfully. Returns the updated subscription.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
        '422':
          description: Trial cannot be updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                noTrial:
                  summary: Subscription has no trial
                  value:
                    message: "This subscription does not have a trial period."
                invalidEndDate:
                  summary: End date is before trial start
                  value:
                    message: "The end date must be after the trial start date."
                locked:
                  summary: Trial is locked
                  value:
                    message: "This trial cannot be edited because subsequent transactions have been invoiced or charged."
  /subscriptions/{uuid}/interval:
    put:
      summary: Change a subscription's billing interval
      description: |
        Change the billing interval of a subscription. The change takes effect from the next upcoming period.

        **Behavior:**
        - If no transactions have been invoiced yet, the interval and price are updated directly on the subscription.
        - Otherwise, a scheduled change is created for the next upcoming period. The new price is automatically looked up from the plan's configured renewal intervals.

        **Side effects:**
        - Any pending price changes (permanent or temporary) on or after the effective date are removed.
        - Future transactions are regenerated to reflect the new interval and pricing.
      operationId: updateSubscriptionInterval
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription whose interval to change
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - interval
              properties:
                interval:
                  type: integer
                  description: "The new billing interval in months. Must be one of: 1, 3, 4, 6, 12."
                  enum: [1, 3, 4, 6, 12]
                  example: 3
      responses:
        '200':
          description: Interval changed successfully. Returns the updated subscription.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
        '422':
          description: Validation error or business rule violation
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/Error'
              examples:
                validationError:
                  summary: Validation error
                  value:
                    message: "The interval field is required."
                    errors:
                      interval: ["The interval field is required."]
                invalidInterval:
                  summary: Invalid interval value
                  value:
                    message: "Invalid interval. Must be one of: 1, 3, 4, 6, 12."
                noPriceConfigured:
                  summary: No price for interval
                  value:
                    message: "The plan does not have a price configured for this interval."
  /subscriptions/{uuid}/quantity:
    put:
      summary: Change a subscription's license quantity
      description: |
        Change the number of licenses (units) on a license-based subscription.

        **Only applicable to license-based subscriptions.** Attempting to change quantity on a flat-rate subscription will return a 422 error.

        **Behavior:**
        - If `effective_date` is today or not provided, the change is applied immediately with proration.
        - If `effective_date` is in the future, a scheduled change is created.

        **Proration:**
        - When increasing quantity (upgrade), a prorated charge is calculated for the remaining days in the current billing period.
        - When decreasing quantity (downgrade), a prorated credit is calculated.
        - A percentage discount on that period scales the proration, so the adjustment matches the rate the period itself is billed at. A fixed discount does not: it is a one-off amount already applied in full to the period's own line, so the change is prorated at the undiscounted rate.
        - A percentage that expires part-way through a period is recorded as a fixed amount for the days it covered, and therefore follows the fixed rule above. A change made in the same period is prorated across the whole period rather than only the days outside the expired window, so it can be slightly off in that case.
        - No adjustment is created when nothing is owed for the rest of the period - for example on a fully discounted period. The quantity still changes.
        - Set `invoice_immediately` to `true` to generate a standalone proration invoice right away. No invoice is raised when there is no adjustment.

        **Side effects:**
        - The subscription's `standard_price` is recalculated as `unit_price * quantity`.
        - Future transactions are regenerated to reflect the new quantity.
      operationId: updateSubscriptionQuantity
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription whose quantity to change
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - quantity
              properties:
                quantity:
                  type: integer
                  minimum: 1
                  description: The new number of licenses/units.
                  example: 10
                effective_date:
                  type: string
                  format: date
                  nullable: true
                  description: |
                    Date when the quantity change should take effect (YYYY-MM-DD).

                    - If omitted or set to today's date, the change is applied immediately with proration.
                    - If set to a future date, a scheduled change is created.

                    Must be today or a future date.
                  example: "2026-04-01"
                invoice_immediately:
                  type: boolean
                  nullable: true
                  description: |
                    Whether to generate a standalone proration invoice immediately.

                    Only applicable for immediate changes (when `effective_date` is today or omitted). Defaults to `false`.
                  default: false
                  example: false
            examples:
              immediateUpgrade:
                summary: Immediate quantity increase
                value:
                  quantity: 15
              scheduledChange:
                summary: Scheduled quantity change
                value:
                  quantity: 10
                  effective_date: "2026-04-01"
              immediateWithInvoice:
                summary: Immediate change with proration invoice
                value:
                  quantity: 20
                  invoice_immediately: true
      responses:
        '200':
          description: Quantity changed successfully. Returns the updated subscription.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
        '422':
          description: Validation error or business rule violation
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/Error'
              examples:
                notLicenseBased:
                  summary: Not a license-based subscription
                  value:
                    message: "Quantity changes are only supported for license-based subscriptions."
                validationError:
                  summary: Validation error
                  value:
                    message: "The quantity field is required."
                    errors:
                      quantity: ["The quantity field is required."]
  /subscriptions/{uuid}/plan:
    put:
      summary: Change a subscription to another plan
      description: |
        Schedule a subscription to move to a different plan at a renewal
        boundary - by default its next unlocked one, or an explicit
        `effective_date` chosen from the subscription's
        `available_plan_change_dates`. Existing invoiced or charged periods keep
        their original plan, price, and VAT behaviour. Accounting product
        mappings are resolved from the historical plan when the invoice is
        synchronized.

        The source and target plans must belong to the same team, use the same
        renewal model, support the subscription's current currency and payment
        method, and be fixed-price or tiered plans. Bundle subscriptions,
        installment subscriptions, ended subscriptions, and locked tiers are not
        supported. If accounting is connected, the target plan must already have
        a product mapping.

        Calling this endpoint again replaces the existing pending plan change,
        unless a projected target-plan period has already been invoiced or
        charged. The response includes the effective date and scheduled-change
        UUID. The `subscription.plan_changed` webhook fires only when the change
        actually takes effect.
      operationId: updateSubscriptionPlan
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription whose plan to change
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plan_uuid
                - interval
              properties:
                plan_uuid:
                  type: string
                  format: uuid
                  description: UUID of the target plan.
                interval:
                  type: integer
                  enum: [1, 3, 4, 6, 12]
                  description: Renewal interval configured on the target plan for the subscription's currency.
                  example: 12
                effective_date:
                  type: string
                  format: date
                  nullable: true
                  description: |
                    The renewal date the plan change should take effect from. Must be one of the subscription's `available_plan_change_dates`; any other date returns 422. When omitted, the change takes effect at the next unlocked renewal.
                  example: "2026-09-01"
                custom_fields:
                  type: object
                  nullable: true
                  description: Values for the target plan's custom fields, keyed by public field key.
                  additionalProperties:
                    nullable: true
            example:
              plan_uuid: "987e6543-e21b-12d3-a456-426614174000"
              interval: 12
              custom_fields:
                seat-group: "Enterprise"
      responses:
        '200':
          description: Plan change scheduled. Returns the subscription and scheduling metadata.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
                  meta:
                    type: object
                    properties:
                      scheduled_change_uuid:
                        type: string
                        format: uuid
                      effective_date:
                        type: string
                        format: date
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error or the plan change violates a business rule
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/Error'
              examples:
                applyFailed:
                  summary: A same-day plan change could not be applied
                  value:
                    message: "The plan change could not be applied. No billing changes were made. Please review the subscription and try again."
                    meta:
                      scheduled_change_uuid: "123e4567-e89b-12d3-a456-426614174000"
                      effective_date: "2026-08-01"
                      status: failed

  /subscriptions/{uuid}/price:
    put:
      summary: Change a subscription's price
      description: |
        Change the recurring price of a flat-rate or license-based subscription.

        Send exactly one of two fields, matching the subscription's `pricing_type`:

        - `flat_rate` - send `price`, the new total recurring price.
        - `license_based` - send `unit_price`, the new price per license. `standard_price` is recalculated as `unit_price * quantity`.

        Sending the wrong field for the pricing type returns a 422 error rather than reinterpreting the value, so an integrator that echoes back a subscription's `standard_price` cannot accidentally multiply a license-based price by the license count. Sending both fields is also rejected.

        By default the change takes effect from the next upcoming period. You may instead pass an explicit `effective_date` to choose when the new price applies - it must be one of the subscription's selectable dates, which are exposed as `available_price_change_dates` on the subscription detail endpoint (`GET /subscriptions/{uuid}`). Sending an `effective_date` that is not in that set returns a 422 error.

        **Only applicable to flat-rate and license-based subscriptions.** Usage-based and tiered prices remain controlled by usage or tier configuration and return a 422 error.

        **Behavior:**
        - If nothing has been invoiced yet and the chosen date is the subscription's start date, the price is updated directly on the subscription.
        - Otherwise a scheduled permanent price change is created for the chosen period. If that period has already begun it is applied immediately (repricing the current period); a future period takes effect when it starts.

        **Side effects:**
        - Future transactions are regenerated to reflect the new price.
      operationId: updateSubscriptionPrice
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription whose price to change
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                price:
                  type: integer
                  minimum: 0
                  description: The new total recurring price. Required for a flat-rate subscription, rejected for a license-based one. Expressed in the smallest currency unit (e.g. øre for DKK, cents for EUR/USD).
                  example: 12000
                unit_price:
                  type: integer
                  minimum: 0
                  description: The new price per license. Required for a license-based subscription, rejected for a flat-rate one. Expressed in the smallest currency unit (e.g. øre for DKK, cents for EUR/USD).
                  example: 1500
                effective_date:
                  type: string
                  format: date
                  nullable: true
                  description: |
                    The date from which the new price should apply. Must be one of the subscription's `available_price_change_dates`. When omitted, the change defaults to the next upcoming period.
                  example: "2026-07-01"
      responses:
        '200':
          description: Price changed successfully. Returns the updated subscription.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
                  meta:
                    type: object
                    properties:
                      effective_date:
                        type: string
                        format: date
                        description: The date the price change was actually applied from.
                        example: "2026-07-01"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
        '422':
          description: Validation error or business rule violation
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/Error'
              examples:
                unsupportedPricingType:
                  summary: Unsupported pricing type
                  value:
                    message: "Price changes via this endpoint are only supported for flat-rate and license-based subscriptions."
                wrongPriceFieldForPricingType:
                  summary: Wrong price field for the subscription's pricing type
                  value:
                    message: "A license-based subscription is priced per license. Send unit_price instead of price."
                    errors:
                      unit_price: ["A license-based subscription is priced per license. Send unit_price instead of price."]
                validationError:
                  summary: Validation error
                  value:
                    message: "The price field is required when unit price is not present."
                    errors:
                      price: ["The price field is required when unit price is not present."]
                effectiveDateNotAvailable:
                  summary: Effective date is not selectable
                  value:
                    message: "Please select a valid effective date from the available options."
                    errors:
                      effective_date: ["Please select a valid effective date from the available options."]
  /subscriptions/{uuid}/discounts:
    get:
      summary: List discounts on a subscription
      description: |
        Retrieve all pending (not yet applied) discounts on a subscription.

        Returns only discounts that have not yet been applied to billing periods. Applied discounts are excluded from the results.

        Results are ordered by effective date (earliest first).
      operationId: listSubscriptionDiscounts
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription to list discounts for
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      responses:
        '200':
          description: List of pending discounts on the subscription.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SubscriptionDiscount'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
    post:
      summary: Add a discount to a subscription
      description: |
        Add a discount to a subscription by creating scheduled changes that apply the discount to future billing periods.

        **Discount types:**
        - `percent` - A percentage discount between 0.1 and 100, with at most 2 decimals. The value is a percentage, not basis points: `15` means 15% off and `14.29` means 14.29% off.
        - `fixed` - A fixed amount discount in the smallest currency unit (e.g., cents, øre). For example, a value of `5000` means 50.00 off.

        **A fixed discount does not scale with the license count.**
        It is one amount per billing period, so on a license-based subscription the effective price per license changes whenever the quantity does. To agree a price per license that stays fixed as the quantity changes, set `unit_price` on the subscription (`POST /subscriptions` or `PUT /subscriptions/{uuid}/price`) instead of discounting.

        A percentage has 2 decimals of precision, so an arbitrary agreed amount is not always expressible as one: taking 1350.00 off a 9450.00 period is exactly 14.285714...%, and the nearest supported values discount 1350.41 (14.29%) or 1349.46 (14.28%). Use `unit_price` when the agreed number must be exact.

        **Usage-based subscriptions:**
        Only `percent` discounts are supported on usage-based subscriptions - a percentage scales with the billed usage, so it is applied per consumed unit. A `fixed` discount is rejected with a 422 error.

        **Date requirements:**
        - `from_date` must correspond to an unlocked (not yet invoiced or charged) transaction period start date on the subscription.
        - `to_date` (optional) is exclusive and may fall mid-period: the discount is prorated across the period it cuts through. It must be after `from_date` and must not reach beyond the last generated billing period, otherwise the window could never close. If omitted, the discount applies indefinitely.
        - On a **usage-based** subscription `to_date` must instead land exactly on the start of a later unlocked billing period, because usage cannot prorate a partially-covered period.

        **Overlap prevention:**
        Discounts cannot overlap with existing pending discounts on the same subscription. The API will return a 422 error if the new discount's date range conflicts with an existing one.
      operationId: addSubscriptionDiscount
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription to add a discount to
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddSubscriptionDiscountRequest'
      responses:
        '201':
          description: Discount added successfully. Returns the created discount details.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/SubscriptionDiscount'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Membership]."
        '422':
          description: Validation error or business rule violation (e.g., overlapping discounts, invalid dates)
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/Error'
              examples:
                validationError:
                  summary: Validation error
                  value:
                    message: "The discount name field is required."
                    errors:
                      discount_name: ["The discount name field is required."]
                overlappingDiscount:
                  summary: Overlapping discount
                  value:
                    message: "This discount overlaps with an existing discount."
                invalidDate:
                  summary: Invalid from_date
                  value:
                    message: "The from_date must be a valid unlocked transaction period start date."
  /subscriptions/{uuid}/discounts/{discountUuid}:
    delete:
      summary: Remove a discount from a subscription
      description: |
        Remove a pending discount from a subscription.

        Only pending (not yet applied) discounts can be removed. If the discount has an associated end date, the corresponding end scheduled change is also removed automatically.

        After removal, affected subscription transactions are regenerated to reflect the updated pricing.
      operationId: removeSubscriptionDiscount
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription the discount belongs to
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
        - name: discountUuid
          in: path
          required: true
          description: UUID of the discount to remove (returned as `uuid` in the discount resource)
          schema:
            type: string
            format: uuid
          example: "987e6543-e21b-12d3-a456-426614174000"
      responses:
        '204':
          description: Discount removed successfully. No content returned.
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription or discount not found, or the discount has already been applied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\MembershipScheduledChange]."
  /subscriptions/{uuid}/scheduled-changes/{scheduledChangeUuid}:
    delete:
      summary: Delete a scheduled price or plan change from a subscription
      description: |
        Delete a pending (not yet applied) scheduled price or plan change.

        Use this to undo a plan change (`PUT /subscriptions/{uuid}/plan`) or a
        permanent/temporary price change (`PUT /subscriptions/{uuid}/price`).
        Plan changes are returned in `scheduled_plan_change`; price changes are
        returned in `scheduled_price_changes`.

        Only pending changes can be deleted; a change that has already been applied
        returns 422. A plan change also returns 422 once a projected period on
        the target plan has been invoiced or charged. For a temporary price
        change, deleting the start automatically removes its corresponding end
        change.

        After deletion, the subscription's future transactions are regenerated
        from its current plan and remaining scheduled changes. The updated
        subscription is returned.
      operationId: deleteSubscriptionScheduledChange
      tags:
        - Subscriptions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription the scheduled change belongs to
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
        - name: scheduledChangeUuid
          in: path
          required: true
          description: UUID of the scheduled price or plan change to delete (returned as `uuid` in `scheduled_price_changes` or `scheduled_plan_change`)
          schema:
            type: string
            format: uuid
          example: "987e6543-e21b-12d3-a456-426614174000"
      responses:
        '200':
          description: Scheduled change deleted successfully. Returns the updated subscription.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription or scheduled change not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\MembershipScheduledChange]."
        '422':
          description: The scheduled change has already been applied and cannot be deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "This scheduled change has already been applied and cannot be deleted."
  /subscriptions/{uuid}/usage:
    get:
      summary: Get current usage for a subscription
      description: |
        Retrieve the current billing period's usage summary for a usage-based subscription.

        Returns the total accumulated usage, the unit price, and the current billing period dates.

        **Only applicable to usage-based subscriptions.** Returns 422 for non-usage-based subscriptions.
      operationId: getSubscriptionUsage
      tags:
        - Usage
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the usage-based subscription
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      responses:
        '200':
          description: Current usage summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  current_period_usage:
                    type: integer
                    description: Total usage quantity in the current billing period
                    example: 150
                  unit_name:
                    type: string
                    description: The name of the usage unit (e.g. "SMS", "API calls")
                    example: "SMS"
                  unit_price:
                    type: integer
                    description: Price per unit in the smallest currency unit (e.g., cents)
                    example: 50
                  period_start:
                    type: string
                    format: date
                    nullable: true
                    description: Start date of the current billing period
                    example: "2026-03-01"
                  period_end:
                    type: string
                    format: date
                    nullable: true
                    description: End date of the current billing period
                    example: "2026-03-31"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Subscription is not usage-based
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "This subscription is not usage-based."
    post:
      summary: Record usage on a subscription
      description: |
        Record a usage event on a usage-based subscription.

        The usage is associated with the billing period that contains the `recorded_at` date. At the end of each billing period, accumulated usage is totaled and invoiced at the subscription's `unit_price`.

        **Restrictions:**
        - Only applicable to usage-based subscriptions (422 for others).
        - Cannot record usage for periods that have already been invoiced.
        - Cannot record usage before the subscription's start date.
      operationId: recordSubscriptionUsage
      tags:
        - Usage
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the usage-based subscription
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - quantity
              properties:
                quantity:
                  type: integer
                  minimum: 1
                  description: The number of units to record (e.g. 50 SMS sent)
                  example: 50
                description:
                  type: string
                  nullable: true
                  maxLength: 255
                  description: Optional description of the usage event
                  example: "Batch SMS campaign"
                recorded_at:
                  type: string
                  format: date
                  nullable: true
                  description: |
                    Date when the usage occurred (YYYY-MM-DD). Defaults to today if not provided.

                    Must fall within an active (non-invoiced) billing period.
                  example: "2026-03-15"
            examples:
              simple:
                summary: Record 50 SMS
                value:
                  quantity: 50
              withDetails:
                summary: Record usage with description and date
                value:
                  quantity: 150
                  description: "Marketing campaign batch"
                  recorded_at: "2026-03-10"
      responses:
        '201':
          description: Usage recorded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageRecord'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Subscription is not usage-based, or the period is already invoiced
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notUsageBased:
                  summary: Not a usage-based subscription
                  value:
                    message: "This subscription is not usage-based."
                periodInvoiced:
                  summary: Period already invoiced
                  value:
                    message: "Cannot record usage for an already invoiced period."
  /plans/{uuid}/usage:
    post:
      summary: Record usage by plan (auto-creates subscription)
      description: |
        A convenience endpoint for recording usage without needing to track subscription UUIDs.

        Pass a **plan UUID** and a **customer UUID** along with the usage quantity. The system will:

        1. Look for an existing active subscription for that customer on that plan.
        2. If found, record the usage on that subscription.
        3. If not found, automatically create a new subscription for the customer on that plan and then record the usage.

        **Auto-created subscriptions:**
        - Start date: today
        - Interval and unit price: from the plan's first configured renewal interval
        - Payment method: invoice
        - Source: `api`

        The response includes a `subscription_created` flag so you know whether a new subscription was provisioned.

        **Only applicable to usage-based plans.** Returns 422 for non-usage-based plans.
      operationId: recordUsageByPlan
      tags:
        - Usage
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the usage-based plan
          schema:
            type: string
            format: uuid
          example: "abc12345-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customer_uuid
                - quantity
              properties:
                customer_uuid:
                  type: string
                  format: uuid
                  description: UUID of the customer to record usage for
                  example: "cust-abc-123-456"
                quantity:
                  type: integer
                  minimum: 1
                  description: The number of units to record
                  example: 50
                description:
                  type: string
                  nullable: true
                  maxLength: 255
                  description: Optional description of the usage event
                  example: "Batch SMS campaign"
                recorded_at:
                  type: string
                  format: date
                  nullable: true
                  description: Date when the usage occurred (YYYY-MM-DD). Defaults to today.
                  example: "2026-03-15"
            examples:
              newCustomer:
                summary: Record usage (subscription auto-created if needed)
                value:
                  customer_uuid: "cust-abc-123-456"
                  quantity: 100
                  description: "API calls"
              existingSubscription:
                summary: Record usage on existing subscription
                value:
                  customer_uuid: "cust-abc-123-456"
                  quantity: 50
      responses:
        '201':
          description: Usage recorded successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/UsageRecord'
                  - type: object
                    properties:
                      subscription_created:
                        type: boolean
                        description: Whether a new subscription was auto-created for this request
                        example: true
                      subscription:
                        $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Plan or customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Plan is not usage-based, or has no renewal intervals configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notUsageBased:
                  summary: Not a usage-based plan
                  value:
                    message: "This plan is not usage-based."
                noIntervals:
                  summary: No renewal intervals configured
                  value:
                    message: "This plan has no renewal intervals configured."
  /subscriptions/{uuid}/tier:
    get:
      summary: Get current tier for a subscription
      description: |
        Retrieve the current tier, parameter values, and available tiers for a tiered subscription.

        Use this to inspect which tier a customer is currently on, what parameter values have been reported, and what other tiers are configured on the plan.

        **Only applicable to tiered subscriptions.** Returns 422 for non-tiered subscriptions.
      operationId: getSubscriptionTier
      tags:
        - Tiered Pricing
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the tiered subscription
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      responses:
        '200':
          description: Current tier information
          content:
            application/json:
              schema:
                type: object
                properties:
                  current_tier:
                    type: object
                    nullable: true
                    description: The tier the subscription is currently on
                    properties:
                      uuid:
                        type: string
                        format: uuid
                        example: "tier-uuid-abc-123"
                      name:
                        type: string
                        example: "Growth"
                      position:
                        type: integer
                        example: 2
                      is_free:
                        type: boolean
                        example: false
                  scheduled_tier_change:
                    allOf:
                      - $ref: '#/components/schemas/ScheduledTierChange'
                    nullable: true
                    description: The active future tier change, or null when no tier change is scheduled
                  tier_locked:
                    type: boolean
                    description: Whether automatic tier evaluation is locked by an administrative override
                    example: false
                  parameters:
                    type: object
                    description: |
                      Current parameter values keyed by parameter key. Each value includes the reported amount and when it was last reported.
                    additionalProperties:
                      type: object
                      properties:
                        value:
                          type: integer
                          example: 15
                        reported_at:
                          type: string
                          format: date-time
                          example: "2026-04-17T10:30:00+00:00"
                    example:
                      users:
                        value: 15
                        reported_at: "2026-04-17T10:30:00+00:00"
                      mrr:
                        value: 8500
                        reported_at: "2026-04-17T10:30:00+00:00"
                  available_tiers:
                    type: array
                    description: All tiers configured on the plan, ordered by position
                    items:
                      $ref: '#/components/schemas/PlanTier'
                  available_parameters:
                    type: array
                    description: All parameters configured on the plan
                    items:
                      $ref: '#/components/schemas/PlanTierParameter'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Subscription is not tiered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "This subscription is not tiered."
    put:
      summary: Select a tier for a subscription
      description: |
        Move a tiered subscription to an eligible tier. Immediate upgrades are applied at once; downgrades and next-period upgrades are scheduled at the first future billing period that has not been billed.

        Selecting the current tier removes any replaceable pending tier change and re-enables automatic tier evaluation. The selected tier must be priced for every interval already scheduled on the subscription and must satisfy the latest reported tier parameters. A subscription with an administrative tier override remains locked and returns 422; clear the override in the dashboard first.

        **Only applicable to tiered subscriptions.** Returns 422 when the tier is not eligible or cannot be safely scheduled.
      operationId: updateSubscriptionTier
      tags:
        - Tiered Pricing
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the tiered subscription
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tier_uuid
              properties:
                tier_uuid:
                  type: string
                  format: uuid
                  description: UUID of the tier to select
      responses:
        '200':
          description: Tier selection accepted
          content:
            application/json:
              schema:
                type: object
                required:
                  - current_tier
                  - scheduled_tier_change
                  - tier_locked
                  - action
                  - effective_date
                properties:
                  current_tier:
                    allOf:
                      - $ref: '#/components/schemas/TierSummary'
                    nullable: true
                  scheduled_tier_change:
                    allOf:
                      - $ref: '#/components/schemas/ScheduledTierChange'
                    nullable: true
                  tier_locked:
                    type: boolean
                  action:
                    type: string
                    enum: [unchanged, cancelled, immediate, scheduled]
                  effective_date:
                    type: string
                    format: date
                    nullable: true
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Subscription or tier not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Subscription is not tiered, has an administrative tier override, or the requested tier is not eligible or safely schedulable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /subscriptions/{uuid}/tier-parameters:
    post:
      summary: Report tier parameters for a subscription
      description: |
        Report the current parameter values for a tiered subscription. The system evaluates the correct tier using a "highest tier wins" rule and applies the tier change according to the plan's configured upgrade/downgrade behavior.

        **How tier evaluation works:**

        For each parameter, the system finds the tier whose condition (min/max range) matches the reported value. If multiple tiers match (across different parameters), the one with the highest position wins. The subscription's `standard_price` is updated to reflect the new tier's price.

        **Upgrade behavior** (configured on the plan):
        - `immediate` - Price changes immediately, with a prorated charge for the remaining days in the current period
        - `next_period` - A scheduled change is created that takes effect at the start of the next billing period

        **Downgrade behavior** (configured on the plan):
        - `manual` - No automatic change — admin must manually adjust
        - `automatic_next_period` - A scheduled change is created for the next period

        **Tier lock:** If the tier is locked (e.g., because an admin has manually overridden it), parameter values are still stored but the tier is **not** re-evaluated. The `action` field in the response will be `locked`.

        **Only applicable to tiered subscriptions.** Returns 422 for non-tiered subscriptions.
      operationId: reportSubscriptionTierParameters
      tags:
        - Tiered Pricing
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the tiered subscription
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - parameters
              properties:
                parameters:
                  type: object
                  description: |
                    Object keyed by parameter key (as defined on the plan) with the current absolute value for that parameter. Unknown keys are ignored.
                  additionalProperties:
                    type: integer
                    minimum: 0
                  example:
                    users: 15
                    mrr: 8500
                invoice_immediately:
                  type: boolean
                  nullable: true
                  description: |
                    If `true` and the change causes an immediate upgrade with proration, a proration invoice is created right away. Otherwise, the proration is added to the next regular invoice. When the period's discount leaves nothing owed for the remaining days, no proration and no invoice are created.
                  example: false
            examples:
              singleParameter:
                summary: Report a single parameter
                value:
                  parameters:
                    users: 25
              multipleParameters:
                summary: Report multiple parameters
                value:
                  parameters:
                    users: 25
                    mrr: 12000
              withImmediateInvoice:
                summary: Invoice the proration immediately
                value:
                  parameters:
                    users: 50
                  invoice_immediately: true
      responses:
        '200':
          description: Parameters recorded and tier evaluated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TierEvaluationResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Subscription is not tiered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "This subscription is not tiered."
  /plans/{uuid}/tier-parameters:
    post:
      summary: Report tier parameters by plan (auto-creates subscription)
      description: |
        A convenience endpoint for reporting tier parameters without needing to track subscription UUIDs.

        Pass a **plan UUID** and a **customer UUID** along with the parameter values. The system will:

        1. Look for an existing active subscription for that customer on that plan.
        2. If found, report the parameters on that subscription and evaluate the tier.
        3. If not found, automatically create a new subscription on the lowest tier and then report the parameters.

        **Auto-created subscriptions:**
        - Start date: today
        - Starting tier: the lowest configured tier on the plan
        - Interval: from the lowest tier's first configured price
        - Payment method: invoice
        - Source: `api`

        The response includes a `subscription_created` flag so you know whether a new subscription was provisioned.

        **Only applicable to tiered plans.** Returns 422 for non-tiered plans.
      operationId: reportTierParametersByPlan
      tags:
        - Tiered Pricing
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the tiered plan
          schema:
            type: string
            format: uuid
          example: "abc12345-e89b-12d3-a456-426614174000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customer_uuid
                - parameters
              properties:
                customer_uuid:
                  type: string
                  format: uuid
                  description: UUID of the customer to report parameters for
                  example: "cust-abc-123-456"
                parameters:
                  type: object
                  description: Object keyed by parameter key with current absolute values.
                  additionalProperties:
                    type: integer
                    minimum: 0
                  example:
                    users: 25
                    mrr: 12000
            examples:
              newCustomer:
                summary: Report parameters (subscription auto-created if needed)
                value:
                  customer_uuid: "cust-abc-123-456"
                  parameters:
                    users: 25
                    mrr: 12000
      responses:
        '201':
          description: Parameters recorded successfully
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      current_tier:
                        type: object
                        nullable: true
                        properties:
                          uuid:
                            type: string
                            format: uuid
                          name:
                            type: string
                          position:
                            type: integer
                          is_free:
                            type: boolean
                      tier_changed:
                        type: boolean
                      change_applied:
                        type: boolean
                        description: Whether the current tier was changed immediately by this request.
                      change_scheduled:
                        type: boolean
                        description: Whether a tier change is currently scheduled after this request.
                      manual_action_required:
                        type: boolean
                        description: Whether a lower eligible tier was detected but requires an administrator to select it.
                      scheduling_unavailable:
                        type: boolean
                        description: Whether a tier change was needed but no future unbilled period was available for scheduling.
                      action:
                        type: string
                        description: The tier evaluation outcome; `configuration_error` means the tier price is missing, while `scheduling_unavailable` means no future unbilled period exists. In either case parameters are saved and the subscription remains unchanged.
                        enum: [none, immediate, scheduled, manual_required, scheduling_unavailable, locked, plan_change_pending, configuration_error]
                      tier_locked:
                        type: boolean
                      subscription_created:
                        type: boolean
                        description: Whether a new subscription was auto-created for this request
                        example: true
                      subscription:
                        $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Plan or customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Plan is not tiered, or has no tier prices configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notTiered:
                  summary: Not a tiered plan
                  value:
                    message: "This plan is not tiered."
                noTierPrices:
                  summary: No tier prices configured
                  value:
                    message: "This plan has no tier prices configured."
  /customers:
    get:
      summary: List customers
      description: |
        Retrieve a paginated list of all customers for the authenticated user's team.

        Results are ordered by creation date (newest first) and include customer details such as contact information, address, registration number, and customer type.
      operationId: listCustomers
      tags:
        - Customers
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          description: "Number of customers to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
          example: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: customer_type
          in: query
          description: |
            Filter customers by type. Only customers matching the specified type will be returned.

            **Valid values:**
            - `company` - Company/business customers
            - `individual` - Individual/person customers

            If not provided, all customers are returned regardless of type.
          required: false
          schema:
            type: string
            enum: [company, individual]
          example: "company"
        - name: external_customer_id
          in: query
          description: |
            Filter customers by their external identifier.

            Returns only customers whose `external_customer_id` matches the provided value exactly. The match is team-scoped, so references belonging to other teams are never returned.

            Useful for resolving an Alunta customer from the identifier used in your own system without storing the Alunta UUID.
          required: false
          schema:
            type: string
            maxLength: 255
          example: "12345"
        - name: include_archived
          in: query
          description: |
            Include archived (soft-deleted) customers in the response. Defaults to `false`, which returns only live customers.

            Useful for resolving the state of an `external_customer_id` that has been soft-deleted: the unique constraint on `(team_id, external_customer_id)` still reserves the identifier even after archival, so without this flag a stale `external_customer_id` looks indistinguishable from a never-seen one.

            Archived rows are returned with `archived: true` and a populated `archived_at`.
          required: false
          schema:
            type: boolean
            default: false
          example: true
      responses:
        '200':
          description: Successful response with paginated customers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedCustomersResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error - Invalid request parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                message: "The given data was invalid."
                errors:
                  customer_type: ["The selected customer type is invalid."]
                  per_page: ["The per page must be between 1 and 100."]
    post:
      summary: Create a customer
      description: |
        Create a new customer for the authenticated user's team.

        Only `name` is required. All other fields are optional. If `customer_type` is not provided, it defaults to `company`. If `country` is not provided, it defaults to the team's country.

        To enable EAN/NemHandel delivery, set `invoice_delivery_method` to `ean` and provide either a valid 13-digit `ean`/GLN or a valid Danish CVR in `reg_number`. `their_reference` maps to `cbc:BuyerReference`, while `order_reference` maps to `cac:OrderReference/cbc:ID` in the electronic invoice. EAN activation controls whether Alunta can transmit the document; it does not change this API contract.

        If `external_customer_id` is already reserved by an archived customer, that customer is restored with the same UUID and updated from this request. Previously ended subscriptions remain ended. An active customer using the id still produces a `422` response.
      operationId: createCustomer
      tags:
        - Customers
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCustomerRequest'
            examples:
              fullCompany:
                summary: Company with all fields
                value:
                  name: "Acme Corp"
                  email: "info@acme.com"
                  phone: "+4512345678"
                  address: "Main Street 1"
                  zip: "1000"
                  city: "Copenhagen"
                  country: "DK"
                  reg_number: "DK12345678"
                  customer_type: "company"
                  website: "https://acme.com"
              minimalCustomer:
                summary: Minimal customer (name only)
                value:
                  name: "Jane Doe"
              individual:
                summary: Individual customer
                value:
                  name: "Jane Doe"
                  email: "jane@example.com"
                  customer_type: "individual"
              eanCustomer:
                summary: Customer delivered through EAN/NemHandel
                value:
                  name: "Property Company A/S"
                  address: "Main Street 1"
                  zip: "1000"
                  city: "Copenhagen"
                  country: "DK"
                  reg_number: "12345678"
                  ean: "5790000000005"
                  invoice_delivery_method: "ean"
                  email_invoice_when_ean: false
                  their_reference: "PROPERTY-61"
                  order_reference: "PO-12345"
      responses:
        '201':
          description: Customer created successfully, or an archived customer restored successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: |
            Validation error. Returned for any invalid input, including when the supplied `external_customer_id` is already in use by an active customer on this team.

            On collision the existing customer's UUID is included in the error message so you can recover programmatically — typically by treating the existing customer as the target instead of creating a duplicate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              examples:
                missingName:
                  summary: Missing required field
                  value:
                    message: "The given data was invalid."
                    errors:
                      name: ["The name field is required."]
                externalCustomerIdCollision:
                  summary: external_customer_id already in use
                  value:
                    message: "The given data was invalid."
                    errors:
                      external_customer_id:
                        - "A customer with this external_customer_id already exists. Existing customer UUID: 456e7890-e89b-12d3-a456-426614174001"
  /customers/{uuid}:
    get:
      summary: Get a single customer
      description: Retrieve a single customer by UUID for the authenticated user's team.
      operationId: getCustomer
      tags:
        - Customers
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the customer to retrieve
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
        - name: include_archived
          in: query
          required: false
          description: |
            When `true`, returns the customer even if it has been archived (soft-deleted). Defaults to `false`, which returns 404 for archived customers.
          schema:
            type: boolean
            default: false
          example: true
      responses:
        '200':
          description: Successful response with customer details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Customer]."
    patch:
      summary: Update a customer
      description: |
        Update one or more fields on an existing customer in the authenticated user's team. Only the fields you include in the request body are changed; omitted fields are left untouched.

        Changing a VAT-determining field (`reg_number`, `country`, or `customer_type`) clears any prior VAT validation on the customer. The new number is then re-validated asynchronously, and reverse charge is only re-applied once it is confirmed valid - so any invoice issued in the meantime conservatively charges VAT.

        `external_customer_id` cannot be changed here; it is the stable identifier set once at creation.

        EAN delivery requires a valid GLN in `ean` or Danish CVR in `reg_number`. Switching `invoice_delivery_method` from `ean` to `email` clears `order_reference` and disables `email_invoice_when_ean`; the general `their_reference` and stored EAN number are retained.
      operationId: updateCustomer
      tags:
        - Customers
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the customer to update
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCustomerRequest'
            examples:
              updateAddress:
                summary: Correct the billing address
                value:
                  address: "New Street 2"
                  zip: "8000"
                  city: "Aarhus"
              updateVatNumber:
                summary: Change the VAT number (clears prior validation)
                value:
                  reg_number: "DK87654321"
              enableEanDelivery:
                summary: Enable EAN/NemHandel delivery
                value:
                  ean: "5790000000005"
                  invoice_delivery_method: "ean"
                  email_invoice_when_ean: true
                  their_reference: "PROPERTY-61"
                  order_reference: "PO-12345"
      responses:
        '200':
          description: Customer updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Customer]."
        '422':
          description: |
            Validation error. Also returned when the body is empty (no updatable field
            supplied) or when `external_customer_id` is included (it cannot be changed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              examples:
                invalidField:
                  summary: Invalid field value
                  value:
                    message: "The given data was invalid."
                    errors:
                      country: ["The country must be 2 characters."]
                emptyBody:
                  summary: No updatable field supplied
                  value:
                    message: "The given data was invalid."
                    errors:
                      fields: ["Provide at least one field to update."]
                externalCustomerIdProhibited:
                  summary: Attempt to change the immutable identifier
                  value:
                    message: "The given data was invalid."
                    errors:
                      external_customer_id: ["The external customer id field is prohibited."]
  /customers/{uuid}/payer:
    post:
      summary: Set or clear the customer's payer (reseller)
      description: |
        Connect this customer to a payer (a reseller billed on its behalf), or clear the link.

        The payer must be another customer on the same team. Once a payer is set, each of this customer's subscriptions can be billed either to the payer (consolidated onto the payer's invoice) or to this customer directly. By default every subscription is paid by the payer; coverage is configured per subscription in the app. This customer is only blocked from being invoiced directly when all of its ready subscriptions are paid by the payer (see `POST /customers/{uuid}/invoices`).

        Pass `payer_uuid: null` to clear the payer.

        The reseller-billing invariants are enforced server-side; a violation returns `422` with `error: invalid_customer_payer`:

        - the payer must belong to the same team,
        - a customer cannot be its own payer,
        - no chains: a payer cannot itself be paid for by someone else, and a customer that already pays for others cannot be given a payer.
      operationId: setCustomerPayer
      tags:
        - Customers
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the customer whose payer is being set
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetCustomerPayerRequest'
            examples:
              set:
                summary: Set a payer
                value:
                  payer_uuid: "789e0123-e89b-12d3-a456-426614174099"
              clear:
                summary: Clear the payer
                value:
                  payer_uuid: null
      responses:
        '200':
          description: The updated customer
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: The customer or the supplied payer was not found on this team
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Customer]."
        '422':
          description: The payer assignment violates a reseller-billing invariant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "The payer must belong to the same team."
                error: "invalid_customer_payer"
  /customers/{uuid}/invoices:
    post:
      summary: Create invoices from the customer's ready transactions
      description: |
        Create real invoices from every membership transaction that is currently ready to be invoiced for this customer. The selection is not configurable — the endpoint always uses the same eligibility predicate the dashboard uses.

        ## Eligibility

        A transaction is included in the invoice when it is:

        - not already invoiced,
        - not on a pause,
        - not inside a trial,
        - has `period_start` within the next 60 days (the invoicing window),
        - has a non-negative `price`,
        - and, for card-gateway subscriptions, the payment has already been successfully charged (except subscriptions covered by a reseller - see below - which are never charged on their own card and so are always eligible).

        Transactions that don't meet all criteria — including far-future periods, paused subscriptions, and trials — are silently skipped.

        ## Resellers (consolidated billing)

        If this customer is a reseller (it pays on behalf of other customers), the created invoice consolidates its own ready transactions together with the ready transactions of every customer it pays for that are marked as paid by the reseller - exactly as the dashboard and the automatic run do. A sub-customer's subscriptions marked as paid by that sub-customer itself are not consolidated here; they are invoiced to the sub-customer directly. Conversely, a customer that is paid for by a reseller is only blocked from being invoiced directly when all of its ready subscriptions are reseller-paid; in that case calling this endpoint returns `422` with `error: customer_paid_by_reseller`. A customer with some self-paid subscriptions is invoiced for those.

        ## Behaviour

        - Transactions are grouped by currency — one invoice is created per currency. The response is always an array of invoices (typically `1`, possibly `2+` for multi-currency customers).
        - Payment records associated with the transactions are linked to the created invoice automatically, and the `InvoiceCreated` event is dispatched (including accounting-system sync if configured).
        - If the customer has no transactions ready to be invoiced, the request fails with `422`. Check `GET /subscription-transactions?ready_to_invoice=1&customer_uuid=…` first if you want to test whether there's anything to commit.

        Exchange-rate-unavailable, missing-currency, and other service-level errors are returned as `422` with a descriptive message.
      operationId: createCustomerInvoices
      tags:
        - Customers
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the customer
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCustomerInvoicesRequest'
            examples:
              empty:
                summary: Invoice all ready transactions with today's date
                value: {}
              withDate:
                summary: Invoice with a specific invoice date
                value:
                  date: "2026-05-15"
      responses:
        '201':
          description: One or more invoices were created. Always returned as an array.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Invoice'
              examples:
                singleInvoice:
                  summary: Single-currency customer — one invoice created
                  value:
                    data:
                      - uuid: "aa111111-e89b-12d3-a456-426614174001"
                        number: 1042
                        date: "2026-05-15T00:00:00.000000Z"
                        currency: "DKK"
                        total: 285000
                        vat: 71250
                        total_with_vat: 356250
                        customer:
                          uuid: "456e7890-e89b-12d3-a456-426614174001"
                          name: "Acme Corporation"
                        invoice_lines:
                          - text: "Monthly subscription"
                            amount: 1
                            price: 285000
                            vat_rate: 25
                        created_at: "2026-05-15T09:04:21.000000Z"
                multiCurrency:
                  summary: Multi-currency customer — one invoice per currency
                  description: A customer with both DKK and EUR subscriptions receives two invoices — one per currency. Callers should iterate over `data` rather than assuming a single result.
                  value:
                    data:
                      - uuid: "aa111111-e89b-12d3-a456-426614174001"
                        number: 1042
                        date: "2026-05-15T00:00:00.000000Z"
                        currency: "DKK"
                        total: 285000
                        vat: 71250
                        total_with_vat: 356250
                        customer:
                          uuid: "456e7890-e89b-12d3-a456-426614174001"
                          name: "Acme Corporation"
                      - uuid: "bb222222-e89b-12d3-a456-426614174002"
                        number: 1043
                        date: "2026-05-15T00:00:00.000000Z"
                        currency: "EUR"
                        total: 5000
                        vat: 0
                        total_with_vat: 5000
                        customer:
                          uuid: "456e7890-e89b-12d3-a456-426614174001"
                          name: "Acme Corporation"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            Validation error. Possible causes:

            - The customer has no transactions ready to be invoiced.
            - The exchange rate for the invoice's currency is not available on the selected date.
            - The customer is paid for by another customer (a reseller) and all of its ready subscriptions are reseller-paid, so it has nothing to invoice directly - those subscriptions are consolidated onto the payer's invoice. In that case `error` is `customer_paid_by_reseller` and `payer_uuid` identifies the paying customer. A customer with one or more self-paid subscriptions is invoiced for those instead.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              examples:
                nothingReady:
                  summary: Customer has nothing ready to invoice
                  value:
                    message: "No transactions are ready to be invoiced for this customer."
                    errors:
                      customer_uuid:
                        - "No transactions are ready to be invoiced for this customer."
                paidByReseller:
                  summary: Customer is paid for by a reseller
                  value:
                    message: "Acme Corporation is paid for by another customer and cannot be invoiced directly."
                    error: "customer_paid_by_reseller"
                    payer_uuid: "789e0123-e89b-12d3-a456-426614174099"
  /customers/{uuid}/invoices/preview:
    post:
      summary: Preview the invoices that would be created from the customer's ready transactions
      description: |
        Compute - but do not persist - the invoices that `POST /customers/{uuid}/invoices` would create right now for this customer. Useful for showing end users a confirmation screen, validating totals, or inspecting line items before committing.

        ## What it does

        - Selects the same set of "ready" membership transactions the real-creation endpoint uses (same eligibility predicate as the dashboard).
        - Groups them by currency and returns one in-memory invoice per currency, including discount lines and pending proration adjustments.
        - Does **not** create rows in `invoices` or `invoice_lines`, does **not** dispatch `InvoiceCreated`, does **not** trigger any accounting-system sync.

        ## Differences from the real response

        The response shape is identical to `POST /customers/{uuid}/invoices`, but a few fields are advisory or null because the invoice has not been persisted:

        - `uuid`, `paid_at`, `created_at`, `pay_url` are `null`.
        - `number` is the next number the team would receive if the invoice were issued *now*; it is not reserved and may be different by the time you actually call `POST /customers/{uuid}/invoices`.
        - `status` reflects the in-memory model and should be treated as informational.

        Totals (`total`, `vat`, `total_with_vat`), `currency`, `customer`, and `invoice_lines` are computed exactly the same way the real-creation endpoint computes them.

        ## Eligibility

        Same as `POST /customers/{uuid}/invoices`. A transaction is included when it is not already invoiced, not on a pause, not inside a trial, has `period_start` within the next 60 days, has a non-negative `price`, and (for card-gateway subscriptions) has been successfully charged.

        If nothing is ready, the request fails with `422` - just like the real endpoint.

        ## Usage-based subscriptions

        For usage-based plans, the preview reflects the `quantity` currently stored on the transaction. The endpoint does **not** finalize usage (i.e. it does not lock in the latest usage events) - that side effect happens when the real invoice is issued. If you ingest usage events between calling preview and calling create-invoice, the final invoice may show a higher quantity than the preview did.
      operationId: previewCustomerInvoices
      tags:
        - Customers
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the customer
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCustomerInvoicesRequest'
            examples:
              empty:
                summary: Preview with today's date
                value: {}
              withDate:
                summary: Preview with a specific invoice date (advisory)
                value:
                  date: "2026-05-15"
      responses:
        '200':
          description: One or more preview invoices. Always returned as an array, one per currency. Nothing is persisted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Invoice'
              examples:
                singleInvoice:
                  summary: Single-currency customer - one preview invoice
                  value:
                    data:
                      - uuid: null
                        number: 1042
                        date: "2026-05-15T00:00:00.000000Z"
                        currency: "DKK"
                        total: 285000
                        vat: 71250
                        total_with_vat: 356250
                        status: null
                        paid_at: null
                        pay_url: null
                        customer:
                          uuid: "456e7890-e89b-12d3-a456-426614174001"
                          name: "Acme Corporation"
                        invoice_lines:
                          - text: "Monthly subscription"
                            amount: 1
                            price: 285000
                            vat_rate: 25
                        created_at: null
                multiCurrency:
                  summary: Multi-currency customer - one preview invoice per currency
                  value:
                    data:
                      - uuid: null
                        number: 1042
                        currency: "DKK"
                        total: 285000
                        vat: 71250
                        total_with_vat: 356250
                        customer:
                          uuid: "456e7890-e89b-12d3-a456-426614174001"
                          name: "Acme Corporation"
                      - uuid: null
                        number: 1043
                        currency: "EUR"
                        total: 5000
                        vat: 0
                        total_with_vat: 5000
                        customer:
                          uuid: "456e7890-e89b-12d3-a456-426614174001"
                          name: "Acme Corporation"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            Validation error. Possible causes:

            - The customer has no transactions ready to be invoiced.
            - The customer is paid for by another customer (a reseller) and all of its ready subscriptions are reseller-paid, so it has nothing to preview directly. Preview shares the create endpoint's eligibility; `error` is `customer_paid_by_reseller` and `payer_uuid` identifies the paying customer. A customer with one or more self-paid subscriptions can preview those.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              examples:
                nothingReady:
                  summary: Customer has nothing ready to invoice
                  value:
                    message: "No transactions are ready to be invoiced for this customer."
                    errors:
                      customer_uuid:
                        - "No transactions are ready to be invoiced for this customer."
                paidByReseller:
                  summary: Customer is paid for by a reseller
                  value:
                    message: "Acme Corporation is paid for by another customer and cannot be invoiced directly."
                    error: "customer_paid_by_reseller"
                    payer_uuid: "789e0123-e89b-12d3-a456-426614174099"
  /customers/{uuid}/invoices/preview/pdf:
    post:
      summary: Render a preview invoice as PDF
      description: |
        Render the would-be invoice for the customer's ready transactions as a PDF, without persisting anything. This is the PDF counterpart to `POST /customers/{uuid}/invoices/preview`.

        ## Behaviour

        - By default, selects the same set of "ready" membership transactions the JSON preview uses.
        - When nothing is ready right now, falls back to the customer's earliest un-invoiced transaction (and any siblings sharing its `period_start`) so callers can preview an upcoming invoice ahead of time.
        - Pass `transaction_uuid` to override both — the preview is then scoped to that transaction's period for the customer.
        - Renders the PDF locally from the in-memory invoice using the same Blade template the dashboard renders the in-app preview with.
        - Does **not** create persisted invoice rows in Alunta and does **not** dispatch `InvoiceCreated`.

        For teams with e-conomic / Dinero configured to use the accounting system's PDF, the eventually-issued invoice will be re-rendered by the accounting system and may differ in styling. The data (lines, totals, VAT) is the same.

        ## Multi-currency

        The selected transactions are grouped by currency and one preview invoice exists per group. This endpoint renders **one** PDF, so:

        - When only one currency is selected, `currency` is optional.
        - When multiple currencies are selected, `currency` is required - else a `422` is returned. Pass `currency` matching one of the selected currencies (e.g. `"DKK"`).

        Use `POST /customers/{uuid}/invoices/preview` first if you need to know which currencies are pending.

        The response body is the raw PDF bytes with `Content-Type: application/pdf`.
      operationId: previewCustomerInvoicePdf
      tags:
        - Customers
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the customer
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                currency:
                  type: string
                  description: ISO 4217 currency code. Required only when multiple currencies are ready for invoicing.
                  example: "DKK"
                transaction_uuid:
                  type: string
                  format: uuid
                  description: |
                    Scope the preview to a specific membership transaction belonging to the customer. The preview will include that transaction and any sibling transactions for the same customer that share its `period_start`, so multi-line invoices preview correctly.

                    When omitted, the endpoint defaults to the ready-to-invoice set (with a fallback to the earliest un-invoiced transaction when nothing is ready right now).
                  example: "0c7d5f12-3b27-4a98-9b58-9f0d9a7c1e22"
            examples:
              singleCurrency:
                summary: Single-currency customer
                value: {}
              multiCurrency:
                summary: Multi-currency customer - pick one
                value:
                  currency: "DKK"
              specificTransaction:
                summary: Preview a specific upcoming transaction
                value:
                  transaction_uuid: "0c7d5f12-3b27-4a98-9b58-9f0d9a7c1e22"
      responses:
        '200':
          description: PDF binary stream
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            Validation error. Possible causes:

            - The customer has no transactions ready or pending to be invoiced.
            - Multiple currencies are selected and no `currency` was provided.
            - The provided `currency` does not match any selected transaction.
            - `transaction_uuid` does not match any transaction belonging to this customer.
            - The customer is paid for by another customer (a reseller) and all of its ready subscriptions are reseller-paid, so it has nothing to preview directly. Preview shares the create endpoint's eligibility; `error` is `customer_paid_by_reseller` and `payer_uuid` identifies the paying customer. A customer with one or more self-paid subscriptions can preview those.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              examples:
                nothingReady:
                  summary: Customer has nothing to invoice
                  value:
                    message: "No transactions are ready to be invoiced for this customer."
                    errors:
                      customer_uuid:
                        - "No transactions are ready to be invoiced for this customer."
                transactionNotFound:
                  summary: Provided transaction_uuid does not belong to this customer
                  value:
                    message: "Transaction not found for this customer."
                    errors:
                      transaction_uuid:
                        - "Transaction not found for this customer."
                multiCurrencyMissingFilter:
                  summary: Multi-currency, no currency specified
                  value:
                    message: "This customer has ready transactions in multiple currencies. Specify which currency to render."
                    errors:
                      currency:
                        - "This customer has ready transactions in multiple currencies. Specify which currency to render."
  /customers/{uuid}/usage-events:
    post:
      summary: Record a usage event on a customer
      description: |
        Record a usage event for a customer against a **usage parameter**. Usage parameters are team-global definitions (e.g. "SMS sent", "Active users") that you create either in the Alunta UI or implicitly by sending data here.

        ## How it works

        - If the parameter doesn't yet exist for your team, it is **auto-created on first ingest** with the supplied `kind` (defaulting to `counter`). The `parameter` field is normalized to a slug (lowercased, spaces and dashes become underscores).
        - The event is appended to the customer's usage history. It will appear on the customer's "Usage" tab in Alunta regardless of whether a subscription consumes it.
        - If the customer has an active subscription that meters this parameter, the event is **automatically linked** to that subscription:
          - `counter` events on a usage-based plan → counted toward the current period's billable usage.
          - `gauge` events on a tiered plan → trigger tier re-evaluation (same flow as `POST /subscriptions/{uuid}/tier-parameters`).
          - `gauge` events on a license-based plan → linked for visibility, but **do not** automatically change the membership quantity in v1.

        ## Counter vs gauge

        | Kind | Semantics | Use case |
        |------|-----------|----------|
        | `counter` (default) | Each event is a delta. The customer's current value is the SUM in the period. | SMS sent, API calls, transactions. |
        | `gauge` | Each event is a snapshot. The customer's current value is the LATEST report. | Active users, storage GB, current state. |

        Once a parameter is created with a given `kind`, the kind cannot be changed.

        ## Idempotency

        Pass an `idempotency_key` to make the request safely retryable. The key is unique per team for ~30 days. A retry with the same key returns the previously stored record with HTTP **200** (instead of `201`).

        ## Restrictions

        - Records on subscription periods that are already invoiced are rejected with `422`.
        - Records will not auto-create a subscription. If the customer doesn't have a matching subscription, the record is still stored but `subscription_uuid` will be `null`.
      operationId: recordCustomerUsageEvent
      tags:
        - Usage
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the customer
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - parameter
                - quantity
              properties:
                parameter:
                  type: string
                  maxLength: 255
                  description: |
                    Parameter key or human-readable name. Will be normalized to a slug for matching/creation.
                    For example, both `"SMS sent"` and `"sms_sent"` resolve to key `sms_sent`.
                  example: "sms_sent"
                quantity:
                  type: integer
                  description: |
                    For `counter` parameters: the delta to add (e.g. number of SMS sent in this batch). Negative values are allowed and will decrease the accumulated total for the period (e.g. to record a refund or correction).
                    For `gauge` parameters: the absolute current value (e.g. number of active users right now). Negative values are allowed.
                  example: 50
                kind:
                  type: string
                  enum: [counter, gauge]
                  nullable: true
                  description: |
                    Hint about how to interpret the parameter. Used **only when auto-creating a new parameter** — ignored if the parameter already exists. Defaults to `counter`.
                  example: "counter"
                recorded_at:
                  type: string
                  format: date-time
                  nullable: true
                  description: When the event occurred (ISO 8601). Defaults to now.
                  example: "2026-04-27T14:30:00+00:00"
                description:
                  type: string
                  nullable: true
                  maxLength: 255
                  description: Optional description shown on the customer's usage timeline.
                  example: "Batch campaign #4421"
                idempotency_key:
                  type: string
                  nullable: true
                  maxLength: 255
                  description: Idempotency key, unique per team. Retries with the same key return the existing record.
                  example: "evt_2026_04_27_4421"
            examples:
              counter:
                summary: Counter event (e.g. SMS sent)
                value:
                  parameter: "sms_sent"
                  quantity: 50
                  description: "Marketing campaign batch"
              gauge:
                summary: Gauge event (e.g. update active user count)
                value:
                  parameter: "active_users"
                  quantity: 42
                  kind: "gauge"
              idempotent:
                summary: Idempotent retry-safe event
                value:
                  parameter: "api_calls"
                  quantity: 1
                  idempotency_key: "req_abc123"
      responses:
        '201':
          description: Usage event recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CustomerUsageRecord'
        '200':
          description: Idempotent replay - the record already existed and is returned unchanged
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CustomerUsageRecord'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error or invoiced period
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invoicedPeriod:
                  summary: Recording on an already-invoiced subscription period
                  value:
                    message: "Cannot record usage for an already invoiced period."
    get:
      summary: List usage events for a customer
      description: |
        Returns a paginated list of usage events recorded for the customer, ordered by `recorded_at` descending.

        Supports filtering by parameter key and date range.
      operationId: listCustomerUsageEvents
      tags:
        - Usage
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the customer
          schema:
            type: string
            format: uuid
        - name: parameter
          in: query
          required: false
          description: Filter to events for a specific parameter key.
          schema:
            type: string
          example: "sms_sent"
        - name: from
          in: query
          required: false
          description: Only include events with `recorded_at` on or after this date (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Only include events with `recorded_at` on or before this date (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: per_page
          in: query
          required: false
          description: Items per page (max 200). Defaults to 50.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        '200':
          description: Paginated list of usage events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedCustomerUsageRecordsResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '404':
          description: Customer not found
  /portal-link/{uuid}:
    post:
      summary: Generate a portal link
      description: |
        Generate a portal link for your team. The behavior depends on whether a customer UUID is provided:

        **With a valid customer UUID:** Returns a signed auto-login URL that logs the customer directly into the self-service portal when visited. The link expires after a configurable duration (default: 15 minutes, maximum: 24 hours). Treat this link as a sensitive credential.

        **Without a UUID, or with an unrecognized UUID:** Returns the standard portal login URL for your team. Customers visiting this URL will see the login page where they can request a magic link via email.
      operationId: createPortalLink
      tags:
        - Portal
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: false
          description: UUID of the customer to generate an auto-login link for. If omitted or not found, the standard portal login URL is returned.
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePortalLinkRequest'
            examples:
              noBody:
                summary: No body (default expiry)
                value: {}
              customExpiry:
                summary: Custom expiry (1 hour)
                value:
                  expires_in_minutes: 60
      responses:
        '200':
          description: Portal link generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalLinkResponse'
              examples:
                autoLogin:
                  summary: Auto-login link (customer found)
                  value:
                    data:
                      url: "https://app.example.com/portal/my-team/verify/123?expires=1709470500&signature=abc123"
                      expires_at: "2026-03-03T12:15:00.000000Z"
                standardLogin:
                  summary: Standard login URL (no customer or not found)
                  value:
                    data:
                      url: "https://app.example.com/portal/my-team/login"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /plans:
    get:
      summary: List plans
      description: |
        Retrieve a paginated list of all plans for the authenticated user's team.

        Results are ordered by name (alphabetically) and include plan details such as name, description, renewal settings, currency, and pricing intervals.

        **Note:** Archived plans are excluded from results by default. Only active plans are returned.
      operationId: listPlans
      tags:
        - Plans
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          description: "Number of plans to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
          example: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: available_in_checkout
          in: query
          description: |
            Filter plans by checkout availability. Only plans matching the specified availability will be returned.

            **Values:**
            - `true` - Only plans available in checkout (publicly available for purchase)
            - `false` - Only plans not available in checkout (internal/manual plans)

            If not provided, all plans are returned regardless of checkout availability.
          required: false
          schema:
            type: boolean
          example: true
      responses:
        '200':
          description: Successful response with paginated plans
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPlansResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error - Invalid request parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                message: "The given data was invalid."
                errors:
                  available_in_checkout: ["The available in checkout field must be true or false."]
                  per_page: ["The per page must be between 1 and 100."]
    post:
      summary: Create a plan
      description: |
        Create a new plan with a single pricing interval.

        The plan is created with auto-renewal enabled and is not available in checkout by default. Use the `interval` field with string values (`monthly`, `quarterly`, `half-yearly`, `yearly`) to set the billing period.

        Renewal defaults to `fixed` (every subscriber on this plan renews on the same day). Pass `renewal: "flexible"` to renew each subscription individually based on its own start date.
      operationId: createPlan
      tags:
        - Plans
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePlanRequest'
      responses:
        '201':
          description: Plan created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Plan'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /plans/{uuid}:
    get:
      summary: Get a single plan
      description: Retrieve a single plan by UUID for the authenticated user's team.
      operationId: getPlan
      tags:
        - Plans
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the plan to retrieve
          schema:
            type: string
            format: uuid
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
      responses:
        '200':
          description: Successful response with plan details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Plan'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Plan]."
    put:
      summary: Update a plan
      description: |
        Update the `name` and/or `description` of an existing plan.

        Only the fields included in the request body are changed. Omit a field to leave it untouched. Pricing and renewal intervals are updated through the dedicated renewal-interval endpoint.
      operationId: updatePlan
      tags:
        - Plans
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the plan to update
          schema:
            type: string
            format: uuid
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePlanRequest'
            examples:
              nameAndDescription:
                summary: Update both name and description
                value:
                  name: "Premium plan"
                  description: "Updated product description"
              nameOnly:
                summary: Update name only
                value:
                  name: "Premium plan"
      responses:
        '200':
          description: Plan updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Plan'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
    delete:
      summary: Delete a plan
      description: |
        Delete a plan by UUID. The plan is soft-deleted (archived).

        A plan cannot be deleted if it has active subscriptions. Returns HTTP 422 if the plan has active subscriptions,
        or HTTP 409 while an accounting product operation still needs reconciliation or is in progress.
      operationId: deletePlan
      tags:
        - Plans
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the plan to delete
          schema:
            type: string
            format: uuid
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
      responses:
        '204':
          description: Plan deleted successfully
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Accounting product synchronization is in progress or requires reconciliation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Plan has active subscriptions and cannot be deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "Plan has active subscriptions"
  /plans/{uuid}/renewal-intervals:
    get:
      summary: List renewal intervals for a plan
      description: Retrieve all renewal intervals (pricing tiers) for a specific plan.
      operationId: listPlanRenewalIntervals
      tags:
        - Plans
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the plan
          schema:
            type: string
            format: uuid
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
      responses:
        '200':
          description: Successful response with renewal intervals
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PlanRenewalInterval'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /plans/{uuid}/renewal-intervals/{intervalUuid}:
    get:
      summary: Get a single renewal interval
      description: Retrieve a single renewal interval by UUID for a specific plan.
      operationId: getPlanRenewalInterval
      tags:
        - Plans
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the plan
          schema:
            type: string
            format: uuid
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
        - name: intervalUuid
          in: path
          required: true
          description: UUID of the renewal interval
          schema:
            type: string
            format: uuid
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        '200':
          description: Successful response with renewal interval details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PlanRenewalInterval'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan or renewal interval not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      summary: Update a plan renewal interval price
      description: |
        Update the `price` of a specific renewal interval on a plan.

        This only changes the configured price going forward. Existing subscriptions using this interval continue at the price they were created with unless separately adjusted.

        The `interval` (duration) and `currency` cannot be changed via this endpoint. To add or remove renewal intervals, use the plan management UI.
      operationId: updatePlanRenewalInterval
      tags:
        - Plans
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the plan
          schema:
            type: string
            format: uuid
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
        - name: intervalUuid
          in: path
          required: true
          description: UUID of the renewal interval to update
          schema:
            type: string
            format: uuid
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePlanRenewalIntervalRequest'
            examples:
              default:
                summary: Set a new price
                value:
                  price: 150000
      responses:
        '200':
          description: Renewal interval updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PlanRenewalInterval'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan or renewal interval not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /subscription-transactions:
    get:
      summary: List subscription transactions
      description: |
        Retrieve a paginated list of all subscription transactions for the authenticated user's team.

        Results are ordered by period start date (newest first) and include transaction details such as price, currency, period dates, invoicing status, and related subscription, customer, and plan information.

        **Understanding Subscription Transactions:**

        - **Transactions:** Each transaction represents a billing period for a subscription. Transactions are created automatically based on the subscription's billing interval and dates.

        - **Period Dates:** `period_start` and `period_end` define the billing period covered by this transaction. The `price` is charged for this entire period.

        - **Invoicing:** The `is_invoiced` field indicates whether an invoice has been created for this transaction. Transactions can be filtered by invoicing status.

        ## Building a "ready to invoice" queue

        To find all transactions that *should* be invoiced but are not yet, pass `ready_to_invoice=1`. This is a stricter filter than `invoiced=false` - it also excludes transactions that are on a pause, part of a trial, beyond the 60-day invoicing window, or have a negative price. It's the right filter if you want to mirror the dashboard's "generate invoices" queue in your own system.
      operationId: listSubscriptionTransactions
      tags:
        - Subscription Transactions
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          description: "Number of transactions to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
          example: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: customer_uuid
          in: query
          description: |
            Filter transactions by customer UUID. Only transactions belonging to subscriptions owned by the specified customer will be returned.

            Returns 422 if the customer UUID does not exist.
          required: false
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
        - name: invoiced
          in: query
          description: |
            Filter transactions by invoicing status.

            **Values:**
            - `true` - Only transactions that have been invoiced
            - `false` - Only transactions that have not been invoiced

            If not provided, all transactions are returned regardless of invoicing status.

            This is a raw "has an invoice line" check. To list only transactions that are *ready* to be invoiced (excluding paused, trial, and out-of-window rows), use `ready_to_invoice=true` instead.
          required: false
          schema:
            type: boolean
          example: false
        - name: ready_to_invoice
          in: query
          description: |
            When `true`, restricts the result to transactions that should be invoiced but are not yet. This is a stricter filter than `invoiced=false`:

            - not yet invoiced
            - not part of a pause
            - not part of a trial
            - `period_start` is within the next 60 days (the invoicing window)
            - `price` is non-negative

            Use this to generate a "ready to invoice" queue in your own system. Note: card-gateway transactions are included even if they have not yet been successfully charged - filter further on your side if you only want card-settled rows.
          required: false
          schema:
            type: boolean
          example: true
        - name: membership_uuid
          in: query
          description: |
            Filter transactions by subscription UUID.

            Only transactions belonging to the specified subscription will be returned.
          required: false
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
        - name: period_start_from
          in: query
          description: |
            Filter transactions by period start date (from).

            Only transactions with `period_start` on or after this date will be returned. Date format: YYYY-MM-DD
          required: false
          schema:
            type: string
            format: date
          example: "2024-01-01"
        - name: period_start_to
          in: query
          description: |
            Filter transactions by period start date (to).

            Only transactions with `period_start` on or before this date will be returned. Date format: YYYY-MM-DD
          required: false
          schema:
            type: string
            format: date
          example: "2024-12-31"
        - name: period_end_from
          in: query
          description: |
            Filter transactions by period end date (from).

            Only transactions with `period_end` on or after this date will be returned. Date format: YYYY-MM-DD
          required: false
          schema:
            type: string
            format: date
          example: "2024-01-01"
        - name: period_end_to
          in: query
          description: |
            Filter transactions by period end date (to).

            Only transactions with `period_end` on or before this date will be returned. Date format: YYYY-MM-DD
          required: false
          schema:
            type: string
            format: date
          example: "2024-12-31"
      responses:
        '200':
          description: Successful response with paginated subscription transactions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSubscriptionTransactionsResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error - Invalid UUID provided
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                message: "The selected membership uuid is invalid."
                errors:
                  membership_uuid: ["The selected membership uuid does not exist."]
  /subscription-transactions/{uuid}:
    get:
      summary: Get a single subscription transaction
      description: Retrieve a single subscription transaction by UUID for the authenticated user's team.
      operationId: getSubscriptionTransaction
      tags:
        - Subscription Transactions
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the subscription transaction to retrieve
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174003"
      responses:
        '200':
          description: Successful response with subscription transaction details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/SubscriptionTransaction'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription transaction not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\MembershipTransaction]."
  /invoices:
    get:
      summary: List invoices
      description: |
        Retrieve a paginated list of all invoices for the authenticated user's team.

        Results are ordered by invoice date (newest first) and include invoice details such as number, dates, status, amounts, payment status, and customer information.
      operationId: listInvoices
      tags:
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          description: "Number of invoices to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
          example: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: customer_uuid
          in: query
          description: |
            Filter invoices by customer UUID.

            Only invoices belonging to the specified customer will be returned.
          required: false
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
        - name: date_from
          in: query
          description: |
            Filter invoices by invoice date (from).

            Only invoices with `date` on or after this date will be returned. Date format: YYYY-MM-DD
          required: false
          schema:
            type: string
            format: date
          example: "2024-01-01"
        - name: date_to
          in: query
          description: |
            Filter invoices by invoice date (to).

            Only invoices with `date` on or before this date will be returned. Date format: YYYY-MM-DD
          required: false
          schema:
            type: string
            format: date
          example: "2024-12-31"
      responses:
        '200':
          description: Successful response with paginated invoices
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInvoicesResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error - Invalid UUID provided
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                message: "The selected customer uuid is invalid."
                errors:
                  customer_uuid: ["The selected customer uuid does not exist."]
    post:
      summary: Create a one-time invoice
      description: |
        Create a one-time invoice (not tied to a subscription) for a customer.

        This is useful for project fees, one-off charges, or any billing that is not recurring. The invoice is created and booked immediately.

        Prices must be provided in the smallest currency unit (e.g., cents for USD, ore for DKK). For example, 100.00 DKK should be sent as `10000`.

        If `vat_rate` is not provided on a line, it will be automatically calculated based on the customer's country and VAT status.

        The `invoice.created` webhook event fires when the invoice is created (regardless of payment status). If the team has an active Stripe integration and the invoice is payable online, the response includes a `pay_url` you can redirect the customer to for online card payment. The `pay_url` is `null` when online payment is not available, including invoices that will be collected automatically via Leverandørservice. When the customer pays, an `invoice.paid` webhook is fired; if the payment attempt fails, `invoice.payment_failed` is fired.
      operationId: createInvoice
      tags:
        - Invoices
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInvoiceRequest'
      responses:
        '201':
          description: Invoice created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error or unsupported currency
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /invoices/{uuid}:
    get:
      summary: Get a single invoice
      description: Retrieve a single invoice by UUID for the authenticated user's team.
      operationId: getInvoice
      tags:
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the invoice to retrieve
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174002"
      responses:
        '200':
          description: Successful response with invoice details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Invoice]."
  /invoices/{uuid}/pdf:
    get:
      summary: Download an invoice as PDF
      description: |
        Fetch the invoice rendered as a PDF.

        When the team has an accounting integration configured to use the accounting system's PDF (e-conomic, Dinero, Billy), the PDF is retrieved from that system. Otherwise a PDF is generated from the invoice data.

        The response body is the raw PDF bytes with `Content-Type: application/pdf` and `Content-Disposition: inline; filename="{invoice-number}.pdf"`.
      operationId: getInvoicePdf
      tags:
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the invoice
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174002"
      responses:
        '200':
          description: PDF binary stream
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The invoice is waiting for its accounting-system document number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "The invoice is waiting for its accounting-system number."
                code: "customer_facing_number_pending"
  /invoices/{uuid}/credit:
    post:
      summary: Credit selected invoice lines
      description: |
        Creates a booked credit note for all or part of one or more positive lines on a booked invoice. This is the API equivalent of the in-app credit flow and supports subscription-period, one-time and fee lines.

        Amounts are supplied excluding VAT in the smallest currency unit (e.g. cents or ore). The credit note inherits the original invoice's immutable VAT calculation method, exposed as `totals_calculation_method` on invoice responses. Documents priced with grouped VAT use `rate_group_rounding_v2`; documents whose amount was originally calculated without a grouped-method snapshot use `line_rounding_v1`, including invoices completed from legacy pending checkouts after deployment. Each requested amount must be no greater than that line's `remaining_creditable_amount`.

        On a `rate_group_rounding_v2` invoice, every partial credit must be representable as a standalone accounting document while leaving a representable invoice balance within every VAT-rate group. A selection that strands an indivisible VAT-rounding amount is rejected with `422`; adjust the selected amounts or credit the remaining balance together. Documents using `line_rounding_v1` allocate VAT cumulatively per original line and do not apply this grouped representability check. `remaining_creditable_amount` is an upper bound, not a guarantee that every intermediate amount below it is representable in isolation.

        The credit note is linked to the original invoice and is synchronized automatically to e-conomic, Dinero or Billy when an accounting integration is connected. Crediting a subscription-period line also records the cancellation against its subscription transaction so the period cannot be credited twice. Positive proration lines are credited independently and do not reduce the underlying period's remaining amount.

        By default the endpoint only creates the credit note. Pass `refund_payment=true` to also refund the selected amount including VAT through the original payment provider. A provider refund is attempted before the credit note is created, so a failed refund leaves no credit note. Providers that settle asynchronously return `202 Accepted`; no credit note exists until settlement is confirmed.

        The endpoint does not send the credit note by email. After a synchronous `201` response, use `POST /invoices/{credit_note_uuid}/send` if an email should be sent. EAN/NemHandel delivery remains automatic where configured.

        Fires `invoice.created` when the credit note is created. Also fires `invoice.refunded` when a provider payment is successfully refunded.

        ### Limitations

        - Only positive original invoice lines can be selected. Negative discount, credit and proration lines cannot themselves be credited.
        - Every selected line must belong to the invoice in the URL.
        - Installment-billed subscription periods are credited per invoice line/installment; the endpoint does not gather installments from other invoices.
        - Negative discount lines are not selectable in this per-line endpoint. An automatic whole-invoice reversal instead creates the exact signed counterpart of every remaining positive and negative line, preserving discounts and document-rounding adjustments explicitly.
        - On `rate_group_rounding_v2` invoices, a VAT-rounding split that cannot produce both a valid standalone credit note and a valid remaining balance within every VAT-rate group is rejected with `422`; adjust the selected amounts or credit the remaining balance together. This limitation does not apply to `line_rounding_v1` invoices.
        - When `refund_payment=true`, invoices with multiple refundable provider payments are rejected.
      operationId: creditInvoiceLines
      tags:
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the invoice containing the lines to credit
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174002"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreditInvoiceRequest'
            examples:
              subscriptionPeriod:
                summary: Partially credit a subscription period without refunding payment
                value:
                  lines:
                    - uuid: "123e4567-e89b-12d3-a456-426614174003"
                      amount_excl_vat: 4000
                  reason: "Partial period credit"
                  refund_payment: false
      responses:
        '201':
          description: Credit note created, with a completed refund when requested and available
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      credit_note:
                        $ref: '#/components/schemas/Invoice'
                      refund:
                        oneOf:
                          - $ref: '#/components/schemas/PaymentRefund'
                          - type: 'null'
                        description: Completed provider refund, or `null` when no payment was refunded.
        '202':
          description: Provider accepted an asynchronous refund; no credit note exists until settlement is confirmed
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      credit_note:
                        type: 'null'
                        description: Always `null` while the provider refund is pending.
                      refund:
                        $ref: '#/components/schemas/PaymentRefund'
                      status:
                        type: string
                        enum: [pending]
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            Validation or business-rule error. Common causes include an invalid or duplicate line UUID, a line from another invoice, a non-positive line, an amount above the remaining creditable amount, a selection that cannot be represented exactly when the original invoice uses rate-group VAT rounding, an unbooked or already credited invoice, a pending or manually unresolved refund on the invoice, multiple refundable payments, a missing exchange rate, or a provider refund/configuration failure.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/Error'
              examples:
                amountExceeded:
                  summary: Requested amount exceeds the line's remaining amount
                  value:
                    message: "Cancellation amount exceeds remaining amount for line Subscription period."
                vatRounding:
                  summary: Selection cannot form an exact standalone credit note and remaining balance
                  value:
                    message: "This partial credit cannot be represented exactly because of VAT rounding. Adjust the selected amounts or credit the remaining balance together."
                providerRefundFailed:
                  summary: Provider refund failed
                  value:
                    message: "Your card was declined."
                    errors:
                      refund_payment: ["Your card was declined."]
  /invoices/{uuid}/refund:
    post:
      summary: Reverse a one-off invoice and refund its payment
      description: |
        Reverses a booked **one-off invoice** by issuing a credit note for its remaining uncredited net basis, and (optionally) refunds the underlying provider payment (e.g. Stripe) for the same amount.

        Use this when an upstream record - such as a booking or order created via `POST /checkout-sessions` (`type=one_off_invoice`) - is cancelled and the invoice + payment must be undone.

        Subscription invoices are not supported; reverse those by cancelling the underlying subscription via `POST /subscriptions/{uuid}/cancel`.

        ### Behaviour

        - Issues a credit note linked to the original invoice. Every remaining line is reversed with its opposite sign, including positive counterparts for negative discounts and document-rounding adjustments. Lines that are already fully credited are skipped. The credit note is created in your accounting system (e-conomic, Dinero, Billy) automatically when an integration is connected.
        - When `refund_payment=true` (the default) and the invoice has exactly one provider payment (e.g. Stripe, OnPay), a refund for the credit note's amount (incl. VAT) is initiated against the provider before any database changes are committed. **If the provider refund fails, no credit note is created** (atomic semantics) - the call returns `422` with the provider error message.
        - Some providers settle a refund **asynchronously** (e.g. QuickPay accepts it and confirms later). In that case the call returns **`202 Accepted`** with `credit_note: null`, `status: "pending"` and the pending `refund` record - **no credit note exists yet**. The credit note is created (and the `invoice.refunded` webhook fired) only once the provider confirms the refund; if the provider declines on settlement, no credit note is ever created and the refund record transitions to `failed`. Treat a `202` as "refund in progress", not "refunded".
        - When `refund_payment=false`, only the credit note is created. Use this if you have already refunded the customer manually.
        - When the invoice has no refundable provider payment (e.g. an unpaid bank-transfer invoice), the credit note is created and `refund` is `null` in the response.
        - Fully credited invoices are rejected with `422`.

        Fires the `invoice.refunded` webhook for each successful refund.

        ### Limitations (v1)

        - One-off invoices only - subscription invoices are rejected with `422`.
        - Whole-invoice scope only - the API credits the complete remaining invoice basis by reversing each remaining line with its opposite sign. Per-line partial credits are available through `POST /invoices/{uuid}/credit`.
        - Invoices with multiple provider payments cannot be refunded automatically; the integrator must reverse those manually.
      operationId: refundInvoice
      tags:
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the invoice to reverse
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174002"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefundInvoiceRequest'
      responses:
        '201':
          description: Credit note created (and refund initiated when applicable)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      credit_note:
                        $ref: '#/components/schemas/Invoice'
                      refund:
                        oneOf:
                          - $ref: '#/components/schemas/PaymentRefund'
                          - type: 'null'
                        description: The refund record, or `null` if no refund was issued.
        '202':
          description: |
            Refund accepted but settling asynchronously at the provider (e.g. QuickPay). No credit note exists yet - it is created only once the provider confirms the refund. Poll the refund or await the `invoice.refunded` webhook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      credit_note:
                        type: 'null'
                        description: Always `null` - the credit note is not created until the refund settles.
                      refund:
                        $ref: '#/components/schemas/PaymentRefund'
                      status:
                        type: string
                        enum: [pending]
                        description: Indicates the refund is still settling at the provider.
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            Validation error. Common causes:
            - Invoice has already been fully credited.
            - Invoice has not been booked yet.
            - Invoice is a subscription invoice (only one-off invoices are supported).
            - Invoice has multiple provider payments.
            - Provider refund call failed (returns the provider error message).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                alreadyCredited:
                  summary: Invoice already credited
                  value:
                    message: "This invoice has already been fully credited."
                providerRefundFailed:
                  summary: Provider refund failed
                  value:
                    message: "Your card was declined."
                    errors:
                      refund_payment: ["Your card was declined."]
  /invoices/{uuid}/mark-as-paid:
    post:
      summary: Register a manual payment against an invoice
      description: |
        Record a manual payment against an unpaid (or partially paid) invoice and recompute its status. Use this to reconcile payments that happened outside of Alunta - for example a bank transfer you matched in your own system.

        ### Behaviour

        - Creates a `Payment` row with provider `manual`, attached to the invoice and its customer.
        - The invoice's status is recomputed from the resulting balance: `paid` when fully settled, `partially_paid` when the payment is smaller than the outstanding balance, and `overpaid` when the payment exceeds it. If the invoice already has a related credit note, it becomes `credited` once the remaining balance is fully covered.
        - Fires the `invoice.paid` webhook on the first transition from an outstanding balance to a fully covered balance, whether the resulting status is `paid`, `overpaid` or `credited`. A partial payment does not fire it.

        ### Defaults

        - `amount` defaults to the invoice's outstanding balance (full settlement).
        - `date` defaults to today.

        Call this endpoint multiple times to register multiple partial payments.
      operationId: markInvoiceAsPaid
      tags:
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the invoice
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174002"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarkInvoiceAsPaidRequest'
      responses:
        '201':
          description: Payment recorded and invoice status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      invoice:
                        $ref: '#/components/schemas/Invoice'
                      payment:
                        $ref: '#/components/schemas/Payment'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            Validation error. Common causes:
            - Invoice is a credit note.
            - Invoice already has no outstanding balance.
            - Another payment reduced the outstanding balance before this explicit amount could be recorded.
            - The supplied `payment_method` code does not resolve to an active payment method on the team.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /invoices/{uuid}/mark-as-sent:
    post:
      summary: Log that an invoice was sent
      description: |
        Record that an invoice has been delivered to the recipient through your own channel (e.g. you emailed the PDF yourself, posted it to a customer portal, or printed and mailed it). Use this when you handle delivery outside of Alunta but still want the invoice to appear as "sent" inside Alunta's UI and reports.

        This endpoint does **not** send any email. It only writes a sent-log entry attached to the invoice. After calling it the invoice is considered sent for the purpose of UI badges and the `isSent` check, identical to having sent it via `POST /invoices/{uuid}/send`.

        ### Behaviour

        - Creates an internal notification of type `invoice_sent` linked to the invoice and its customer.
        - Stores the supplied `email` (or, if omitted, the customer's email on file) as the recorded receiver.
        - Can be called multiple times - each call appends a new sent-log entry.
      operationId: markInvoiceAsSent
      tags:
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the invoice
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174002"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 255
                  description: Email address the invoice was delivered to. Defaults to the customer's email on file.
                  example: "recipient@example.com"
      responses:
        '200':
          description: Sent-log entry recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error (e.g. invalid email format)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /invoices/{uuid}/send:
    post:
      summary: Send an invoice by email
      description: |
        Email the invoice to a recipient as a PDF attachment.

        The email is queued for delivery and includes the invoice PDF, a localized subject and body (Danish or English based on the customer's country), and a "Pay invoice" call-to-action when the invoice is unpaid and a payment provider is configured.

        A `200` response confirms that the email was accepted for queued delivery, not that delivery has completed. If the invoice is configured to use its accounting-system number and that number has not been assigned yet, delivery remains queued until the final number is available; Alunta does not send the local invoice number as a fallback.

        If `email` is omitted, the invoice's customer email is used. If neither is available, a 422 is returned.

        The endpoint can be called multiple times to re-send an invoice; each call queues a new email.
      operationId: sendInvoice
      tags:
        - Invoices
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the invoice
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174002"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 255
                  description: Recipient email address. Defaults to the customer's email.
                  example: "recipient@example.com"
      responses:
        '200':
          description: Invoice queued for delivery
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: No recipient email available, or the supplied email is invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /payments:
    get:
      summary: List payments
      description: |
        Retrieve a paginated list of payments for the authenticated user's team.

        Results are ordered by payment date (newest first) and include the payment amount, currency, provider, and linked customer and invoice (when the payment settles an invoice).

        **Typical uses:**

        - Reconcile payments against your own ledger by date range.
        - List all payments collected against a given invoice (including partial payments).
        - Filter by provider (`stripe`, `onpay`, `quickpay`, `invoice`, `manual`) to isolate a single settlement flow.

        Payments that are not linked to an invoice (e.g. standalone manual payments) have `invoice: null`.
      operationId: listPayments
      tags:
        - Payments
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          description: "Number of payments to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
          example: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: customer_uuid
          in: query
          description: |
            Filter payments by customer UUID.

            Returns 422 if the customer UUID does not exist.
          required: false
          schema:
            type: string
            format: uuid
          example: "456e7890-e89b-12d3-a456-426614174001"
        - name: invoice_uuid
          in: query
          description: |
            Filter payments by invoice UUID. Only payments linked to the specified invoice are returned.

            Returns 422 if the invoice UUID does not exist.
          required: false
          schema:
            type: string
            format: uuid
          example: "789e0123-e89b-12d3-a456-426614174002"
        - name: payment_provider
          in: query
          description: |
            Filter payments by the provider that processed them.

            **Values:**
            - `stripe` - Collected via Stripe
            - `onpay` - Collected via OnPay
            - `quickpay` - Collected via QuickPay
            - `invoice` - Recorded as a bank transfer against an invoice
            - `manual` - Recorded manually (cash, other)
          required: false
          schema:
            type: string
            enum: [stripe, onpay, quickpay, invoice, manual]
          example: stripe
        - name: date_from
          in: query
          description: |
            Filter payments by payment date (from).

            Only payments with `date` on or after this date will be returned. Date format: YYYY-MM-DD
          required: false
          schema:
            type: string
            format: date
          example: "2024-01-01"
        - name: date_to
          in: query
          description: |
            Filter payments by payment date (to).

            Only payments with `date` on or before this date will be returned. Date format: YYYY-MM-DD
          required: false
          schema:
            type: string
            format: date
          example: "2024-12-31"
      responses:
        '200':
          description: Successful response with paginated payments
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPaymentsResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error - Invalid UUID or unknown provider
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                message: "The selected customer uuid is invalid."
                errors:
                  customer_uuid: ["The selected customer uuid does not exist."]
  /payments/{uuid}:
    get:
      summary: Get a single payment
      description: Retrieve a single payment by UUID for the authenticated user's team.
      operationId: getPayment
      tags:
        - Payments
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the payment to retrieve
          schema:
            type: string
            format: uuid
          example: "b3c4d5e6-f7a8-9012-bcde-f12345678901"
      responses:
        '200':
          description: Successful response with payment details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Payment'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Payment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Payment]."
  /webhooks:
    get:
      summary: List webhooks
      description: |
        Retrieve a paginated list of webhook delivery logs for your team.
        Use filters to find specific webhooks, e.g. failed deliveries that need to be replayed.
      operationId: listWebhooks
      tags:
        - Webhook Deliveries
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          description: Filter by delivery status
          required: false
          schema:
            type: string
            enum: [succeeded, failed, dispatching]
          example: "failed"
        - name: event
          in: query
          description: Filter by event type alias (e.g. subscription.created, customer.updated)
          required: false
          schema:
            type: string
          example: "subscription.created"
        - name: per_page
          in: query
          description: Number of results per page (1-100, default 15)
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
      responses:
        '200':
          description: Paginated list of webhook deliveries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedWebhooksResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /webhooks/{uuid}:
    get:
      summary: Get a single webhook
      description: Retrieve details of a specific webhook delivery by UUID.
      operationId: getWebhook
      tags:
        - Webhook Deliveries
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the webhook delivery
          schema:
            type: string
            format: uuid
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        '200':
          description: Webhook delivery details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/WebhookEvent'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Webhook]."
  /deals:
    get:
      summary: List deals
      description: |
        Retrieve a paginated list of custom offers (deals) for your team.
        Optionally filter by customer or status. Only plan-based deals are
        returned; bundle-based deals are managed in-app and not exposed here.
      operationId: listDeals
      tags:
        - Deals
      security:
        - bearerAuth: []
      parameters:
        - name: customer_id
          in: query
          required: false
          description: Filter deals by customer UUID
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: status
          in: query
          required: false
          description: Filter deals by status
          schema:
            type: string
            enum: [active, redeemed, expired]
          example: "active"
        - name: per_page
          in: query
          required: false
          description: Number of results per page (1-100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
      responses:
        '200':
          description: Paginated list of deals
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Deal'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
    post:
      summary: Create a deal
      description: |
        Create a custom offer (deal) for a customer with optional custom pricing, discounts, and trial periods.
        A unique checkout link is generated for the customer to complete the purchase.

        **Usage-based plans:** a deal on a usage-based plan may only carry a `percent` discount (a
        percentage scales with the billed usage, i.e. per unit). A `fixed` discount is rejected with a
        422. A time-limited discount must also cover whole billing periods - express it with
        `discount_duration_value`/`discount_duration_unit` in whole periods, or leave it indefinite; an
        absolute `discount_end_date` is not accepted for usage-based plans. These constraints are
        enforced at creation so a deal can never be redeemed with a discount that silently fails to apply.
      operationId: createDeal
      tags:
        - Deals
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDealRequest'
      responses:
        '201':
          description: Deal created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Deal'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error or no matching renewal interval
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /deals/{uuid}:
    get:
      summary: Get a deal
      description: |
        Retrieve a single deal by its UUID. Only plan-based deals are
        accessible via the API; bundle-based deals return a 404.
      operationId: getDeal
      tags:
        - Deals
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the deal
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '200':
          description: Deal details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Deal'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Deal not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      summary: Delete a deal
      description: |
        Delete an active deal and its associated checkout link.
        Only deals with status `active` can be deleted.
      operationId: deleteDeal
      tags:
        - Deals
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the deal to delete
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '204':
          description: Deal deleted successfully
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Deal not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Deal is not active and cannot be deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "Only active deals can be deleted."
  /events:
    get:
      summary: List events
      description: |
        Retrieve a paginated list of events that have happened in your team.

        Events are an audit trail of significant domain occurrences (subscription started, invoice generated, invoice sent, payment created, webhook delivery failed, etc.). Each event carries a `type`, optional `metadata`, and links to the related customer, subscription, and invoice when applicable.

        Results are ordered by creation date (newest first). Use the `types[]` parameter to restrict the response to specific event types, and `membership_uuid` to limit results to events tied to a single subscription.
      operationId: listEvents
      tags:
        - Events
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          description: "Number of events to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
          example: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: membership_uuid
          in: query
          description: |
            Filter events by subscription UUID. Only events linked to the specified subscription will be returned.

            Returns 422 if the subscription UUID does not exist in your team.
          required: false
          schema:
            type: string
            format: uuid
          example: "123e4567-e89b-12d3-a456-426614174000"
        - name: types[]
          in: query
          description: |
            Filter events by event type. Pass one or more values to restrict the response to those types only. If omitted, all event types are returned.
          required: false
          style: form
          explode: true
          schema:
            type: array
            items:
              $ref: '#/components/schemas/EventType'
          example: ["invoice_sent", "invoice_generated"]
      responses:
        '200':
          description: Successful response with paginated events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedEventsResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error - Invalid filter value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /bank-accounts:
    get:
      summary: List bank accounts
      description: |
        Retrieve a paginated list of all bank accounts for the authenticated user's team.

        Results are ordered by creation date (newest first). You can filter by currency code and active status.
      operationId: listBankAccounts
      tags:
        - Bank Accounts
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          description: "Number of bank accounts to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
          example: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: currency_code
          in: query
          description: Filter bank accounts by currency code (3-letter ISO 4217)
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
          example: "DKK"
        - name: is_active
          in: query
          description: Filter by active status (1 for active, 0 for inactive)
          required: false
          schema:
            type: integer
            enum: [0, 1]
          example: 1
      responses:
        '200':
          description: Successful response with paginated bank accounts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedBankAccountsResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
    post:
      summary: Create a bank account
      description: |
        Create a new bank account for the authenticated user's team.

        Bank accounts are used to display payment information on invoices. Each bank account can be set as the default for a specific currency. When `is_default_for_currency` is set to `true`, any other bank account for the same currency on the same team will automatically lose its default status.
      operationId: createBankAccount
      tags:
        - Bank Accounts
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBankAccountRequest'
            examples:
              danish:
                summary: Danish bank account with local details
                value:
                  label: "Main Account"
                  currency_code: "DKK"
                  country_code: "DK"
                  account_holder_name: "Acme Corp"
                  bank_name: "Danske Bank"
                  iban: "DK5000400440116243"
                  swift_bic: "DABADKKK"
                  local_details:
                    reg_nr: "0040"
                    account_nr: "0440116243"
                  is_default_for_currency: true
              minimal:
                summary: Minimal (required fields only)
                value:
                  currency_code: "EUR"
                  country_code: "DK"
                  account_holder_name: "Acme Corp"
      responses:
        '201':
          description: Bank account created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/BankAccount'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                message: "The given data was invalid."
                errors:
                  currency_code: ["The currency code field is required."]
                  country_code: ["The country code field is required."]
                  account_holder_name: ["The account holder name field is required."]
  /bank-accounts/{uuid}:
    get:
      summary: Get a bank account
      description: Retrieve a single bank account by UUID.
      operationId: getBankAccount
      tags:
        - Bank Accounts
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the bank account
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '200':
          description: Bank account details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/BankAccount'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Bank account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      summary: Update a bank account
      description: |
        Update an existing bank account. Only the fields provided in the request body will be updated (partial update).

        When `is_default_for_currency` is set to `true`, any other bank account for the same currency on the same team will automatically lose its default status.
      operationId: updateBankAccount
      tags:
        - Bank Accounts
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the bank account to update
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateBankAccountRequest'
            examples:
              updateLabel:
                summary: Update label and bank name
                value:
                  label: "New Label"
                  bank_name: "Nordea"
              setDefault:
                summary: Set as default for currency
                value:
                  is_default_for_currency: true
      responses:
        '200':
          description: Bank account updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/BankAccount'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Bank account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
    delete:
      summary: Delete a bank account
      description: Soft-delete a bank account. The bank account will no longer appear in listings but existing invoice snapshots are preserved.
      operationId: deleteBankAccount
      tags:
        - Bank Accounts
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the bank account to delete
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '204':
          description: Bank account deleted successfully
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Bank account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /webhooks/replay/{uuid}:
    post:
      summary: Replay a webhook
      description: |
        Re-dispatch a previously sent webhook using its original payload and stable UUID. The persisted
        payload fields and values remain unchanged, including the original `integration_settings` snapshot;
        the JSON request body may be re-encoded for delivery.
        This is useful for recovering from failed deliveries caused by downtime or deployment issues.

        Marketplace deliveries are sent to the integration's current endpoint using its current secret,
        and replay is rejected if the integration is no longer active for the team or no longer subscribes
        to the event. Team-configured deliveries retain their original destination URL and use the team's
        current webhook signing configuration; replay requires the API module, destination, and event to
        remain enabled.

        The webhook is re-dispatched asynchronously via the queue. The response reflects the webhook's
        status immediately after replay is initiated (status will be `dispatching`). Replay is rejected
        while a delivery or automatic retry is still expected, but stale delivery state can be recovered.

        The same UUID is reused, so the existing webhook record is updated rather than creating a duplicate.
      operationId: replayWebhook
      tags:
        - Webhook Deliveries
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          description: UUID of the webhook to replay
          schema:
            type: string
            format: uuid
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        '200':
          description: Webhook replay initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/WebhookEvent'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "No query results for model [App\\Models\\Webhook]."
        '422':
          description: Replay is no longer authorized by the current webhook configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                settingsMissing:
                  summary: Team webhook settings are missing
                  value:
                    message: "Webhook settings not found."
                marketplaceIntegrationInactive:
                  summary: Marketplace integration is no longer active for the team
                  value:
                    message: "The marketplace integration is no longer active for this team."
                marketplaceEventDisabled:
                  summary: Marketplace integration no longer subscribes to the event
                  value:
                    message: "The marketplace integration no longer receives this webhook event."
                webhookUrlInactive:
                  summary: Team webhook destination is no longer configured
                  value:
                    message: "The webhook URL is no longer active."
                webhookEventDisabled:
                  summary: Team webhook event is no longer enabled
                  value:
                    message: "The webhook event is no longer enabled."
                apiModuleDisabled:
                  summary: Team API module is no longer enabled
                  value:
                    message: "The API module is no longer enabled for this team."
                alreadyDispatching:
                  summary: Webhook replay is already in progress
                  value:
                    message: "The webhook is already being dispatched."
  /subscription-bundles:
    get:
      summary: List subscription bundles
      description: Retrieve a paginated list of bundle templates for the authenticated team. A bundle groups several plans (and optional one-off charges) into a single product the customer purchases together.
      operationId: listSubscriptionBundles
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: per_page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
        - name: available_in_checkout
          in: query
          description: Filter by whether the bundle is purchasable via public self-service checkout.
          required: false
          schema:
            type: boolean
      responses:
        '200':
          description: Successful response with paginated subscription bundles
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SubscriptionBundle'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
    post:
      summary: Create a subscription bundle
      description: |
        Create a new bundle template combining several existing plans plus optional one-off charges. All plans in a bundle must share the same currency.
        Bundle templates are reusable: once created, a bundle can be activated for any number of customers via `POST /v1/customers/{uuid}/subscription-bundles`.
      operationId: createSubscriptionBundle
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionBundleRequest'
      responses:
        '201':
          description: Subscription bundle created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/SubscriptionBundle'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '422':
          description: Validation error - e.g. plans from different currencies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /subscription-bundles/{uuid}:
    get:
      summary: Get a subscription bundle
      operationId: getSubscriptionBundle
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Subscription bundle details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/SubscriptionBundle'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
    put:
      summary: Update a subscription bundle
      description: Update the bundle template. Providing `plan_uuids` replaces the plan set; providing `one_off_lines` replaces the one-off line set. Existing customer instances of the bundle are not retroactively modified.
      operationId: updateSubscriptionBundle
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSubscriptionBundleRequest'
      responses:
        '200':
          description: Subscription bundle updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/SubscriptionBundle'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
    delete:
      summary: Delete a subscription bundle template
      description: Soft-deletes the bundle template. Purchased subscriptions created from the bundle keep running independently and retain their historical bundle reference.
      operationId: deleteSubscriptionBundle
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Subscription bundle deleted successfully
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
  /customers/{customerUuid}/subscription-bundles:
    get:
      summary: List a customer's subscription bundle instances
      operationId: listCustomerSubscriptionBundles
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: customerUuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: per_page
          in: query
          description: "Number of bundle purchases to return per page (default: 15, max: 100)"
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 15
        - name: page
          in: query
          description: "Page number to retrieve (default: 1)"
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
      responses:
        '200':
          description: Successful response with the customer's paginated bundle instances
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedMembershipBundleInstancesResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          description: Validation error - Invalid pagination parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
    post:
      summary: Create a bundle purchase for a customer
      description: |
        Creates a bundle purchase record for the customer, with one child `Membership` per plan in the bundle. The bundle is purchase/audit context; after creation each child subscription is billed and managed individually.
        Child subscriptions keep their own billing rules, including plan default trials. The first invoice only includes amounts due immediately plus bundle one-off charges; child subscriptions with trials or usage-based billing may invoice later.
        Each child subscription also inherits its plan's default intro offer (like default trials): the first charge for that child is discounted accordingly, so the amount due now reflects every child's intro price.
        Per-membership `subscription.created` webhooks fire for each child, and a single `subscription_bundle.created` webhook fires for the bundle purchase.
      operationId: createCustomerSubscriptionBundle
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: customerUuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMembershipBundleRequest'
      responses:
        '201':
          description: Bundle activated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MembershipBundleInstance'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  /customers/{customerUuid}/subscription-bundles/{instanceUuid}:
    get:
      summary: Get a single subscription bundle instance for a customer
      operationId: getCustomerSubscriptionBundle
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: customerUuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: instanceUuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Bundle instance details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MembershipBundleInstance'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
    delete:
      summary: Cancel a bundle purchase for a customer
      description: |
        Bulk convenience that cancels the bundle purchase's child subscriptions at once. Each still-active child is cancelled individually at the end of its own current period and fires its normal `subscription.cancelled` webhook; a single consolidated `subscription_bundle.cancelled` webhook also fires. Any pending prorated adjustments on a cancelled child are settled on a final invoice. Children that have already been cancelled or ended are skipped, so this can cancel some, all, or none of them - a bundle does not guarantee any children remain active. Child subscriptions can equally be cancelled one by one via the subscriptions endpoints; the bundle is purchase/audit context, not a lifecycle container. Returns 422 when the bundle has no subscriptions left that can be cancelled (every child has already ended) - symmetric with resume.

        This non-expansive operation remains available while a Team's management access is payment-locked, so future billing can still be stopped. It returns 402 after the Team reaches terminal closure.
      operationId: cancelCustomerSubscriptionBundle
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: customerUuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: instanceUuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Bundle cancelled successfully
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          description: Bundle has no subscriptions left that can be cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /customers/{customerUuid}/subscription-bundles/{instanceUuid}/resume:
    post:
      summary: Resume a cancelled bundle purchase for a customer
      description: |
        The operational inverse of cancelling a bundle. Resumes the bundle purchase's still-resumable child subscriptions, clearing each one's pending end-of-period cancellation and firing its normal per-child `subscription.resumed` webhook. There is deliberately no consolidated `subscription_bundle.resumed` webhook - children resume individually. Much of the bundle may already be active - this resumes only the children still under cancellation, and children that have already ended cannot be resumed and are skipped, so it can affect some, all, or none of them. A bundle does not guarantee any children remain. Child subscriptions can equally be resumed one by one via the subscriptions endpoints; the bundle is purchase/audit context, not a lifecycle container. Returns 422 if the bundle is not under cancellation, or if it has no subscriptions left that can be resumed.
      operationId: resumeCustomerSubscriptionBundle
      tags:
        - Subscription Bundles
      security:
        - bearerAuth: []
      parameters:
        - name: customerUuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: instanceUuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Bundle resumed; returns the resumed bundle instance with its now-active memberships
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MembershipBundleInstance'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '402':
          $ref: '#/components/responses/PaymentRequiredError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          description: Bundle is not under cancellation, or has no subscriptions left that can be resumed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Use Laravel Passport OAuth2 tokens for authentication.
        Include the token in the Authorization header as: `Bearer {token}`

        ## Forcing the team picker (`prompt=consent`)

        The OAuth `/oauth/authorize` endpoint honors the standard `prompt=consent` query parameter. Append it to your authorize URL when starting the flow.

        Without `prompt=consent`, if the user has previously authorized your client, Alunta silently re-issues a token without showing the consent screen - which means the team picker is skipped, and the new token is scoped to the user's currently active team in their Alunta session. For users who own multiple teams this can lead to a token issued for the wrong team.

        With `prompt=consent`, Alunta always renders the consent screen. When the user belongs to more than one eligible team, the team picker is shown with no pre-selected option - the user must make a deliberate choice before the token is issued. We recommend partner integrations always send `prompt=consent` so each "Connect to Alunta" click results in a clean, explicit team selection.

        After the OAuth handshake, call `GET /me` to confirm which team the access token is scoped to.
  schemas:
    PingResponse:
      type: object
      properties:
        message:
          type: string
          example: pong
      required:
        - message
    MeResponse:
      type: object
      properties:
        team_uuid:
          type: string
          format: uuid
          description: The UUID of the team that the access token is scoped to.
          example: "9b2c5a48-3e7f-4d1a-b6e2-7f9c8a1b2c3d"
        team_name:
          type: string
          description: Display name of the team.
          example: "Acme ApS"
        scopes:
          type: array
          description: OAuth scopes granted to this access token.
          items:
            type: string
          example: ["read:customers", "read:subscriptions", "read:invoices"]
        base_currency:
          type: string
          description: ISO 4217 currency code used as the team's base currency.
          example: "DKK"
        timezone:
          type: string
          description: IANA timezone name for the team. Defaults to "Europe/Copenhagen".
          example: "Europe/Copenhagen"
      required:
        - team_uuid
        - team_name
        - scopes
        - base_currency
        - timezone
    Team:
      type: object
      description: A team (the SaaS tenant the access token is scoped to).
      properties:
        name:
          type: string
          description: Display name of the team.
          example: "Acme ApS"
        email:
          type: string
          format: email
          nullable: true
          description: Contact email for the team.
          example: "billing@acme.dk"
        phone:
          type: string
          nullable: true
          description: Contact phone number for the team.
          example: "+4512345678"
        vat_number:
          type: string
          nullable: true
          description: VAT/CVR registration number.
          example: "DK12345678"
        address:
          type: string
          nullable: true
          description: Street address.
          example: "Strandvejen 1"
        zip:
          type: string
          nullable: true
          description: Postal code.
          example: "2900"
        city:
          type: string
          nullable: true
          description: City.
          example: "Hellerup"
        invoicing_days_offset:
          type: integer
          nullable: true
          description: |
            Read-only. Number of days the team's invoice generation is offset relative to the period start. Negative values generate invoices ahead of the period (e.g. `-50` means invoices are generated 50 days before the period starts); `0` means invoices are generated on the period start date itself.

            Configured at provisioning time via `POST /v1/internal/provision-team` and not modifiable via the public API.
          example: 0
        invoice_due_days:
          type: integer
          nullable: true
          minimum: 0
          maximum: 365
          description: |
            Number of days customers have to pay generated invoices, counted from the invoice date. `0` means invoices are due on the invoice date itself. Defaults to `7`.
          example: 7
      required:
        - name
    UpdateTeamRequest:
      type: object
      description: All fields are optional. Only provided fields will be updated.
      properties:
        name:
          type: string
          maxLength: 255
          description: Display name of the team.
          example: "Acme ApS"
        email:
          type: string
          format: email
          nullable: true
          maxLength: 255
          description: Contact email for the team.
          example: "billing@acme.dk"
        phone:
          type: string
          nullable: true
          maxLength: 255
          description: Contact phone number for the team.
          example: "+4512345678"
        vat_number:
          type: string
          nullable: true
          maxLength: 255
          description: VAT/CVR registration number.
          example: "DK12345678"
        address:
          type: string
          nullable: true
          maxLength: 255
          description: Street address.
          example: "Strandvejen 1"
        zip:
          type: string
          nullable: true
          maxLength: 20
          description: Postal code.
          example: "2900"
        city:
          type: string
          nullable: true
          maxLength: 255
          description: City.
          example: "Hellerup"
        invoice_due_days:
          type: integer
          minimum: 0
          maximum: 365
          description: |
            Number of days customers have to pay generated invoices, counted from the invoice date. `0` means invoices are due on the invoice date itself.
          example: 14
    CreateCheckoutSessionRequest:
      type: object
      properties:
        type:
          type: string
          enum:
            - subscription
            - one_off_invoice
          default: subscription
          description: |
            Type of checkout session. Defaults to `subscription` when omitted.

            - `subscription`: Recurring subscription checkout. Requires `plan_id` and `external_customer_id`. Fires `checkout.completed` on completion.
            - `one_off_invoice`: Payment link for a one-off invoice. Requires `metadata.lines` and `metadata.currency`. `plan_id` must not be supplied. `external_customer_id` is optional. Fires `checkout.completed` (with `type: one_off_invoice`) and `invoice.created` on completion; `invoice.paid` fires immediately for card payments and later (when reconciled) for bank transfer.
          example: "subscription"
        external_customer_id:
          type: string
          nullable: true
          description: |
            Your platform's customer identifier. This will be included in the `checkout.completed` webhook payload.

            Required when `type` is `subscription`. Optional when `type` is `one_off_invoice`.
          example: "customer_12345"
        plan_id:
          type: string
          format: uuid
          nullable: true
          description: |
            UUID of the plan to pre-select in checkout. Must belong to your team.

            Required when `type` is `subscription`. Must not be supplied when `type` is `one_off_invoice`.
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
        quantity:
          type: integer
          nullable: true
          minimum: 1
          maximum: 1000
          description: |
            Initial quantity to pre-select in the hosted checkout quantity field. The customer can still change the value before completing checkout.

            Only supported for `subscription` sessions using a license-based plan. Must be omitted for other pricing types, bundles, and `one_off_invoice` sessions. The final chosen value is returned as `data.quantity` and `data.subscription.quantity` in `checkout.completed`.
          example: 25
        back_url:
          type: string
          format: uri
          nullable: true
          description: URL to redirect customers back to if they cancel checkout. Only shown if provided.
          example: "https://your-app.com/checkout/back"
        success_url:
          type: string
          format: uri
          nullable: true
          description: URL to redirect customers to after successful checkout. Takes priority over team's default confirmation URL.
          example: "https://your-app.com/checkout/success"
        payment_providers:
          type: array
          nullable: true
          description: |
            Whitelist of payment providers to expose on the public checkout page. Only valid for `one_off_invoice` sessions and must be omitted for `subscription` sessions (subscription provider selection comes from the plan).

            When omitted, every payment provider that is enabled on the team is offered. When supplied, the public checkout shows exactly the providers in this list (still filtered by team availability for safety). Use this to force bank transfer only, card only, or any explicit subset per checkout link.

            Allowed values: `invoice` (bank transfer), `onpay`, `stripe`, `quickpay`. Each value must be available for the team — `invoice` is always available; gateways require the corresponding integration to be connected.
          items:
            type: string
            enum:
              - invoice
              - onpay
              - stripe
              - quickpay
          example: ["invoice"]
        metadata:
          type: object
          nullable: true
          description: |
            Optional metadata object for pre-filling checkout form fields, configuring checkout behavior, setting custom trial periods, and attaching custom data to the resulting subscription. All fields are optional unless noted.

            **Customer Pre-fill Fields:**
            - `preferred_interval` (integer): Preferred renewal interval (in months) to auto-select in checkout. Must be a valid interval for the selected plan (e.g., 1, 3, 6, 12). If not provided, the first available interval will be selected.
            - `usage_parameters` (object, subscription with tiered plan only): Map of usage parameter keys to non-negative integer values used to place the new subscription at the correct initial tier on signup, and persisted as the subscription's first tier-parameter readings. Without this map a tiered subscription starts at the lowest tier and is only promoted later when usage is reported. Unknown keys, negatives, and non-integer values are dropped. Consumed at checkout time and not echoed back on the subscription's `metadata` field. Example: `{"seats": 25, "api_calls": 10000}`.
            - `starting_tier_uuid` (UUID, single tiered-plan subscription only): Minimum tier for the new subscription. The tier must belong to `plan_id`. `usage_parameters` may select a higher tier, but never a lower one. Consumed at checkout time and not echoed back on the subscription's `metadata` field.
            - `is_company` (boolean or string): Set customer type ("company" or "individual")
            - `name`: Company or person name
            - `email`: Email address
            - `phone`: Phone number
            - `country`: 2-letter country code (e.g., "dk", "us")
            - `address`: Street address
            - `zip`: Postal code
            - `city`: City name
            - `reg_number`: VAT/registration number

            **Trial Configuration Fields (subscription only):**
            - `trial_enabled` (boolean): Enable or disable trial period for this checkout. Overrides plan defaults.
            - `trial_type` (string): Type of trial - "days" or "months"
            - `trial_value` (integer): Duration of the trial (1-365 for days, 1-12 for months)

            **One-off invoice fields (required when `type` is `one_off_invoice`):**
            - `currency` (string, required): 3-letter ISO currency code. Must be one of the team's supported currencies.
            - `lines` (array, required, min 1): Invoice line items. See `CheckoutSessionInvoiceLine` schema.
            - `note` (string, optional): Note appended to the invoice.
            - `heading` (string, optional, max 255): Heading shown above the invoice on the public payment page. When omitted, the page renders without a heading.
            - `charge_vat` (boolean, optional): Defaults to `true`. Set to `false` to issue the invoice without VAT (the public payment page also shows 0% VAT). Useful for VAT-exempt sales where the team's normal country/customer VAT logic would otherwise apply a rate.

            **Custom Subscription Metadata:**

            Any additional keys beyond the reserved fields listed above are automatically forwarded to the resulting subscription's `metadata` field. You can read these values back via the subscriptions API. Values should be strings (max 500 characters). Not applicable for `one_off_invoice` sessions.
          additionalProperties: true
          example:
            preferred_interval: 12
            starting_tier_uuid: "7f9f8a5a-784f-4fe8-8f25-5a673c9550f3"
            is_company: true
            name: "Acme Corporation"
            reg_number: "12345678"
            email: "contact@acme.com"
            phone: "12345678"
            country: "dk"
            address: "Main Street 123"
            zip: "2100"
            city: "Copenhagen"
            trial_enabled: true
            trial_type: "days"
            trial_value: 14
            founding_tier: "gold"
            referral_code: "PARTNER_ABC"
      required:
        - type
    CheckoutSessionInvoiceLine:
      type: object
      description: A single line on a one-off invoice payment link.
      properties:
        text:
          type: string
          maxLength: 500
          description: Line description shown to the customer and on the invoice.
          example: "Consulting services"
        amount:
          type: integer
          minimum: 1
          description: Quantity (whole units).
          example: 2
        price:
          type: integer
          description: Unit price in minor units (e.g. øre/cents) excluding VAT.
          example: 150000
        accounting_product_number:
          type: string
          nullable: true
          maxLength: 50
          description: |
            Driver-specific external product identifier from the team's accounting integration. Optional.

            **Values per driver:**
            - **Dinero**: `ProductGuid` (UUID) from `GET https://api.dinero.dk/v1/{orgId}/products`
            - **e-conomic**: `productNumber`
            - **Billy**: `id`

            For e-conomic, the product must be active, not barred.

            This is *not* a chart-of-accounts number (e.g. "1000" / "1010"). To control which income account a line books on, set the sales account directly on the product in your accounting system.
          example: "a1b2c3d4-5678-90ab-cdef-1234567890ab"
      required:
        - text
        - amount
        - price
    CheckoutSession:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Checkout session ID (UUID)
          example: "550e8400-e29b-41d4-a716-446655440000"
        checkout_url:
          type: string
          format: uri
          nullable: true
          description: URL where customers can complete checkout. Expires after 24 hours.
          example: "https://app.alunta.com/checkout/550e8400-e29b-41d4-a716-446655440000"
      required:
        - id
        - checkout_url
    WebhookEvent:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier of the webhook delivery
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        event:
          type: string
          description: The event alias (e.g. subscription.created, customer.updated)
          example: "subscription.created"
        status:
          type: string
          enum: [dispatching, succeeded, failed]
          description: Current delivery status
          example: "dispatching"
        webhook_url:
          type: string
          format: uri
          description: The URL the webhook was sent to
          example: "https://example.com/webhook"
        payload:
          type: object
          description: The original webhook payload
          additionalProperties: true
        response_status_code:
          type: integer
          nullable: true
          description: HTTP status code from the last delivery attempt
          example: null
        attempt:
          type: integer
          nullable: true
          description: Number of delivery attempts made
          example: 1
        dispatched_at:
          type: string
          format: date-time
          nullable: true
          description: When the webhook was last dispatched
          example: "2026-03-13T10:30:00.000000Z"
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: When the last delivery attempt completed
          example: null
        created_at:
          type: string
          format: date-time
          description: When the webhook record was created
          example: "2026-03-13T10:00:00.000000Z"
      required:
        - uuid
        - event
        - status
        - webhook_url
        - payload
    PaginatedWebhooksResponse:
      type: object
      description: Paginated response containing webhook deliveries
      properties:
        data:
          type: array
          description: Array of webhook delivery objects
          items:
            $ref: '#/components/schemas/WebhookEvent'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
            last:
              type: string
              format: uri
              nullable: true
            prev:
              type: string
              format: uri
              nullable: true
            next:
              type: string
              format: uri
              nullable: true
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
            from:
              type: integer
              nullable: true
            last_page:
              type: integer
            path:
              type: string
              format: uri
            per_page:
              type: integer
            to:
              type: integer
              nullable: true
            total:
              type: integer
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    ValidationError:
      type: object
      properties:
        message:
          type: string
          description: Error message
        errors:
          type: object
          description: Validation errors by field
          additionalProperties:
            type: array
            items:
              type: string
      required:
        - message
    Error:
      type: object
      properties:
        message:
          type: string
          description: Error message
        code:
          type: string
          description: Error code
      required:
        - message
    CustomerUsageRecord:
      type: object
      description: |
        A usage event recorded on a customer against a usage parameter.

        Records are visible on the customer's "Usage" tab regardless of whether they are linked to a subscription. For `counter` parameters they accumulate within a billing period; for `gauge` parameters the latest record is the current value.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the usage record
          example: "9b2dd0fd-7c64-4e8a-9bda-c3a1c2d6f5e1"
        customer_uuid:
          type: string
          format: uuid
          description: UUID of the customer this record belongs to
          example: "456e7890-e89b-12d3-a456-426614174001"
        parameter:
          type: object
          description: The usage parameter this record is for. Always loaded.
          properties:
            uuid:
              type: string
              format: uuid
            key:
              type: string
              description: Slug used to identify the parameter in the API
              example: "sms_sent"
            name:
              type: string
              description: Human-readable display name
              example: "SMS Sent"
            kind:
              type: string
              enum: [counter, gauge]
              description: How the parameter aggregates - counter (sum) or gauge (latest value).
            unit:
              type: string
              nullable: true
              description: Optional unit label (e.g. "stk", "MB", "users")
        quantity:
          type: integer
          description: |
            For counter records: number of units recorded in this event (delta).
            For gauge records: the absolute value at the time of recording.
          example: 50
        recorded_at:
          type: string
          format: date-time
          description: When the event occurred (ISO 8601)
          example: "2026-04-27T14:30:00+00:00"
        description:
          type: string
          nullable: true
          description: Optional description supplied at recording time
          example: "Batch campaign"
        source:
          type: string
          enum: [api, manual, integration, legacy]
          description: |
            Where this record originated:
            - `api` - posted via the API
            - `manual` - logged via the Alunta UI
            - `integration` - created by an internal integration
            - `legacy` - migrated from the deprecated `membership_usage_records` table
        subscription_uuid:
          type: string
          format: uuid
          nullable: true
          description: |
            UUID of the subscription this event was auto-linked to, or `null` if no matching subscription was found at recording time.
          example: "123e4567-e89b-12d3-a456-426614174000"
        idempotency_key:
          type: string
          nullable: true
          description: The idempotency key supplied when recording, if any.
      required:
        - uuid
        - customer_uuid
        - parameter
        - quantity
        - recorded_at
        - source
    UsageRecord:
      type: object
      description: |
        A recorded usage event on a usage-based subscription.

        Usage records accumulate over a billing period and are totaled when the period ends to generate an invoice.
      properties:
        id:
          type: integer
          description: Unique identifier for the usage record
          example: 42
        quantity:
          type: integer
          description: Number of units consumed in this event
          example: 50
        description:
          type: string
          nullable: true
          description: Optional description of the usage event
          example: "Batch SMS campaign"
        recorded_at:
          type: string
          format: date-time
          description: When the usage occurred (ISO 8601)
          example: "2026-03-15T00:00:00+00:00"
        current_period_usage:
          type: integer
          description: Total accumulated usage in the current billing period (after this record)
          example: 150
      required:
        - id
        - quantity
        - recorded_at
        - current_period_usage
    Deal:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the deal
          example: "550e8400-e29b-41d4-a716-446655440000"
        customer_id:
          type: string
          format: uuid
          description: UUID of the customer this deal belongs to
          example: "660e8400-e29b-41d4-a716-446655440001"
        external_customer_id:
          type: string
          nullable: true
          description: Your platform's customer identifier. Included in the `checkout.completed` webhook payload when the deal is redeemed.
          example: "customer_12345"
        plan_id:
          type: string
          format: uuid
          description: UUID of the plan associated with this deal
          example: "770e8400-e29b-41d4-a716-446655440002"
        status:
          type: string
          enum: [active, redeemed, expired]
          description: Current status of the deal
          example: "active"
        effective_price:
          type: integer
          description: The effective price in the smallest currency unit (e.g., cents/øre), after applying the custom price override if set
          example: 10000
        custom_price:
          type: integer
          nullable: true
          description: Custom price override in the smallest currency unit. Null if using the plan's default price.
          example: 10000
        currency:
          type: string
          description: Three-letter currency code (uppercase)
          example: "DKK"
        interval:
          type: string
          description: Billing interval (e.g., "monthly", "quarterly", "yearly")
          example: "quarterly"
        checkout_url:
          type: string
          format: uri
          description: Unique checkout URL for the customer to complete the purchase
          example: "https://example.com/checkout/abc123"
        discount:
          type: object
          nullable: true
          description: Discount configuration, if any
          properties:
            name:
              type: string
              description: Descriptive name of the discount
              example: "Special offer"
            type:
              type: string
              enum: [percent, fixed]
              description: Type of discount
              example: "percent"
            value:
              type: integer
              description: Discount value (percentage 1-100 for percent, amount in smallest currency unit for fixed)
              example: 50
            end_date:
              type: string
              format: date-time
              nullable: true
              description: "Legacy absolute end date (ISO 8601), kept for backward compatibility. Null for duration-based or indefinite discounts."
              example: null
            duration:
              type: object
              nullable: true
              description: How long the discount runs from the customer's first invoice. Null for an indefinite or legacy end-date discount.
              properties:
                value:
                  type: integer
                  example: 1
                unit:
                  type: string
                  enum: [months, years]
                  example: "years"
        trial:
          type: object
          nullable: true
          description: Trial period configuration, if any
          properties:
            enabled:
              type: boolean
              example: true
            type:
              type: string
              enum: [days, months]
              example: "days"
            value:
              type: integer
              description: Duration of the trial period
              example: 14
        notes:
          type: string
          nullable: true
          description: Notes shown to the customer on the checkout page
          example: "As agreed upon in our meeting"
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: When the deal and its checkout link expire (ISO 8601). Null for no expiration.
          example: "2026-04-01T00:00:00+00:00"
        start_date:
          type: string
          format: date
          nullable: true
          description: "Optional subscription start date. It may be up to 10 years in the past or 20 years in the future. When omitted, the subscription starts when checkout is completed. Bundle deal start dates can currently be set through the Alunta interface only."
          example: "2026-04-15"
        created_at:
          type: string
          format: date-time
          description: When the deal was created (ISO 8601)
          example: "2026-03-16T10:30:00+00:00"
    CreateDealRequest:
      type: object
      properties:
        customer_id:
          type: string
          format: uuid
          description: UUID of the customer to create the deal for
          example: "550e8400-e29b-41d4-a716-446655440000"
        plan_id:
          type: string
          format: uuid
          description: UUID of the plan for this deal
          example: "660e8400-e29b-41d4-a716-446655440001"
        interval:
          type: integer
          description: Billing interval in months (e.g., 1 for monthly, 3 for quarterly, 12 for yearly)
          example: 3
        currency:
          type: string
          description: Three-letter currency code
          example: "DKK"
        external_customer_id:
          type: string
          nullable: true
          description: Your platform's customer identifier. This will be included in the `checkout.completed` webhook payload when the deal is redeemed, allowing you to automatically map the completed checkout back to a user in your system.
          maxLength: 255
          example: "customer_12345"
        start_date:
          type: string
          format: date
          nullable: true
          description: "Optional subscription start date. It may be up to 10 years in the past or 20 years in the future. When omitted, the subscription starts when checkout is completed."
          example: "2026-04-15"
        custom_price:
          type: integer
          nullable: true
          description: Custom price in the smallest currency unit (e.g., cents/øre). Omit to use the plan's default price.
          example: 10000
        discount_name:
          type: string
          nullable: true
          description: Name for the discount (required if applying a discount)
          maxLength: 255
          example: "Special offer"
        discount_type:
          type: string
          nullable: true
          enum: [percent, fixed]
          description: "Type of discount. Required when discount_name is provided. NOTE: on a usage-based plan only `percent` is accepted (a percentage applies per consumed unit); `fixed` is rejected with a 422."
          example: "percent"
        discount_value:
          type: integer
          nullable: true
          description: Discount value. For percent type (1-100), for fixed type the amount in smallest currency unit. Required when discount_name is provided.
          example: 50
        discount_duration_value:
          type: integer
          nullable: true
          minimum: 1
          description: "How long the discount runs, starting from the customer's first invoice. Must be provided together with discount_duration_unit. Omit both for an indefinite discount. The normalized duration (years x 12) may not exceed 24 months. On a usage-based plan the duration must cover whole billing periods (a multiple of the plan's billing interval), otherwise it is rejected with a 422."
          example: 1
        discount_duration_unit:
          type: string
          nullable: true
          enum: [months, years]
          description: The unit for discount_duration_value. Required together with it.
          example: "years"
        discount_end_date:
          type: string
          format: date
          nullable: true
          description: "Legacy: an absolute date the discount runs through (mapped onto the customer's invoice periods at redemption). Kept for backward compatibility; prefer discount_duration_value/unit. Mutually exclusive with the duration fields. Must be in the future, after the first billable date (the fixed start date plus any trial), and no more than 24 months after that first billable date. Not accepted on a usage-based plan (use a whole-period duration or leave the discount indefinite)."
          example: "2027-06-01"
        trial_enabled:
          type: boolean
          nullable: true
          description: Whether to include a trial period
          example: true
        trial_type:
          type: string
          nullable: true
          enum: [days, months]
          description: Type of trial period
          example: "days"
        trial_value:
          type: integer
          nullable: true
          description: Duration of the trial (1-365 for days, 1-12 for months)
          example: 14
        expires_at:
          type: string
          format: date
          nullable: true
          description: Date when the deal and its checkout link expire. Must be in the future.
          example: "2026-04-01"
        notes:
          type: string
          nullable: true
          description: Notes shown to the customer on the checkout page
          maxLength: 1000
          example: "As agreed upon in our meeting"
      required:
        - customer_id
        - plan_id
        - interval
        - currency
    AddSubscriptionDiscountRequest:
      type: object
      properties:
        discount_name:
          type: string
          description: A descriptive name for the discount (e.g., "Summer Campaign", "Loyalty Discount").
          maxLength: 255
          example: "Summer Campaign"
        discount_type:
          type: string
          enum: [percent, fixed]
          description: |
            The type of discount to apply.

            - `percent` - A percentage discount. `discount_value` is a percentage between 0.1 and 100 with at most 2 decimals.
            - `fixed` - A fixed amount discount. `discount_value` is a whole number in the smallest currency unit (e.g., cents, øre), and is one amount per billing period - it does not scale with the license count.
          example: "percent"
        discount_value:
          oneOf:
            - type: number
            - type: string
          description: |
            The discount amount. Interpretation depends on `discount_type`:

            - For `percent`: The percentage to discount, between 0.1 and 100 with at most 2 decimals. Example: `15` means 15% off, `14.29` means 14.29% off. A numeric string is also accepted, with either a dot or a comma as the decimal separator (`"14.29"`, `"14,29"`). More than 2 decimals, or a value outside the range, is rejected with a 422 error on this field.
            - For `fixed`: A whole number in the smallest currency unit (e.g., cents, øre), minimum `1`. Example: `5000` means 50.00 off. Decimals are rejected.
          example: 15
        from_date:
          type: string
          format: date
          description: |
            The start date for the discount (YYYY-MM-DD format).

            Must correspond to an unlocked (not yet invoiced or charged) transaction period start date on the subscription.
          example: "2026-04-01"
        to_date:
          type: string
          format: date
          nullable: true
          description: |
            The end date for the discount (YYYY-MM-DD format). Optional and exclusive.

            If provided, it must be after `from_date` and must not reach beyond the last generated billing period. It may fall mid-period: the discount is prorated across the period it cuts through. On a usage-based subscription it must instead land exactly on the start of a later unlocked billing period. If omitted or null, the discount applies indefinitely.
          example: "2026-07-01"
      required:
        - discount_name
        - discount_type
        - discount_value
        - from_date
    SubscriptionDiscount:
      type: object
      description: A discount applied to a subscription via scheduled changes.
      properties:
        uuid:
          type: string
          format: uuid
          description: The unique identifier for this discount. Use this UUID when removing the discount via the DELETE endpoint.
          example: "987e6543-e21b-12d3-a456-426614174000"
        discount_name:
          type: string
          description: The name of the discount.
          example: "Summer Campaign"
        discount_type:
          type: string
          enum: [percent, fixed]
          description: The type of discount applied.
          example: "percent"
        discount_value:
          type: number
          format: float
          description: |
            The discount amount. For `percent` type, this is a percentage (0.1-100) and may include up to 2 decimals. For `fixed` type, this is an integer amount in the smallest currency unit.
          example: 12.5
        effective_date:
          type: string
          format: date-time
          description: The date from which the discount takes effect (ISO 8601 format).
          example: "2026-04-01T00:00:00.000000Z"
        end_date:
          type: string
          format: date-time
          nullable: true
          description: |
            The date when the discount ends (ISO 8601 format).

            `null` if the discount is indefinite (no end date was specified).
          example: "2026-07-01T00:00:00.000000Z"
        subscription:
          type: object
          properties:
            uuid:
              type: string
              format: uuid
              description: UUID of the subscription this discount belongs to.
              example: "123e4567-e89b-12d3-a456-426614174000"
      required:
        - uuid
        - discount_name
        - discount_type
        - discount_value
        - effective_date
        - end_date
        - subscription
    Subscription:
      type: object
      description: |
        A subscription (membership) record representing a customer's subscription to a plan.

        Subscriptions define the billing relationship between a customer and a plan, including pricing, billing frequency, and lifecycle dates.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the subscription (UUID format)
          example: "123e4567-e89b-12d3-a456-426614174000"
        name:
          type: string
          nullable: true
          description: |
            Custom name for this subscription. If not set, defaults to the plan name.

            This field returns the raw value — `null` if no custom name was provided.
          example: "Andersenvej 12, 2. th."
        external_customer_id:
          type: string
          nullable: true
          description: |
            Your platform's customer identifier, as provided when creating the checkout session.

            This allows you to link a subscription back to a customer in your system without relying on webhooks. Will be `null` for subscriptions created without a checkout session or without an `external_customer_id`.
          example: "customer_12345"
        external_subscription_id:
          type: string
          nullable: true
          description: |
            Your platform's subscription identifier, as provided when creating the subscription via the API.

            This lets you map a specific subscription back to a record in your own system. Only settable when creating a subscription through `POST /subscriptions`; will be `null` otherwise.
          example: "sub_abc123"
        bundle_uuid:
          type: string
          nullable: true
          format: uuid
          description: UUID of the bundle purchase this subscription was originally created from, if any.
        purchased_via_bundle:
          type: boolean
          description: Whether this subscription was originally created as part of a bundle purchase.
        standard_price:
          type: integer
          description: |
            Standard price for the subscription interval, expressed in the smallest currency unit (e.g., cents, øre).

            **Important:** This is the price for the entire billing interval, not a monthly price. For example, if `interval` is 12 (annual) and `standard_price` is 120000, the customer pays 120000 currency units once per year, not monthly.

            For license-based subscriptions, this equals `unit_price * quantity`.
            For usage-based subscriptions, this storage-compatible field can be `0` or contain the configured base price per unit, depending on how the subscription was created. It is not the finalized usage total. Use `unit_price`, which normalizes both storage forms, for usage calculations.

            To calculate the monthly recurring revenue (MRR), divide `standard_price` by `interval`. Note: usage-based subscriptions should not be included in MRR calculations as their revenue is variable.
          example: 99900
        plan_price:
          type: integer
          nullable: true
          description: |
            The price configured on the plan for this subscription's current interval, currency and tier, before subscription-specific price overrides and discounts. Expressed in the smallest currency unit.

            For usage-based subscriptions this is the configured price per unit. Returns `null` when the matching plan price cannot be resolved.
          example: 99900
        effective_price:
          type: integer
          nullable: true
          description: |
            The authoritative price for the current full billing interval after the active discount has been applied, expressed in the smallest currency unit.

            For a time-limited discount ending inside the current interval, this reflects the actual prorated transaction price. Divide by `interval` only when a monthly equivalent is needed. Usage-based subscriptions return `0`; use `unit_price` together with `discount` for their variable charge. Returns `null` only when legacy or malformed billing data prevents a safe calculation.
          example: 79920
        vat_rate:
          type: number
          format: float
          nullable: true
          description: |
            The VAT rate that currently applies to this subscription for its customer, after the plan's VAT setting and the customer's country, type and VAT validation have been resolved. `0` means no VAT is charged. Returns `null` when the subscription has no customer or legacy/malformed customer data prevents a safe calculation.
          example: 25
        pricing_type:
          type: string
          nullable: true
          description: |
            The pricing model used by the plan associated with this subscription.

            **Possible values:**
            - `flat_rate` - Standard fixed pricing per interval
            - `license_based` - Per-unit pricing where `standard_price = unit_price * quantity`
            - `usage_based` - Variable pricing based on actual consumption and billed in arrears. Use `unit_price` for the normalized base price per unit; `standard_price` is storage-compatible and is not a finalized usage total.
            - `tiered` - Price determined by tiers based on one or more parameters. The `standard_price` reflects the current tier's price. Tier changes mid-period generate proration lines.

            Returns `null` if the plan has no pricing type set.
          enum: [flat_rate, per_unit, license_based, usage_based, tiered]
          example: "flat_rate"
        quantity:
          type: integer
          nullable: true
          description: |
            Number of units for license-based subscriptions, or `0` for usage-based subscriptions (since quantity is variable).

            Only meaningful when `pricing_type` is `license_based` or `usage_based`. For flat-rate subscriptions, this will be `null` or `1`.
          example: 5
        unit_price:
          type: integer
          nullable: true
          description: |
            Per-unit price expressed in the smallest currency unit (e.g., cents, øre).

            - For `license_based`: total price = `unit_price * quantity`
            - For `usage_based`: this is the base price per unit of consumption, normalized across legacy and current storage. Total is calculated at period end based on actual usage. If a percentage `discount` is active, it is applied to the aggregate usage amount, so do not round a discounted per-unit price and multiply it yourself.
            - For `flat_rate`: this will be `null`.
          example: 10000
        currency:
          type: string
          description: |
            Currency code in ISO 4217 format (e.g., "DKK", "USD", "EUR").

            This determines the currency used for billing and invoicing for this subscription.
          example: "DKK"
        interval:
          type: integer
          description: |
            Billing interval in months. This determines how often the customer is billed.

            **Valid values:**
            - `1` - Monthly billing
            - `3` - Quarterly billing (every 3 months)
            - `4` - Every 4 months
            - `6` - Semi-annual billing (every 6 months)
            - `12` - Annual billing (every 12 months)

            The `standard_price` is charged once per interval period. For example, with `interval: 12` and `standard_price: 120000`, the customer is billed 120000 currency units once per year.
          enum: [1, 3, 4, 6, 12]
          example: 1
        start_date:
          type: string
          format: date-time
          nullable: true
          description: |
            Start date of the subscription period (ISO 8601 format).

            This is the date when the subscription becomes active and billing begins. If the date is in the future, the subscription will have a `status` of "pending" until this date is reached.

            The date is returned as an ISO 8601 date-time string with time set to 00:00:00 UTC.
          example: "2024-01-15T00:00:00.000000Z"
        end_date:
          type: string
          format: date-time
          nullable: true
          description: |
            End date of the subscription if it has been cancelled (ISO 8601 format).

            **Behavior:**
            - `null` - The subscription has no cancellation date set
            - Set to today or a future date - The subscription is "under_cancellation" and will end on this date
            - Set to a date before today - The subscription is "cancelled" and has ended

            When a subscription is cancelled, this field is set to the date when the subscription should end. A cancelled future-start subscription can have an `end_date` before its `start_date` when invoices have already been issued and the subscription therefore cannot be deleted.

            The date is returned as an ISO 8601 date-time string with time set to 23:59:59 UTC (end of day).
          example: null
        end_reason:
          type: string
          nullable: true
          description: |
            The reason why the subscription ended. Only set when `end_date` is set.

            **Possible values:**
            - `cancellation` - An admin cancelled the subscription via the dashboard or API
            - `customer_cancellation` - The customer cancelled the subscription via the self-service portal
            - `payment_failure` - The subscription ended because a payment charge failed (card declined or expired)
            - `membership_deleted` - An admin deleted a subscription that had not already ended. Deleting an already-ended subscription does not emit a second `subscription.ended` event.
            - `customer_deleted` - The subscription ended because the customer was deleted

            This field is `null` for active subscriptions or for historical subscriptions that were cancelled before this field was introduced.
          enum: [cancellation, customer_cancellation, payment_failure, membership_deleted, customer_deleted]
          example: null
        status:
          type: string
          description: |
            Current computed status of the subscription. This is automatically calculated based on `start_date` and `end_date` relative to the current date.

            **Status values:**
            - `pending` - The subscription has not started yet. `start_date` is in the future and `end_date` is `null`.
            - `active` - The subscription is currently active and billing. `start_date` has passed and `end_date` is `null`.
            - `under_cancellation` - The subscription has been cancelled but has not ended yet. `end_date` is today or in the future. This status can also apply to a future-start subscription when issued invoices prevent deletion.
            - `cancelled` - The subscription has ended. `end_date` is before today.

            **Note:** The status is computed dynamically and may change as dates pass.
          enum: [pending, active, under_cancellation, cancelled]
          example: "active"
        current_period_start:
          type: string
          format: date-time
          nullable: true
          description: |
            Start of the subscription's current billing window (ISO 8601).

            For active subscriptions, this is the `period_start` of the ongoing subscription transaction. For `pending` subscriptions, this is the `period_start` of the first upcoming subscription transaction. Returns `null` for `cancelled` subscriptions whose final period has elapsed.
          example: "2026-04-01T00:00:00.000000Z"
        current_period_end:
          type: string
          format: date-time
          nullable: true
          description: |
            End of the subscription's current billing window (ISO 8601).

            For active subscriptions, this is the `period_end` of the ongoing subscription transaction. For `pending` subscriptions, this is the `period_end` of the first upcoming subscription transaction. Returns `null` for `cancelled` subscriptions whose final period has elapsed.
          example: "2026-04-30T23:59:59.000000Z"
        plan:
          type: object
          description: |
            Plan information associated with this subscription.

            The plan defines the product/service the customer is subscribing to. Multiple subscriptions can reference the same plan with different intervals or pricing.
          properties:
            uuid:
              type: string
              format: uuid
              nullable: true
              description: Unique identifier (UUID) of the plan
              example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
            name:
              type: string
              nullable: true
              description: Display name of the plan (e.g., "Premium Plan", "Basic Plan")
              example: "Premium Plan"
          required:
            - uuid
            - name
        customer:
          type: object
          description: |
            Customer information for this subscription.

            The customer is the entity (company or individual) that owns this subscription. A customer can have multiple active subscriptions.
          properties:
            uuid:
              type: string
              format: uuid
              nullable: true
              description: Unique identifier (UUID) of the customer
              example: "456e7890-e89b-12d3-a456-426614174001"
            name:
              type: string
              nullable: true
              description: Display name of the customer (company name or individual's name)
              example: "Example Company"
            external_customer_id:
              type: string
              nullable: true
              description: |
                Stable identifier for this customer in the external system that created them. See the `Customer` schema for details.

                Note: this is the **customer's** external identifier. The top-level `external_customer_id` on the subscription object refers to the subscription's own external reference, which may be different.
              example: "12345"
          required:
            - uuid
            - name
            - external_customer_id
        metadata:
          type: object
          nullable: true
          description: |
            Custom key-value metadata attached to this subscription.

            This contains any non-reserved metadata keys that were passed in the checkout session's `metadata` object when the subscription was created. Can also be set directly when creating subscriptions programmatically.

            Subscriptions purchased as part of a bundle include a reserved `bundle_purchase` object with the original purchase context. Treat reserved keys as read-only.

            Returns `null` if no custom metadata was provided.
          properties:
            bundle_purchase:
              $ref: '#/components/schemas/BundlePurchaseMetadata'
          additionalProperties: true
          example:
            founding_tier: "gold"
            referral_code: "PARTNER_ABC"
            bundle_purchase:
              version: 1
              membership_bundle_uuid: "7b11626f-6ac5-407c-b6ee-96855b5f3b9a"
              bundle_uuid: "0a60f61e-7d8e-46f5-bc60-226f6a1f9c6d"
              bundle_name: "Premium bundle"
              bundle_slug: "premium-bundle"
              line_type: "subscription"
              plan_uuid: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
              plan_name: "Premium Plan"
              plan_slug: "premium-plan"
              pricing_type: "fixed"
              interval: 1
              currency: "DKK"
              standard_price: 19900
              quantity: 1
              unit_price: null
              start_date: "2026-06-19"
              source: "self_service"
              payment_provider: "invoice"
              checkout_session_id: null
        available_price_change_dates:
          type: array
          description: |
            The dates from which a permanent price change may take effect for this flat-rate or license-based subscription, as ISO 8601 date strings. These are the period starts of the still-unlocked (not yet invoiced or charged) billing periods - exactly the set accepted by the `effective_date` field of `PUT /subscriptions/{uuid}/price`.

            Only included when the subscription's transactions are loaded (the detail endpoint `GET /subscriptions/{uuid}` and the price-change response). Absent from the list endpoint.
          items:
            type: string
            format: date
          example: ["2026-06-01", "2026-07-01", "2026-08-01"]
        available_plan_change_dates:
          type: array
          description: |
            The renewal dates a plan change may take effect from, as ISO 8601 date strings, earliest first (at most 12). The first entry is the default used when `effective_date` is omitted from `PUT /subscriptions/{uuid}/plan`; the rest are the period starts of the later still-unlocked billing periods, excluding pause and trial periods. Empty when the subscription has no future billable period.

            Only included when the subscription's transactions are loaded - the detail endpoint `GET /subscriptions/{uuid}` and the write endpoints that return the full subscription. Absent from the list endpoint.
          items:
            type: string
            format: date
          example: ["2026-06-01", "2026-07-01", "2026-08-01"]
        discount:
          type: object
          nullable: true
          description: |
            The currently active discount on this subscription, if any.

            Returns `null` if no discount is currently active. A discount that has been applied but has since expired will also return `null`.

            Included when scheduled changes are loaded, including subscription list and detail responses and `subscription.*` webhook payloads.
          properties:
            name:
              type: string
              description: Name of the discount (e.g., "Summer Sale", "Loyalty Discount")
              example: "Summer Sale"
            value:
              type: number
              format: float
              description: |
                Discount amount. Interpretation depends on `type`:
                - For `percent`: percentage (0.1-100), may include up to 2 decimals, e.g. 12.5 means 12.5% off
                - For `fixed`: integer amount in smallest currency unit (e.g., cents, øre)
              example: 12.5
            type:
              type: string
              description: Type of discount
              enum: [percent, fixed]
              example: "percent"
            end_date:
              type: string
              format: date
              nullable: true
              description: Calendar date when the discount expires (YYYY-MM-DD). Null for indefinite discounts.
              example: "2024-09-01"
          required:
            - name
            - value
            - type
            - end_date
        scheduled_price_changes:
          type: array
          description: |
            Pending (not yet applied) scheduled price changes on this flat-rate or license-based subscription. These are price changes that were scheduled with a future `effective_date` via `PUT /subscriptions/{uuid}/price` and have not yet taken effect.

            Each entry can be undone with `DELETE /subscriptions/{uuid}/scheduled-changes/{scheduledChangeUuid}`. For temporary price changes only the start (`temporary_price_start`) is listed; deleting it also removes the corresponding end change.

            Included when scheduled changes are loaded, including subscription list and detail responses and `subscription.*` webhook payloads.
          items:
            type: object
            properties:
              uuid:
                type: string
                format: uuid
                description: UUID of the scheduled price change, used to delete it.
                example: "987e6543-e21b-12d3-a456-426614174000"
              change_type:
                type: string
                description: |
                  The type of price change.
                  - `permanent_price` - A permanent change to the standard price from `effective_date` onward.
                  - `temporary_price_start` - The start of a temporary price change for a date range.
                enum: [permanent_price, temporary_price_start]
                example: "permanent_price"
              effective_date:
                type: string
                format: date
                description: The date from which the price change takes effect (YYYY-MM-DD).
                example: "2026-07-01"
              new_price:
                type: integer
                nullable: true
                description: |
                  The scheduled total recurring price in the smallest currency unit (e.g., cents, øre).

                  For a license-based subscription this is derived from `new_unit_price` and the subscription's current `quantity`, matching what will actually be charged if the license count is unchanged when the change takes effect. For a usage-based subscription it is `0` - there is no fixed recurring component.
                example: 15000
              new_unit_price:
                type: integer
                nullable: true
                description: The scheduled per-license price in the smallest currency unit for a license-based subscription; null for flat-rate subscriptions.
                example: 1500
              currency:
                type: string
                description: Currency code in ISO 4217 format (matches the subscription's `currency`).
                example: "DKK"
            required:
              - uuid
              - change_type
              - effective_date
              - new_price
              - new_unit_price
              - currency
        scheduled_plan_change:
          type: object
          nullable: true
          description: |
            The pending plan change, if any. It can be cancelled with
            `DELETE /subscriptions/{uuid}/scheduled-changes/{uuid}`.
            A failed change stays visible with failure details and continues to
            block billing at its boundary until corrected, replaced, or cancelled.

            Included when scheduled changes are loaded, including subscription list and detail responses and `subscription.*` webhook payloads.
          properties:
            uuid:
              type: string
              format: uuid
            effective_date:
              type: string
              format: date
            source_plan:
              type: object
              properties:
                uuid:
                  type: string
                  format: uuid
                name:
                  type: string
            target_plan:
              type: object
              properties:
                uuid:
                  type: string
                  format: uuid
                name:
                  type: string
            interval:
              type: integer
            standard_price:
              type: integer
              nullable: true
              description: |
                Locked period price for a fixed-price target plan. For a tiered
                target plan this is an estimate based on the tier at scheduling
                time; the tier and price are recalculated when the change takes
                effect.
            currency:
              type: string
            status:
              type: string
              enum: [scheduled, failed]
            failure_code:
              type: string
              nullable: true
              enum: [plan_change_failed]
            failure_message:
              type: string
              nullable: true
              description: A generic client-safe explanation. Operational error details are not exposed.
        trial:
          type: object
          nullable: true
          description: |
            Trial period information for this subscription, if applicable.

            Only present when the subscription has (or had) a trial period. Returns `null` if no trial exists.
          properties:
            start_date:
              type: string
              format: date
              description: Start date of the trial period (YYYY-MM-DD)
              example: "2024-01-01"
            end_date:
              type: string
              format: date
              description: End date of the trial period (YYYY-MM-DD)
              example: "2024-01-14"
            is_active:
              type: boolean
              description: Whether the trial is currently active
              example: true
          required:
            - start_date
            - end_date
            - is_active
        custom_fields:
          type: array
          nullable: true
          description: |
            Custom field values attached to this subscription.

            Custom fields are defined on the plan and filled in when a subscription is created. They allow collecting additional structured data per subscription, such as a domain name, a reference number, or a date.

            Each item includes the field's unique key, display name, data type, and the value set for this subscription. Returns `null` if the custom fields module is not enabled or no fields are defined.
          items:
            type: object
            properties:
              key:
                type: string
                description: Unique machine-readable key for the field (derived from the field name)
                example: "website"
              name:
                type: string
                description: Human-readable display name of the field
                example: "Website"
              type:
                type: string
                description: |
                  Data type of the field.

                  **Possible values:**
                  - `string` - Free-form text
                  - `domain` - A domain name (e.g., example.com)
                  - `integer` - A whole number
                  - `date` - A date value
                enum: [string, domain, integer, date]
                example: "domain"
              value:
                type: string
                nullable: true
                description: The value set for this field on this subscription
                example: "example.com"
            required:
              - key
              - name
              - type
              - value
        tier:
          type: object
          nullable: true
          description: |
            The customer's current tier on this subscription.

            Only present when `pricing_type` is `tiered`. Returns `null` if no tier has been assigned yet.
          properties:
            uuid:
              type: string
              format: uuid
              description: Unique identifier of the tier
              example: "tier-uuid-abc-123"
            name:
              type: string
              description: Display name of the tier
              example: "Growth"
            position:
              type: integer
              description: Position of the tier (0 is lowest). Higher positions indicate higher tiers.
              example: 2
            is_free:
              type: boolean
              description: Whether this is a free tier (price = 0)
              example: false
          required:
            - uuid
            - name
            - position
            - is_free
        tier_locked:
          type: boolean
          description: |
            Whether the subscription's tier is locked to the current value.

            When locked, API calls to report tier parameters will still store the values but will not re-evaluate or change the tier. The lock is set automatically when an admin manually overrides the tier and can be cleared from the dashboard.

            Only present when `pricing_type` is `tiered`.
          example: false
      required:
        - uuid
        - standard_price
        - pricing_type
        - quantity
        - unit_price
        - currency
        - interval
        - start_date
        - end_date
        - end_reason
        - status
        - plan
        - customer
        - metadata
        - trial
        - custom_fields
    PaginatedSubscriptionsResponse:
      type: object
      description: Paginated response containing subscriptions
      properties:
        data:
          type: array
          description: Array of subscription objects
          items:
            $ref: '#/components/schemas/Subscription'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
              description: URL to the first page
            last:
              type: string
              format: uri
              nullable: true
              description: URL to the last page
            prev:
              type: string
              format: uri
              nullable: true
              description: URL to the previous page
            next:
              type: string
              format: uri
              nullable: true
              description: URL to the next page
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
              description: Current page number
            from:
              type: integer
              nullable: true
              description: Starting record number for current page
            last_page:
              type: integer
              description: Last page number
            path:
              type: string
              format: uri
              description: Base URL for pagination
            per_page:
              type: integer
              description: Number of items per page
            to:
              type: integer
              nullable: true
              description: Ending record number for current page
            total:
              type: integer
              description: Total number of items
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    WebhookPayload:
      type: object
      description: |
        Base structure for all webhook payloads. All webhooks follow this structure with event-specific data in the `data` field.
      properties:
        event:
          type: string
          description: The event type that triggered this webhook
          enum:
            - subscription.created
            - subscription.cancelled
            - subscription.resumed
            - subscription.ended
            - subscription.started
            - subscription.payment_succeeded
            - subscription.payment_failed
            - subscription.tier_changed
            - subscription.plan_changed
            - subscription.transferred
            - subscription_bundle.created
            - subscription_bundle.cancelled
            - customer.created
            - customer.updated
            - customer.deleted
            - invoice.created
            - invoice.paid
            - invoice.payment_failed
            - invoice.refunded
            - checkout.completed
            - customer.usage_recorded
        team_uuid:
          type: string
          format: uuid
          description: The Alunta team UUID that triggered the event. Use this to route the webhook to the correct account in your system when one integration receives events from many Alunta teams.
          example: "123e4567-e89b-12d3-a456-426614174001"
        test_mode:
          type: boolean
          description: Whether the sending team was in test mode when the event was created.
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 UTC timestamp for the webhook envelope. It can include microseconds and remains unchanged across automatic retries and manual replay. For most events it records dispatch time; event-specific documentation states when it preserves the domain event's occurrence time.
        webhook_uuid:
          type: string
          format: uuid
          description: Stable delivery UUID included for marketplace integrations. It remains unchanged across automatic retries and manual replay and should be used as the primary idempotency key.
        integration_settings:
          type: object
          additionalProperties: true
          description: Non-sensitive activation settings included for marketplace integrations.
        data:
          type: object
          description: |
            Event-specific data payload. The structure varies by event type:

            - **subscription.\*** events: Contains `subscription`, `plan`, `customer`, and `team` objects
            - **subscription.started**: Contains a stable `event_id` UUID. The lifecycle transition is claimed atomically before dispatch and delivery is at least once; deduplicate recovery replays by `event_id`.
            - **subscription.payment_succeeded**: Contains a stable `event_id` UUID plus `subscription`, `plan`, `payment`, `customer`, and `team` objects, and `external_customer_id` when configured. `plan` is null only for a legacy subscription that has no plan relationship or historical plan snapshot. Fires once per locally recorded Payment and affected subscription, regardless of payment rail. Delivery is at least once; deduplicate replays by `event_id`. A Payment can be partial or an installment, so this event confirms that the payment itself succeeded; it does not guarantee that a linked invoice has no remaining balance. Otherwise, `plan` identifies the earliest billing period covered by this payment and can differ from `subscription.plan` after a plan change.
            - **subscription.payment_failed**: Contains `subscription`, `plan`, `customer`, and `team` objects, and `external_customer_id` when configured. `subscription` is the subscription's current persisted state; `plan` identifies the plan attached to the failed billing period and can therefore differ from `subscription.plan` after a plan change. Fires when a subscription collection fails on a card or Leverandørservice, and when a prior subscription payment is charged back. You may receive it more than once for the same subscription across separate retry attempts. Cancellation, when it happens, is reported separately via `subscription.cancelled` / `subscription.ended`.
            - **subscription.ended**: Contains a stable `event_id` UUID and a top-level `reason` field indicating why the subscription ended (e.g., `cancellation`, `customer_cancellation`, `payment_failure`, `membership_deleted`, `customer_deleted`). Deleting a subscription emits `membership_deleted` only when the deletion itself ends it; deleting an already-ended subscription does not emit a second `subscription.ended`. The lifecycle transition is claimed atomically before dispatch and delivery is at least once; deduplicate recovery replays by `event_id`.
            - **subscription.tier_changed**: Also includes top-level `old_tier`, `new_tier`, and `is_upgrade` fields. Each tier object contains `uuid`, `name`, `position`, and `is_free`. `old_tier` is `null` when the subscription had no tier before. Fires for both immediate tier changes and scheduled tier changes once they take effect.
            - **subscription.plan_changed**: Fires when a scheduled plan change takes effect. Includes `old_plan`, `new_plan`, `effective_date`, old/new interval, and old/new standard price. Historical transactions and invoices remain attached to the old plan.
            - **subscription.transferred**: Contains `subscription`, `plan`, `from_customer`, `to_customer`, and `team` objects. Fires when a subscription is moved between customers on the same team. Invoices and payments issued before the transfer remain bound to `from_customer` and are not re-pointed.
            - **customer.\*** events: Contains `customer` and `team` objects
            - **invoice.created**: Contains `invoice`, `customer`, and `team` objects
            - **invoice.paid**: Contains a stable `event_id` UUID plus `invoice`, `payment`, `customer`, and `team` objects. Fires when a Payment first brings the invoice from an outstanding balance to a fully covered balance. The resulting status can be `paid`, `overpaid` or, when a related credit note exists, `credited`. This covers hosted online payment, auto-charge on a saved card, manual registration via the UI, and subscription payments. Delivery is at least once; deduplicate replays by `event_id`.
            - **invoice.payment_failed**: Contains `invoice`, `customer`, `team`, and an `error` object. Fires only when a gateway or payment rail confirms that a payment attempt on an existing invoice failed. Browser cancellation, local Leverandørservice preflight rejection, and chargeback do not emit this event.
            - **checkout.completed**: Fires for both subscription and one-off invoice checkouts. Always contains `customer`, `team`, a `type` discriminator (`subscription` or `one_off_invoice`), and a `metadata` object carrying any custom (non-reserved) keys you set on the originating checkout session - useful for round-tripping your own identifiers (e.g. `booking_id`). When a session is involved, `external_customer_id` is also present. The variant-specific fields are: for `subscription`, `subscription` and `plan` (and `deal` when the checkout originated from a deal); license-based subscriptions additionally expose the customer's final selection as root-level `quantity` and `subscription.quantity`; for `one_off_invoice`, `invoice` and `payment` (the latter is omitted when the customer chose bank transfer, in which case the invoice is left unpaid until reconciled). One-off invoice checkouts also continue to emit `invoice.created`; `invoice.paid` fires immediately for card payments and later (when reconciled) for bank transfer.
          properties:
            event_id:
              type: string
              format: uuid
              description: Stable deduplication key included for `invoice.paid`, `subscription.payment_succeeded`, `subscription.started`, and `subscription.ended` events. The same local transition keeps the same value across retries and recovery replays.
            external_customer_id:
              type: string
              description: Your customer identifier when configured and exposed by the event as a root-level routing field.
          additionalProperties: true
      required:
        - event
        - team_uuid
        - test_mode
        - timestamp
        - data
    SubscriptionTransaction:
      type: object
      description: |
        A subscription transaction record representing a billing period for a subscription.

        Transactions are created automatically based on the subscription's billing interval and define the price charged for a specific period. Each transaction covers a date range from `period_start` to `period_end`.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the transaction (UUID format)
          example: "123e4567-e89b-12d3-a456-426614174000"
        price:
          type: integer
          description: |
            Price for this transaction period, expressed in the smallest currency unit (e.g., cents, øre).

            This is the amount charged for the entire period covered by `period_start` and `period_end`.
          example: 99900
        currency:
          type: string
          nullable: true
          description: Currency code in ISO 4217 format (e.g., "DKK", "USD", "EUR")
          example: "DKK"
        period_start:
          type: string
          format: date-time
          nullable: true
          description: |
            Start date of the billing period covered by this transaction (ISO 8601 format).

            This is when the billing period begins. The date is returned as an ISO 8601 date-time string with time set to 00:00:00 UTC.
          example: "2024-01-15T00:00:00.000000Z"
        period_end:
          type: string
          format: date-time
          nullable: true
          description: |
            End date of the billing period covered by this transaction (ISO 8601 format).

            This is when the billing period ends. The date is returned as an ISO 8601 date-time string with time set to 00:00:00 UTC.
          example: "2024-02-14T00:00:00.000000Z"
        membership:
          type: object
          description: Subscription (membership) information
          properties:
            uuid:
              type: string
              format: uuid
              nullable: true
              description: Subscription UUID
              example: "123e4567-e89b-12d3-a456-426614174000"
          required:
            - uuid
        customer:
          type: object
          description: Customer information
          properties:
            uuid:
              type: string
              format: uuid
              nullable: true
              description: Customer UUID
              example: "456e7890-e89b-12d3-a456-426614174001"
            name:
              type: string
              nullable: true
              description: Customer name
              example: "Example Company"
          required:
            - uuid
            - name
        plan:
          type: object
          description: Plan information
          properties:
            uuid:
              type: string
              format: uuid
              nullable: true
              description: Plan UUID
              example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
            name:
              type: string
              nullable: true
              description: Plan name
              example: "Premium Plan"
          required:
            - uuid
            - name
        is_invoiced:
          type: boolean
          description: |
            Whether the whole service period has been invoiced.

            - `true` - An invoice has been created for this transaction
            - `false` - No invoice has been created yet for this transaction

            For a subscription paid in installments the period is never billed as a
            single document - it is `true` only once **every** installment slice has
            its own invoice. Use the `installments` array to see the per-slice state.
          example: false
        invoice:
          type: object
          nullable: true
          description: |
            The invoice this transaction has been billed on. `null` until the transaction is invoiced (i.e. while `is_invoiced` is `false`). Always `null` for an installment period - each slice has its own invoice, listed under `installments`.
          properties:
            uuid:
              type: string
              format: uuid
              description: Invoice UUID
              example: "789e0123-e89b-12d3-a456-426614174002"
            number:
              type: integer
              description: Human-facing invoice number assigned at creation time
              example: 1042
            paid_at:
              type: string
              format: date-time
              nullable: true
              description: When the invoice was marked paid (ISO 8601). `null` while the invoice is still outstanding.
              example: "2026-05-15T09:04:21.000000Z"
          required:
            - uuid
            - number
            - paid_at
        installments:
          type: array
          description: |
            Present only when the subscription is paid in installments (rater). One
            entry per payment slice, each with its own bill date and invoice. Omitted
            entirely for a normal (single-charge) transaction.
          items:
            type: object
            properties:
              number:
                type: integer
                description: This slice's position in the schedule (1-based)
                example: 1
              count:
                type: integer
                description: Total number of slices the period is split into
                example: 3
              amount:
                type: integer
                description: This slice's net amount in minor units (e.g. øre)
                example: 40000
              currency:
                type: string
                description: ISO 4217 currency code
                example: "DKK"
              bill_at:
                type: string
                format: date-time
                description: When this slice is invoiced/charged (ISO 8601)
                example: "2026-05-19T00:00:00.000000Z"
              is_invoiced:
                type: boolean
                description: Whether this slice has its own invoice yet
                example: true
              is_paid:
                type: boolean
                description: Whether this slice is paid (its invoice is paid, or it was charged directly on a card)
                example: false
              invoice:
                type: object
                nullable: true
                description: This slice's invoice. `null` until the slice is invoiced.
                properties:
                  uuid:
                    type: string
                    format: uuid
                    example: "789e0123-e89b-12d3-a456-426614174002"
                  number:
                    type: integer
                    example: 1042
                  paid_at:
                    type: string
                    format: date-time
                    nullable: true
                    example: "2026-05-15T09:04:21.000000Z"
                required:
                  - uuid
                  - number
                  - paid_at
            required:
              - number
              - count
              - amount
              - currency
              - bill_at
              - is_invoiced
              - is_paid
              - invoice
      required:
        - uuid
        - price
        - currency
        - period_start
        - period_end
        - membership
        - customer
        - plan
        - is_invoiced
    PaginatedSubscriptionTransactionsResponse:
      type: object
      description: Paginated response containing subscription transactions
      properties:
        data:
          type: array
          description: Array of subscription transaction objects
          items:
            $ref: '#/components/schemas/SubscriptionTransaction'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
              description: URL to the first page
            last:
              type: string
              format: uri
              nullable: true
              description: URL to the last page
            prev:
              type: string
              format: uri
              nullable: true
              description: URL to the previous page
            next:
              type: string
              format: uri
              nullable: true
              description: URL to the next page
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
              description: Current page number
            from:
              type: integer
              nullable: true
              description: Starting record number for current page
            last_page:
              type: integer
              description: Last page number
            path:
              type: string
              format: uri
              description: Base URL for pagination
            per_page:
              type: integer
              description: Number of items per page
            to:
              type: integer
              nullable: true
              description: Ending record number for current page
            total:
              type: integer
              description: Total number of items
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    EventType:
      type: string
      description: |
        The type of event. New types may be added over time, so consumers should treat this as an open-ended string.
      enum:
        - company_created_via_checkout
        - membership_created_via_checkout
        - membership_created_manual
        - membership_started
        - membership_cancelled
        - membership_ended
        - membership_renewed
        - invoice_generated
        - invoice_sent
        - webhook_failed
        - checkout_completed
        - subscription_charged
        - payment_created
        - ls_mandate_activated
        - ls_mandate_cancelled
        - ls_collection_completed
        - ls_collection_rejected
        - ls_collection_charged_back
        - ls_sftp_error
        - ls_pending_mandate_stale
        - ls_outgoing_delivery_stale
      example: "invoice_sent"
    Event:
      type: object
      description: |
        An event recorded in your team's audit trail. Events are emitted automatically when significant domain occurrences happen (e.g. a subscription starts, an invoice is generated, a payment is created, a webhook delivery fails).
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the event
          example: "abc12345-e89b-12d3-a456-426614174000"
        type:
          $ref: '#/components/schemas/EventType'
        metadata:
          type: object
          nullable: true
          description: |
            Free-form JSON metadata describing the event. The shape depends on the event `type`.
          additionalProperties: true
          example:
            invoice_number: 1042
            receiver_email: "billing@example.com"
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When the event was recorded (ISO 8601)
          example: "2026-05-08T09:04:21.000000Z"
        customer:
          type: object
          nullable: true
          description: The related customer, if the event is tied to one. `null` otherwise.
          properties:
            uuid:
              type: string
              format: uuid
              description: Customer UUID
              example: "456e7890-e89b-12d3-a456-426614174001"
          required:
            - uuid
        membership:
          type: object
          nullable: true
          description: The related subscription, if the event is tied to one. `null` otherwise.
          properties:
            uuid:
              type: string
              format: uuid
              description: Subscription UUID
              example: "123e4567-e89b-12d3-a456-426614174000"
          required:
            - uuid
        invoice:
          type: object
          nullable: true
          description: The related invoice, if the event is tied to one. `null` otherwise.
          properties:
            uuid:
              type: string
              format: uuid
              description: Invoice UUID
              example: "789e0123-e89b-12d3-a456-426614174002"
          required:
            - uuid
      required:
        - uuid
        - type
        - metadata
        - created_at
        - customer
        - membership
        - invoice
    PaginatedEventsResponse:
      type: object
      description: Paginated response containing events
      properties:
        data:
          type: array
          description: Array of event objects
          items:
            $ref: '#/components/schemas/Event'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
            last:
              type: string
              format: uri
              nullable: true
            prev:
              type: string
              format: uri
              nullable: true
            next:
              type: string
              format: uri
              nullable: true
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
            from:
              type: integer
              nullable: true
            last_page:
              type: integer
            path:
              type: string
              format: uri
            per_page:
              type: integer
            to:
              type: integer
              nullable: true
            total:
              type: integer
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    PlanRenewalInterval:
      type: object
      description: |
        A pricing interval for a plan, defining the price for a specific billing period.

        Plans can have multiple renewal intervals, allowing different pricing for monthly, quarterly, semi-annual, and annual billing periods.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for this renewal interval. Use this when creating subscriptions to specify which interval to use.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        interval:
          type: integer
          description: |
            Billing interval in months.

            **Valid values:**
            - `1` - Monthly billing
            - `3` - Quarterly billing (every 3 months)
            - `6` - Semi-annual billing (every 6 months)
            - `12` - Annual billing (every 12 months)
          enum: [1, 3, 6, 12]
          example: 1
        price:
          type: integer
          description: |
            Price for this interval, expressed in the smallest currency unit (e.g., cents, øre).

            This is the price charged once per interval period. For example, if `interval` is 12 (annual) and `price` is 120000, the customer pays 120000 currency units once per year.
          example: 99900
        currency:
          type: string
          nullable: true
          description: Currency code for this interval in ISO 4217 format (e.g., "DKK", "USD", "EUR")
          example: "DKK"
      required:
        - interval
        - price
        - currency
    PlanTierParameter:
      type: object
      description: |
        A parameter that helps determine which tier a customer belongs to.

        A tiered plan can have up to 3 parameters (e.g., "Number of users", "MRR", "Number of URLs"). When reporting tier parameters via the API, use the parameter's `key` as the object key.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for this parameter
          example: "param-abc-123"
        key:
          type: string
          description: Machine-readable key used when reporting tier parameters via the API
          example: "users"
        name:
          type: string
          description: Human-readable display name for the parameter
          example: "Number of users"
      required:
        - uuid
        - key
        - name
    PlanTierCondition:
      type: object
      description: |
        A range condition on a parameter that determines whether a tier matches a given parameter value.
      properties:
        parameter_key:
          type: string
          description: Key of the parameter this condition applies to
          example: "users"
        parameter_name:
          type: string
          description: Display name of the parameter
          example: "Number of users"
        min_value:
          type: integer
          nullable: true
          description: |
            Lower bound of the range (inclusive). A value must be greater than or equal to `min_value` to match.

            Auto-calculated from the previous tier's `max_value + 1`. The lowest tier always starts at 0.
          example: 11
        max_value:
          type: integer
          nullable: true
          description: |
            Upper bound of the range (inclusive). A value must be less than or equal to `max_value` to match.

            `null` means no upper limit ("unlimited"). Only the highest tier should have a null upper bound.
          example: 50
      required:
        - parameter_key
        - parameter_name
        - min_value
        - max_value
    PlanTierPrice:
      type: object
      description: |
        The price for a tier at a specific interval and currency.

        Each tier can have multiple prices (one per interval/currency combination).
      properties:
        interval:
          type: integer
          enum: [1, 3, 4, 6, 12]
          description: Billing interval in months
          example: 1
        price:
          type: integer
          description: Price in the smallest currency unit (e.g., cents, øre)
          example: 17900
        currency:
          type: string
          description: Currency code (ISO 4217, 3 characters)
          example: "DKK"
      required:
        - interval
        - price
        - currency
    PlanTier:
      type: object
      description: |
        A single tier within a tiered plan. Tiers are ordered by `position` (lowest first), and when a customer matches multiple tiers across parameters, the highest position wins.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for this tier
          example: "tier-abc-123"
        name:
          type: string
          description: Display name of the tier
          example: "Pro"
        position:
          type: integer
          description: |
            Position of the tier. `0` is the lowest tier. Higher positions indicate higher tiers.

            When a customer matches conditions on multiple tiers, the one with the highest position wins.
          example: 2
        is_free:
          type: boolean
          description: Whether this tier has a price of 0 (free)
          example: false
        conditions:
          type: array
          description: |
            The range conditions on parameters that determine whether this tier matches. A customer matches this tier when their parameter value falls within the condition's range.
          items:
            $ref: '#/components/schemas/PlanTierCondition'
        prices:
          type: array
          description: The prices for this tier, one per interval/currency combination.
          items:
            $ref: '#/components/schemas/PlanTierPrice'
      required:
        - uuid
        - name
        - position
        - is_free
        - conditions
        - prices
    TierSummary:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        position:
          type: integer
        is_free:
          type: boolean
      required:
        - uuid
        - name
        - position
        - is_free
    ScheduledTierChange:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        effective_date:
          type: string
          format: date
        source:
          type: string
          nullable: true
          enum: [manual_selection, usage_evaluation, override]
        target_tier_name:
          type: string
          nullable: true
          description: Name captured when the change was scheduled; remains available if the referenced tier can no longer be resolved
        target_tier:
          allOf:
            - $ref: '#/components/schemas/TierSummary'
          nullable: true
          description: Current tier details, or null when the referenced tier can no longer be resolved
      required:
        - uuid
        - effective_date
        - source
        - target_tier_name
        - target_tier
    TierEvaluationResponse:
      type: object
      description: |
        The result of reporting tier parameters. Indicates whether the tier changed and what action was taken.
      properties:
        current_tier:
          type: object
          nullable: true
          description: The tier the subscription is currently on after evaluation
          properties:
            uuid:
              type: string
              format: uuid
              example: "tier-abc-123"
            name:
              type: string
              example: "Pro"
            position:
              type: integer
              example: 2
            is_free:
              type: boolean
              example: false
        evaluated_tier:
          type: object
          description: |
            The tier the system evaluated based on the reported parameters. May differ from `current_tier` if the tier is locked or if the change is scheduled for a future period.
          properties:
            uuid:
              type: string
              format: uuid
              example: "tier-abc-123"
            name:
              type: string
              example: "Pro"
            position:
              type: integer
              example: 2
        tier_changed:
          type: boolean
          description: Legacy indicator that the evaluated tier differs and a change was applied, scheduled, or requires manual action. Use the explicit change fields for the actual outcome.
          example: true
        change_applied:
          type: boolean
          description: Whether the current tier was changed immediately by this request
          example: true
        change_scheduled:
          type: boolean
          description: Whether a tier change is currently scheduled after this request
          example: false
        manual_action_required:
          type: boolean
          description: Whether a lower eligible tier was detected but the plan requires an administrator to select it
          example: false
        scheduling_unavailable:
          type: boolean
          description: Whether a tier change was needed but no future unbilled period was available for scheduling
          example: false
        is_upgrade:
          type: boolean
          description: Whether the change is an upgrade (higher position)
          example: true
        is_downgrade:
          type: boolean
          description: Whether the change is a downgrade (lower position)
          example: false
        action:
          type: string
          description: |
            The action taken by the system:

            - `none` - No tier change was needed
            - `immediate` - The tier changed immediately. A proration adjustment is created for the remaining days, unless the period's discount leaves nothing owed for them
            - `scheduled` - The tier change was scheduled for the next billing period
            - `manual_required` - A downgrade was detected but the plan's `tier_downgrade_behavior` is `manual`, so no automatic change was made
            - `scheduling_unavailable` - A change was needed, but no future unbilled period was available for scheduling
            - `locked` - The subscription's tier is locked; parameters were stored but the tier was not changed
            - `plan_change_pending` - Parameters were stored, but an already scheduled plan change owns the future billing terms
            - `configuration_error` - Parameters were stored, but the evaluated tier has no price for the subscription's interval and currency; the subscription was not changed and `tier_changed` is `false`
          enum: [none, immediate, scheduled, manual_required, scheduling_unavailable, locked, plan_change_pending, configuration_error]
          example: "immediate"
        tier_locked:
          type: boolean
          description: Whether the subscription's tier is currently locked
          example: false
        parameters:
          type: object
          description: The current parameter values after the report, keyed by parameter key
          additionalProperties:
            type: object
            properties:
              value:
                type: integer
                example: 15
              reported_at:
                type: string
                format: date-time
                example: "2026-04-17T10:30:00+00:00"
      required:
        - current_tier
        - evaluated_tier
        - tier_changed
        - change_applied
        - change_scheduled
        - manual_action_required
        - scheduling_unavailable
        - is_upgrade
        - is_downgrade
        - action
        - tier_locked
        - parameters
    Plan:
      type: object
      description: |
        A plan record representing a subscription plan/product that customers can subscribe to.

        Plans define the product/service offering and can have multiple pricing intervals (monthly, quarterly, etc.). Each plan can be configured with different renewal behaviors and checkout availability.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the plan (UUID format)
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
        name:
          type: string
          nullable: true
          description: Display name of the plan
          example: "Premium Plan"
        slug:
          type: string
          nullable: true
          description: URL-friendly identifier for the plan (used in URLs and checkout)
          example: "premium-plan"
        description:
          type: string
          nullable: true
          description: Detailed description of the plan and its features
          example: "Our premium plan with all features included"
        available_in_checkout:
          type: boolean
          nullable: true
          description: |
            Whether this plan is available for purchase through the public checkout.

            - `true` - Plan is publicly available and can be purchased via checkout
            - `false` - Plan is only available for manual assignment (not shown in checkout)
          example: true
        pricing_type:
          type: string
          nullable: true
          description: |
            The pricing model for this plan.

            **Possible values:**
            - `flat_rate` - Standard fixed pricing per interval
            - `license_based` - Per-unit pricing where the total is calculated as `unit_price * quantity`
            - `usage_based` - Variable pricing based on actual consumption (billed in arrears)
            - `tiered` - Price determined by tiers based on one or more parameters. See `tier_parameters` and `tiers` for configuration.

            Returns `null` if no pricing type is set.
          enum: [flat_rate, per_unit, license_based, usage_based, tiered]
          example: "flat_rate"
        unit_name:
          type: string
          nullable: true
          description: |
            The display name of the usage parameter linked to this plan (e.g., "users", "seats", "API tokens", "SMS").

            Derived from the linked usage parameter's unit (or its name when no unit is set). Present for plans with `pricing_type` of `license_based` or `usage_based`. Returns `null` for flat-rate and tiered plans, and for plans that are not linked to a usage parameter.
          example: "users"
        renewal:
          type: string
          enum: [fixed, flexible]
          description: Renewal mode used by subscriptions on the plan.
          example: "flexible"
        auto_renewal:
          type: boolean
          description: Whether subscriptions on the plan renew automatically.
          example: true
        charge_vat:
          type: boolean
          description: Whether invoice lines generated from the plan include VAT.
          example: true
        payment_provider_restricted:
          type: boolean
          description: Whether the plan restricts subscriptions to the providers listed in `payment_providers`. When false, every payment provider enabled for the team is allowed.
          example: false
        payment_providers:
          type: array
          nullable: true
          description: |
            Payment methods accepted for subscriptions on this plan.

            `null` or an empty array means the plan is unrestricted and accepts every payment provider enabled for the team. When `payment_provider_restricted` is true, only the listed providers are accepted.
          items:
            type: string
            enum: [invoice, onpay, stripe, quickpay, manual]
          example: ["stripe", "invoice"]
        renewal_intervals:
          type: array
          nullable: true
          description: |
            Array of pricing intervals available for this plan.

            Each interval defines the price for a specific billing period (monthly, quarterly, etc.). Plans can have multiple intervals with different prices.

            **Note:** Empty for tiered plans — see `tiers` instead.
          items:
            $ref: '#/components/schemas/PlanRenewalInterval'
        tier_upgrade_behavior:
          type: string
          nullable: true
          description: |
            How tier upgrades are applied. Only present for tiered plans.

            **Possible values:**
            - `immediate` - Price changes immediately, with a prorated charge for the remaining days in the current billing period
            - `next_period` - Price changes at the start of the next billing period (scheduled change)
          enum: [immediate, next_period]
          example: "immediate"
        tier_downgrade_behavior:
          type: string
          nullable: true
          description: |
            How tier downgrades are applied. Only present for tiered plans.

            **Possible values:**
            - `manual` - Downgrades require manual action from an admin (no automatic price change)
            - `automatic_next_period` - Downgrade is scheduled to take effect at the start of the next billing period
          enum: [manual, automatic_next_period]
          example: "manual"
        tier_parameters:
          type: array
          nullable: true
          description: |
            The parameters that determine which tier a customer belongs to. Only present for tiered plans.

            Each plan can have up to 3 parameters (e.g., "Number of users", "MRR", "Number of URLs").
          items:
            $ref: '#/components/schemas/PlanTierParameter'
        tiers:
          type: array
          nullable: true
          description: |
            The configured tiers for this plan, ordered by position (lowest first). Only present for tiered plans.

            When a customer matches multiple tiers (across multiple parameters), the highest matching tier wins.
          items:
            $ref: '#/components/schemas/PlanTier'
        created_at:
          type: string
          format: date-time
          nullable: true
          description: Date and time when the plan was created (ISO 8601 format)
          example: "2024-01-15T10:30:00.000000Z"
      required:
        - uuid
        - name
        - slug
        - description
        - available_in_checkout
        - pricing_type
        - unit_name
        - renewal
        - auto_renewal
        - charge_vat
        - payment_provider_restricted
        - payment_providers
        - renewal_intervals
        - created_at
    CreatePlanRequest:
      type: object
      description: Request body for creating a new plan
      properties:
        name:
          type: string
          description: Display name of the plan
          example: "Boligafgift"
        description:
          type: string
          nullable: true
          description: Description of the plan
          example: "Månedlig boligafgift"
        amount:
          type: integer
          minimum: 1
          description: Price in the smallest currency unit (e.g., cents). 285000 = 2.850,00 kr.
          example: 285000
        currency:
          type: string
          description: Currency code in ISO 4217 format (3 characters)
          example: "DKK"
        interval:
          type: string
          description: |
            Billing interval as a string.

            **Valid values:**
            - `monthly` - Billed every month
            - `quarterly` - Billed every 3 months
            - `half-yearly` - Billed every 6 months
            - `yearly` - Billed every 12 months
          enum: [monthly, quarterly, half-yearly, yearly]
          example: "monthly"
        renewal:
          type: string
          nullable: true
          description: |
            Renewal mode for subscriptions on this plan. Defaults to `fixed` when omitted.

            - `fixed` - All subscribers renew on the same calendar day (e.g. the 1st of the month). New signups are aligned to the next renewal.
            - `flexible` - Each subscription renews based on its own start date. Different subscribers can have different renewal days.
          enum: [fixed, flexible]
          example: "fixed"
        charge_vat:
          type: boolean
          nullable: true
          description: |
            Whether invoices for this plan include VAT. Defaults to `true` when omitted. Set to `false` for VAT-exempt plans (e.g. memberships covered by a domestic VAT exemption); the plan's invoices will then be issued with `0%` VAT regardless of the customer's country.
          example: true
          default: true
        payment_providers:
          type: array
          nullable: true
          minItems: 1
          description: |
            Payment methods to enable on this plan. Each value must be a payment provider that the team has connected (e.g. Stripe, OnPay, QuickPay) - supplying a provider the team has not connected returns `422`.

            Omit the field or pass `null` to allow every payment provider enabled for the team. Pass an array to restrict the plan to the listed providers.
          items:
            type: string
            enum: [invoice, onpay, stripe, quickpay, manual]
          example: ["stripe", "invoice"]
        economic_product_group_number:
          type: integer
          nullable: true
          description: |
            The e-conomic product group number this plan should be saved under in e-conomic. Determines which product group the synced product is assigned to.

            **Required** when the team is connected to e-conomic - omitting it returns `422`. **Must be omitted** when the team is not connected to e-conomic - sending it for a non-connected team also returns `422` (so integrators don't accidentally store an e-conomic-specific value on a team that has no e-conomic integration).
          example: 1
      required:
        - name
        - amount
        - currency
        - interval
    CreateInvoiceRequest:
      type: object
      description: Request body for creating a one-time invoice
      properties:
        customer_uuid:
          type: string
          format: uuid
          description: UUID of the customer to create the invoice for
          example: "456e7890-e89b-12d3-a456-426614174001"
        currency:
          type: string
          description: "Three-letter currency code (ISO 4217). Supported currencies: DKK, EUR, GBP, USD, CAD, AUD, SEK, NOK, CHF, PLN. Case-insensitive."
          minLength: 3
          maxLength: 3
          example: "DKK"
        date:
          type: string
          format: date
          nullable: true
          description: Invoice date (YYYY-MM-DD). Defaults to today if not provided.
          example: "2025-06-15"
        note:
          type: string
          nullable: true
          description: Optional note to include on the invoice
          maxLength: 1000
          example: "Project payment for Q1 2025"
        lines:
          type: array
          description: Invoice line items (minimum 1, maximum 100)
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/CreateInvoiceLineRequest'
      required:
        - customer_uuid
        - currency
        - lines
    RefundInvoiceRequest:
      type: object
      description: Request body for reversing an invoice and refunding its payment
      properties:
        reason:
          type: string
          nullable: true
          description: Optional human-readable reason. Stored on the credit note's note field and on the refund record.
          maxLength: 1000
          example: "Booking cancelled by customer"
        refund_payment:
          type: boolean
          nullable: true
          description: |
            When `true` (the default), a full refund is initiated against the original payment provider before the credit note is created. When `false`, only the credit note is issued (use this when the refund has already been handled out-of-band).
          default: true
          example: true
    CreditInvoiceRequest:
      type: object
      description: Request body for crediting selected positive invoice lines
      properties:
        lines:
          type: array
          minItems: 1
          maxItems: 100
          description: Lines and amounts to credit. Each line UUID may appear only once.
          items:
            $ref: '#/components/schemas/CreditInvoiceLineRequest'
        reason:
          type: string
          nullable: true
          maxLength: 1000
          description: Optional human-readable reason stored on the credit note and any provider refund.
          example: "Partial period credit"
        refund_payment:
          type: boolean
          nullable: true
          default: false
          description: When `true`, refund the selected amount including VAT through the original payment provider before creating the credit note. Defaults to `false`.
          example: false
      required:
        - lines
    CreditInvoiceLineRequest:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
          description: UUID of a positive invoice line belonging to the invoice in the URL.
          example: "123e4567-e89b-12d3-a456-426614174003"
        amount_excl_vat:
          type: integer
          minimum: 1
          description: Amount to credit excluding VAT, expressed in the invoice currency's smallest unit. Must not exceed `remaining_creditable_amount`. When the original invoice uses `rate_group_rounding_v2`, the complete selection must also be representable under rate-group VAT rounding or the endpoint returns `422`; historical `line_rounding_v1` invoices retain cumulative per-line VAT allocation.
          example: 4000
      required:
        - uuid
        - amount_excl_vat
    MarkInvoiceAsPaidRequest:
      type: object
      description: Request body for registering a manual payment against an invoice. All fields are optional.
      properties:
        amount:
          type: integer
          nullable: true
          minimum: 1
          description: Payment amount in the smallest currency unit (e.g. cents/ore). Defaults to the invoice's outstanding balance.
          example: 124875
        date:
          type: string
          format: date
          nullable: true
          description: Date the payment was received (`YYYY-MM-DD`). Defaults to today.
          example: "2026-05-01"
        payment_method:
          type: string
          nullable: true
          maxLength: 255
          description: Code of an active payment method on the team (e.g. `bank_transfer`). When omitted, no payment method is associated with the payment.
          example: "bank_transfer"
        transaction_reference:
          type: string
          nullable: true
          maxLength: 255
          description: Free-form reference to the underlying transaction (e.g. a bank statement line ID).
          example: "BANKREF-2026-04812"
        memo:
          type: string
          nullable: true
          maxLength: 1000
          description: Optional human-readable memo stored on the payment. Falls back to a default referencing the invoice number.
          example: "Reconciled from April bank statement"
    PaymentRefund:
      type: object
      description: A provider refund, local payment reversal, or chargeback recorded against a payment.
      properties:
        uuid:
          type: string
          format: uuid
          example: "5b95c8b4-1a17-4c3a-9e5c-1b7c9b8a4d3f"
        amount:
          type: integer
          description: Refund amount in the smallest currency unit (e.g. cents/ore).
          example: 124875
        currency:
          type: string
          description: Three-letter currency code (ISO 4217).
          example: "DKK"
        kind:
          type: string
          description: Distinguishes money refunded by a provider from a local record reversal and a payment chargeback.
          enum: [provider_refund, local_reversal, chargeback]
          example: "provider_refund"
        status:
          type: string
          description: Lifecycle status of the refund.
          enum: [pending, completed, failed]
          example: "completed"
        provider:
          type: string
          description: Payment provider that processed the refund.
          enum: [invoice, onpay, stripe, quickpay, pensopay, vipps, manual, leverandorservice]
          example: "stripe"
        provider_refund_id:
          type: string
          nullable: true
          description: Identifier returned by the payment provider.
          example: "re_3OxyzABC123"
        reason:
          type: string
          nullable: true
          description: Reason supplied when the refund was issued.
          example: "Booking cancelled by customer"
        refunded_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp the refund was completed at the provider. `null` while pending.
          example: "2026-04-29T11:20:00Z"
        created_at:
          type: string
          format: date-time
          example: "2026-04-29T11:20:00Z"
        original_invoice_uuid:
          type: string
          format: uuid
          nullable: true
          description: Stable UUID snapshot of the original invoice whose payment was refunded or reversed. `null` only when the payment was never linked to an invoice or for a legacy refund whose invoice was deleted before snapshots were introduced.
          example: "123e4567-e89b-12d3-a456-426614174004"
      required:
        - uuid
        - amount
        - currency
        - kind
        - status
        - provider
        - provider_refund_id
        - reason
        - refunded_at
        - created_at
        - original_invoice_uuid
    CreateInvoiceLineRequest:
      type: object
      description: A single line item for a one-time invoice
      properties:
        text:
          type: string
          description: Description of the line item
          maxLength: 255
          example: "Consulting services"
        amount:
          type: integer
          description: Quantity (must be at least 1)
          minimum: 1
          example: 2
        price:
          type: integer
          description: "Unit price expressed in the smallest currency unit (e.g., cents, ore). For example, 100.00 DKK = 10000. Negative values are allowed for credit lines."
          example: 10000
        vat_rate:
          type: number
          format: float
          nullable: true
          description: "VAT percentage (0-100). If not provided, it will be automatically calculated based on the customer's country and VAT status."
          minimum: 0
          maximum: 100
          example: 25.5
        accounting_product_number:
          type: string
          nullable: true
          description: |
            Driver-specific external product identifier from the team's accounting integration. Optional.

            **Values per driver:**
            - **Dinero**: `ProductGuid` (UUID) from `GET https://api.dinero.dk/v1/{orgId}/products`
            - **e-conomic**: `productNumber`
            - **Billy**: `id`

            For e-conomic, the product must be active, not barred.

            This is *not* a chart-of-accounts number (e.g. "1000" / "1010"). To control which income account a line books on, set the sales account directly on the product in your accounting system.
          maxLength: 255
          example: "a1b2c3d4-5678-90ab-cdef-1234567890ab"
      required:
        - text
        - amount
        - price
    UpdatePlanRequest:
      type: object
      description: |
        Request body for updating a plan. All fields are optional — omit a field to leave it unchanged.
      properties:
        name:
          type: string
          maxLength: 255
          description: New plan name. Cannot be empty when provided.
          example: "Premium plan"
        description:
          type: string
          nullable: true
          description: New plan description. Pass `null` to clear.
          example: "Updated product description"
        payment_providers:
          type: array
          nullable: true
          minItems: 1
          description: |
            Replace the plan's payment methods. Each value must be a payment provider that the team has connected (e.g. Stripe, OnPay, QuickPay) - supplying a provider the team has not connected returns `422`.

            Omit the field to leave the existing restriction unchanged. Pass `null` to remove the restriction and allow every payment provider enabled for the team.
          items:
            type: string
            enum: [invoice, onpay, stripe, quickpay, manual]
          example: ["stripe", "invoice"]
    UpdatePlanRenewalIntervalRequest:
      type: object
      description: Request body for updating a plan renewal interval. Only the price can be changed.
      properties:
        price:
          type: integer
          minimum: 0
          description: New price for the renewal interval, in the smallest currency unit (e.g., øre/cents).
          example: 150000
      required:
        - price
    CreateCustomerInvoicesRequest:
      type: object
      description: |
        Request body for creating invoices from the customer's ready membership transactions. All fields are optional.
      properties:
        date:
          type: string
          format: date
          description: Invoice date (YYYY-MM-DD). Defaults to today.
          example: "2026-05-15"
    CreateSubscriptionRequest:
      type: object
      description: Request body for creating a new subscription
      properties:
        plan_uuid:
          type: string
          format: uuid
          description: UUID of the plan to subscribe to
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
        customer_uuid:
          type: string
          format: uuid
          description: UUID of the customer to create the subscription for
          example: "456e7890-e89b-12d3-a456-426614174001"
        renewal_interval_uuid:
          type: string
          format: uuid
          nullable: true
          description: |
            UUID of the plan's renewal interval to use for pricing and billing period. Get available intervals from the plan's `renewal_intervals` array.

            On a non-tiered plan send **either this or `interval`** - one of the two is required. This uuid identifies exactly one price row on its own, so `currency` is not needed alongside it.

            On a tiered plan it does not identify a price and is rejected with a 422 - tiered plans have no renewal intervals, so use `interval` (and `currency` where needed) instead.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        interval:
          type: integer
          nullable: true
          description: |
            Billing interval in months.

            On a non-tiered plan this is the alternative to `renewal_interval_uuid` - one of the two is required. On a tiered plan it is how a tier price is selected, because `renewal_interval_uuid` does not apply there.

            Pair it with `currency` when the interval alone still matches prices in more than one currency - see `currency`.

            **Valid values:** `1` (monthly), `3` (quarterly), `4` (every 4 months), `6` (semi-annual), `12` (annual).
          enum: [1, 3, 4, 6, 12]
          example: 1
        currency:
          type: string
          nullable: true
          description: |
            Currency code (ISO 4217, 3 characters). Must match a currency configured in the plan's tier prices (or, for a non-tiered plan, in its renewal intervals).

            Not needed when `renewal_interval_uuid` is sent, because that already identifies a single price row. Otherwise it is required **only when the selection would still be ambiguous**: a plan priced in one currency, or one where the given `interval` already narrows the candidates to a single currency, does not need it.

            The subscription's currency is taken from the matched price row, so when more than one candidate remains the request is rejected with a 422 naming the field to send, rather than booking an arbitrary currency onto the subscription.
          example: "DKK"
        quantity:
          type: integer
          minimum: 1
          description: Initial license count. Only supported for `license_based` plans; defaults to `1` when omitted.
          example: 7
        unit_price:
          type: integer
          minimum: 0
          description: Customer-specific per-license price in the smallest currency unit. Only supported for `license_based` plans; defaults to the selected renewal interval's price when omitted.
          example: 89700
        external_customer_id:
          type: string
          nullable: true
          description: Optional external identifier for tracking (e.g., location ID from your system)
          example: "location-42"
        external_subscription_id:
          type: string
          nullable: true
          description: Optional identifier to reference this specific subscription back to your own system (e.g., your internal subscription ID).
          example: "sub_abc123"
        name:
          type: string
          nullable: true
          description: Optional custom name for the subscription (defaults to plan name if not provided)
          example: "Andersenvej 12, 2. th."
        start_date:
          type: string
          format: date
          nullable: true
          description: Start date for the subscription (YYYY-MM-DD). Defaults to today if not provided.
          example: "2026-05-01"
        trial_end_date:
          type: string
          format: date
          nullable: true
          description: |
            Last day of a free trial period (YYYY-MM-DD, inclusive). When set, the subscription starts with a trial running from `start_date` through this date - no invoicing or charging happens during the trial, and regular billing begins the day after.

            An explicit `trial_end_date` overrides the plan's default trial configuration. Omit the field to fall back to the plan's default trial (if one is configured).

            **Constraints:**
            - Must be on or after `start_date` (equal to `start_date` gives a 1-day trial).
            - Must be within 365 days of `start_date`.
            - Not allowed for usage-based plans (returns a 422 error).
          example: "2026-05-15"
        custom_fields:
          type: object
          nullable: true
          description: |
            Custom field values to set on the subscription, as an object keyed by each field's public `key` (custom fields are defined per plan under the custom fields module).

            Requires the module to be enabled. Unknown keys are rejected with `422`, and each value is validated against the field's type (`string`, `domain`, `integer`, `date`). Send every value as a string - including integer and date fields (e.g. `"42"`, `"2026-06-01"`); values are also returned as strings. All custom fields are optional via the API (a field the team marks as required is only enforced in the dashboard). Values can be changed later via `PUT /subscriptions/{uuid}/custom-fields`.
          additionalProperties:
            type: string
            nullable: true
          example:
            license-key: "LIC-ABC-123"
        payment_provider:
          type: string
          nullable: true
          enum: [invoice, onpay, stripe, quickpay, pensopay, costplus, leverandorservice]
          description: |
            Optional. Omit to inherit the customer's existing payment method - their
            primary card if they have one, otherwise invoice - so the subscription
            bills the same way as the rest of their subscriptions. When given it must
            be a provider the team has enabled.

            Inheritance is not unconditional: the resolved provider (inherited or
            explicit) must be allowed by the plan, a card provider is only accepted
            when the customer has a matching card on file, and a card whose gateway is
            no longer connected is rejected. Any of these returns `422`.
          example: invoice
      required:
        - plan_uuid
        - customer_uuid
    BundlePurchaseMetadata:
      type: object
      description: Read-only purchase context stamped on subscriptions that were created as part of a bundle purchase.
      properties:
        version:
          type: integer
          example: 1
        backfilled:
          type: boolean
          description: Present and true when the object was reconstructed for a legacy purchase instead of captured at checkout time.
          example: true
        membership_bundle_uuid:
          type: string
          format: uuid
        bundle_uuid:
          type: string
          format: uuid
          nullable: true
        bundle_name:
          type: string
          nullable: true
        bundle_slug:
          type: string
          nullable: true
        line_type:
          type: string
          enum: [subscription]
        plan_uuid:
          type: string
          format: uuid
          nullable: true
        plan_name:
          type: string
          nullable: true
        plan_slug:
          type: string
          nullable: true
        pricing_type:
          type: string
          nullable: true
        interval:
          type: integer
        currency:
          type: string
          example: DKK
        standard_price:
          type: integer
          description: Price in the smallest currency unit.
        quantity:
          type: integer
          nullable: true
        unit_price:
          type: integer
          nullable: true
        start_date:
          type: string
          format: date
          nullable: true
        source:
          type: string
          nullable: true
        payment_provider:
          type: string
          nullable: true
        checkout_session_id:
          type: string
          nullable: true
      additionalProperties: true
    SubscriptionBundle:
      type: object
      description: |
        A bundle template that groups several existing plans plus optional one-off charges into a single product. A bundle instance for a customer is created via `POST /v1/customers/{uuid}/subscription-bundles`.
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        currency:
          type: string
          example: DKK
        available_in_checkout:
          type: boolean
          description: When true, the bundle can be purchased through the public self-service checkout.
        default_payment_provider:
          type: string
          nullable: true
        metadata:
          type: object
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        plans:
          type: array
          items:
            type: object
            properties:
              uuid:
                type: string
                format: uuid
              name:
                type: string
              sort_order:
                type: integer
        one_off_lines:
          type: array
          description: Pre-defined one-off charges added to the customer's first invoice when the bundle is activated.
          items:
            $ref: '#/components/schemas/BundleOneOffLine'
    BundleOneOffLine:
      type: object
      properties:
        text:
          type: string
          example: Opstartsgebyr
        description:
          type: string
          nullable: true
          maxLength: 255
          description: Optional supporting text shown below the invoice line title and included when the line is exported to the accounting system.
          example: Klargøring og introduktion
        amount:
          type: integer
          minimum: 1
          example: 1
        price:
          type: integer
          description: Unit price in cents.
          example: 50000
        accounting_product_number:
          type: string
          nullable: true
          description: |
            Driver-specific external product identifier from the team's accounting integration, used when the invoice line is pushed to the connected accounting system.

            **Values per driver:**
            - **Dinero**: `ProductGuid` (UUID) from `GET https://api.dinero.dk/v1/{orgId}/products`
            - **e-conomic**: `productNumber`
            - **Billy**: `id`

            For e-conomic, the product must be active, not barred.

            This is *not* a chart-of-accounts number (e.g. "1000" / "1010"). To control which income account a line books on, set the sales account directly on the product in your accounting system.
          example: "a1b2c3d4-5678-90ab-cdef-1234567890ab"
        sort_order:
          type: integer
    MembershipBundleInstance:
      type: object
      description: |
        A customer's purchased bundle. The bundle is a purchase/audit grouping, NOT a lifecycle container: each child subscription has its own independent lifecycle and is cancelled, resumed, transferred, and invoiced as an individual subscription after purchase.

        Do not assume a bundle still holds all - or any - of its original subscriptions. Children may have been cancelled, ended, transferred, or resumed one by one, so a bundle can have all, some, or none of its subscriptions still active. The bulk bundle operations (cancel, resume) act ONLY on the children currently in the relevant state, so they may be partial - or, if nothing is in that state, affect nothing.
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        type:
          type: string
          enum: [bundle_purchase]
        purchase_context:
          type: boolean
        status:
          type: string
          enum: [pending, active, under_cancellation, cancelled]
        started_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            Computed from the child subscriptions - the date the last remaining subscription
            ends, or null while any subscription is still open-ended. This reflects the bundle
            as a whole; to act on a particular subscription, read that subscription's own state
            rather than inferring it from the bundle-level `ends_at`.
        source:
          type: string
        metadata:
          type: object
          nullable: true
        bundle:
          type: object
          properties:
            uuid:
              type: string
              format: uuid
            name:
              type: string
            slug:
              type: string
            currency:
              type: string
        customer:
          type: object
          properties:
            uuid:
              type: string
              format: uuid
            name:
              type: string
            external_customer_id:
              type: string
              nullable: true
        memberships:
          type: array
          description: Child subscriptions - one per plan in the bundle.
          items:
            $ref: '#/components/schemas/Subscription'
    PaginatedMembershipBundleInstancesResponse:
      type: object
      description: Paginated response containing a customer's bundle purchases.
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/MembershipBundleInstance'
        links:
          type: object
          properties:
            first:
              type: string
              format: uri
              nullable: true
            last:
              type: string
              format: uri
              nullable: true
            prev:
              type: string
              format: uri
              nullable: true
            next:
              type: string
              format: uri
              nullable: true
          required: [first, last, prev, next]
        meta:
          type: object
          properties:
            current_page:
              type: integer
            from:
              type: integer
              nullable: true
            last_page:
              type: integer
            path:
              type: string
              format: uri
            per_page:
              type: integer
            to:
              type: integer
              nullable: true
            total:
              type: integer
          required: [current_page, from, last_page, path, per_page, to, total]
      required: [data, links, meta]
    CreateSubscriptionBundleRequest:
      type: object
      required:
        - name
        - currency
        - plan_uuids
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: string
          nullable: true
          maxLength: 5000
        slug:
          type: string
          nullable: true
          description: Auto-generated from name if omitted.
        currency:
          type: string
          minLength: 3
          maxLength: 3
        available_in_checkout:
          type: boolean
          default: false
        default_payment_provider:
          type: string
          nullable: true
        plan_uuids:
          type: array
          minItems: 1
          items:
            type: string
            format: uuid
        one_off_lines:
          type: array
          items:
            type: object
            required: [text, amount, price]
            properties:
              text:
                type: string
                maxLength: 255
              description:
                type: string
                nullable: true
                maxLength: 255
              amount:
                type: integer
                minimum: 1
              price:
                type: integer
                description: Unit price in cents.
              accounting_product_number:
                type: string
                nullable: true
                maxLength: 255
                description: |
                  Driver-specific external product identifier. Required if you want one-off lines to land on a specific product when synced to the team's accounting integration.

                  **Values per driver:**
                  - **Dinero**: `ProductGuid` (UUID) from `GET https://api.dinero.dk/v1/{orgId}/products`
                  - **e-conomic**: `productNumber`
                  - **Billy**: `id`

                  For e-conomic, the product must be active, not barred.

                  This is *not* a chart-of-accounts number. To control which income account a line books on, set the sales account directly on the product in your accounting system.
    UpdateSubscriptionBundleRequest:
      type: object
      description: All fields are optional. Provide `plan_uuids` or `one_off_lines` to replace the corresponding set entirely.
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: string
          nullable: true
          maxLength: 5000
        slug:
          type: string
        currency:
          type: string
          minLength: 3
          maxLength: 3
        available_in_checkout:
          type: boolean
        default_payment_provider:
          type: string
          nullable: true
        plan_uuids:
          type: array
          minItems: 1
          items:
            type: string
            format: uuid
        one_off_lines:
          type: array
          items:
            type: object
            required: [text, amount, price]
            properties:
              text:
                type: string
                maxLength: 255
              description:
                type: string
                nullable: true
                maxLength: 255
              amount:
                type: integer
                minimum: 1
              price:
                type: integer
              accounting_product_number:
                type: string
                nullable: true
                maxLength: 255
                description: |
                  Driver-specific external product identifier. Required if you want one-off lines to land on a specific product when synced to the team's accounting integration.

                  **Values per driver:**
                  - **Dinero**: `ProductGuid` (UUID) from `GET https://api.dinero.dk/v1/{orgId}/products`
                  - **e-conomic**: `productNumber`
                  - **Billy**: `id`

                  For e-conomic, the product must be active, not barred.

                  This is *not* a chart-of-accounts number. To control which income account a line books on, set the sales account directly on the product in your accounting system.
    CreateMembershipBundleRequest:
      type: object
      required:
        - bundle_uuid
        - start_date
        - interval
      properties:
        bundle_uuid:
          type: string
          format: uuid
        start_date:
          type: string
          format: date
        interval:
          type: integer
          enum: [1, 3, 4, 6, 12]
        payment_provider:
          type: string
          nullable: true
          enum: [invoice, onpay, stripe, quickpay, pensopay, costplus, leverandorservice]
          description: |
            Optional. Omit to inherit the customer's existing payment method - their
            primary card if they have one, otherwise invoice - so the bundle bills the
            same way as the rest of their subscriptions. When given it must be a
            provider the team has enabled.

            Inheritance is not unconditional: the resolved provider (inherited or
            explicit) must be allowed by the bundle, a card provider is only accepted
            when the customer has a matching card on file, and a card whose gateway is
            no longer connected is rejected. Any of these returns `422`.
          example: invoice
    PaginatedPlansResponse:
      type: object
      description: Paginated response containing plans
      properties:
        data:
          type: array
          description: Array of plan objects
          items:
            $ref: '#/components/schemas/Plan'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
              description: URL to the first page
            last:
              type: string
              format: uri
              nullable: true
              description: URL to the last page
            prev:
              type: string
              format: uri
              nullable: true
              description: URL to the previous page
            next:
              type: string
              format: uri
              nullable: true
              description: URL to the next page
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
              description: Current page number
            from:
              type: integer
              nullable: true
              description: Starting record number for current page
            last_page:
              type: integer
              description: Last page number
            path:
              type: string
              format: uri
              description: Base URL for pagination
            per_page:
              type: integer
              description: Number of items per page
            to:
              type: integer
              nullable: true
              description: Ending record number for current page
            total:
              type: integer
              description: Total number of items
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    Invoice:
      type: object
      description: |
        An invoice record representing a bill sent to a customer.

        Invoices contain line items, payment information, and billing details. They track the financial relationship between your team and customers.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the invoice (UUID format)
          example: "789e0123-e89b-12d3-a456-426614174002"
        number:
          type: integer
          description: |
            The invoice's current document number. It starts as Alunta's provisional sequential number. When the team publishes the accounting system's PDF, successful booking synchronization overwrites this field with the accounting document number, so it is not a stable identifier. When Alunta generates the PDF but is configured to display the accounting number, this field remains Alunta's number even though the customer-facing document can show a different number. Use `uuid` as the stable invoice identifier.
          example: 1001
        document_number:
          type: object
          description: Structured customer-facing document number state. Use this instead of inferring accounting synchronization from `number`.
          properties:
            value:
              type: string
              nullable: true
              description: Customer-facing number, or null while an accounting-owned number is pending.
              example: "8810"
            status:
              type: string
              enum: [pending, provisional, final]
              description: "`pending` awaits accounting, `provisional` is Alunta's legacy number without a frozen source, and `final` is frozen for customer use."
              example: "final"
            source:
              type: string
              enum: [alunta, accounting]
              description: System that owns the customer-facing document number.
              example: "accounting"
            driver:
              type: string
              nullable: true
              description: Accounting driver when the source is accounting.
              example: "dinero"
          required: [value, status, source, driver]
        date:
          type: string
          format: date-time
          nullable: true
          description: Date when the invoice was issued (ISO 8601 format)
          example: "2024-01-15T00:00:00.000000Z"
        due_date:
          type: string
          format: date
          description: Date payment is due (`YYYY-MM-DD`).
          example: "2024-02-14"
        currency:
          type: string
          nullable: true
          description: Currency code in ISO 4217 format (e.g., "DKK", "USD", "EUR")
          example: "DKK"
        total:
          type: integer
          description: |
            Total amount before VAT, expressed in the smallest currency unit (e.g., cents, øre).

            This is the sum of all line items before VAT is applied.
          example: 100000
        vat:
          type: integer
          description: |
            Total VAT amount, expressed in the smallest currency unit (e.g., cents, øre).

            This is the sum of VAT from all line items.
          example: 25000
        total_with_vat:
          type: integer
          description: |
            Total amount including VAT, expressed in the smallest currency unit (e.g., cents, øre).

            This is the final amount the customer needs to pay.
          example: 125000
        totals_calculation_method:
          type: string
          enum: [line_rounding_v1, rate_group_rounding_v2]
          description: Immutable VAT-rounding method used by this document and inherited by its credit notes. Documents priced with grouped VAT use `rate_group_rounding_v2`; documents whose amount was originally calculated without a grouped-method snapshot use `line_rounding_v1`, including invoices completed later from legacy pending checkouts. Use this field to determine whether partial-credit rate-group representability rules apply.
          example: rate_group_rounding_v2
        status:
          type: string
          nullable: true
          description: |
            Current status of the invoice.

            **Values:**
            - `issued` - Invoice was issued; no payment recorded yet.
            - `partially_paid` - Some payment recorded but balance is not yet zero.
            - `paid` - Invoice is fully paid (Payment records bring the outstanding balance to zero).
            - `overdue` - Past the due date with an outstanding balance.
            - `creditnote` - This invoice is itself a credit note (negative total).
            - `credited` - This invoice has been fully credited by a paired credit note.
            - `draft`, `overpaid` - Rare states; most integrations can ignore them.
          enum: [draft, issued, partially_paid, paid, overdue, overpaid, creditnote, credited]
          example: "issued"
        outstanding:
          type: integer
          minimum: 0
          description: Outstanding balance in the smallest currency unit after payments, completed refunds, and related credit notes.
          example: 125000
        paid_at:
          type: string
          format: date-time
          nullable: true
          description: |
            Timestamp when the invoice was fully paid (ISO 8601).

            Set automatically when `Payment` records bring the outstanding balance to zero - whether the payment came from hosted online checkout, an auto-charged saved card, a subscription completion, or a manual "mark as paid" action.

            **Important:** `paid_at` is reliable for teams whose end-customer payments flow through Alunta. It will be `null` for invoices that are paid out-of-band - e.g. teams that take payment by bank transfer and never register a `Payment` in Alunta. Do not assume `paid_at == null` means the invoice is unpaid; decide per Alunta team whether you trust this field or treat all non-credited invoices as paid out-of-band.
          example: "2024-01-16T08:41:00.000000Z"
        pay_url:
          type: string
          format: uri
          nullable: true
          description: |
            Hosted payment page URL where the customer can pay this invoice online. Only present when the team has an active payment gateway (Stripe, OnPay, QuickPay - or any future gateway), the invoice is `issued` or `overdue`, the balance is positive, and the invoice is not being collected automatically via live Leverandørservice. The customer is routed through whichever gateway the team has configured. Redirect or email this URL to the customer; upon successful payment the `invoice.paid` webhook fires.
          example: "https://app.alunta.com/pay-invoice/abc123def456"
        customer:
          type: object
          description: Customer information
          properties:
            uuid:
              type: string
              format: uuid
              nullable: true
              description: Customer UUID
              example: "456e7890-e89b-12d3-a456-426614174001"
            name:
              type: string
              nullable: true
              description: Customer name
              example: "Example Company"
          required:
            - uuid
            - name
        invoice_lines:
          type: array
          description: Array of invoice line items
          items:
            $ref: '#/components/schemas/InvoiceLine'
        created_at:
          type: string
          format: date-time
          nullable: true
          description: Date and time when the invoice was created (ISO 8601 format)
          example: "2024-01-15T10:30:00.000000Z"
      required:
        - uuid
        - number
        - document_number
        - date
        - due_date
        - currency
        - total
        - vat
        - total_with_vat
        - totals_calculation_method
        - status
        - outstanding
        - paid_at
        - pay_url
        - customer
        - invoice_lines
        - created_at
    InvoiceLine:
      type: object
      description: |
        An invoice line item representing a single chargeable item on an invoice.

        Invoice lines can represent subscription charges or one-time charges. Each line includes pricing, VAT, and optional reference to a subscription transaction.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the invoice line (UUID format)
          example: "789e0123-e89b-12d3-a456-426614174003"
        text:
          type: string
          description: Human-readable title of the line item.
          example: "Premium Plan - Monthly Subscription"
        description:
          type: string
          nullable: true
          description: Optional supporting text displayed below the line title and included in accounting exports.
          example: "Klargøring og introduktion"
        amount:
          type: integer
          description: |
            Line-level quantity. For subscription lines this is typically `1`; for one-off invoice lines this is the quantity passed at checkout. The line `total` (before VAT) is `amount * price`.
          example: 1
        price:
          type: integer
          description: |
            Unit price for this line item, expressed in the smallest currency unit (e.g., cents, øre). This is the authoritative per-unit price for the line - prefer `price` over `unit_price` (see `unit_price` notes).
          example: 99900
        vat_rate:
          type: number
          format: float
          description: VAT rate percentage applied to this line item
          example: 25.5
        total:
          type: integer
          description: |
            Total amount for this line item (amount × price), expressed in the smallest currency unit (e.g., cents, øre).

            This is the subtotal before VAT is applied.
          example: 99900
        remaining_creditable_amount:
          type: integer
          minimum: 0
          description: Remaining upper bound that may be credited from this line, excluding VAT and expressed in the smallest currency unit. It is capped by prior credits, the invoice's remaining net total and, for subscription-period lines, the subscription transaction's remaining amount. Negative lines return `0`. On `rate_group_rounding_v2` invoices, some intermediate selections below this bound can still return `422` when rate-group VAT cannot form both a standalone credit note and a representable remaining balance. Historical `line_rounding_v1` invoices retain cumulative per-line VAT allocation and skip that grouped representability check.
          example: 99900
        type:
          type: string
          description: |
            Structural type of this invoice line.

            **Values:**
            - `subscription` - Line is tied to a subscription transaction (recurring billing)
            - `one_time` - Ad-hoc line not linked to any subscription
            - `fee` - Auto-added fee line (e.g. payment-method fee). Follow `fee_uuid` to get the underlying fee configuration.

            Note: This is distinct from a plan's `pricing_type` (`flat_rate`,
            `per_unit`, `license_based`, `usage_based`, `tiered`). To classify
            revenue for MRR purposes, combine this field with the subscription's
            `pricing_type` (available by following `plan_uuid` or
            `subscription_uuid`). For example, `usage_based` subscription lines
            should typically be excluded from MRR since their revenue is variable.
            Fee lines (`type: fee`) are non-recurring and should typically be
            excluded from MRR.
          enum: [subscription, one_time, fee]
          example: subscription
        fee_uuid:
          type: string
          format: uuid
          nullable: true
          description: |
            UUID of the underlying fee configuration if this line was auto-added as a fee. Null for non-fee lines.
          example: null
        line_number:
          type: integer
          nullable: true
          description: Sequential line number for ordering line items on the invoice
          example: 1
        accounting_product_number:
          type: string
          nullable: true
          description: |
            Driver-specific external product identifier this line is tied to in the team's accounting integration. Echoes back the value supplied when the line was created.

            **Values per driver:**
            - **Dinero**: `ProductGuid` (UUID) from `GET https://api.dinero.dk/v1/{orgId}/products`
            - **e-conomic**: `productNumber`
            - **Billy**: `id`

            Null for lines created without an explicit product reference. This is *not* a chart-of-accounts number.
          example: "a1b2c3d4-5678-90ab-cdef-1234567890ab"
        subscription_transaction:
          type: object
          description: Subscription transaction information (only present for subscription-type lines)
          properties:
            uuid:
              type: string
              format: uuid
              nullable: true
              description: UUID of the subscription transaction this line item is linked to
              example: "123e4567-e89b-12d3-a456-426614174000"
          required:
            - uuid
        subscription_uuid:
          type: string
          format: uuid
          nullable: true
          description: UUID of the subscription this line item belongs to. Null for one-time charges.
          example: "123e4567-e89b-12d3-a456-426614174000"
        plan_uuid:
          type: string
          format: uuid
          nullable: true
          description: UUID of the plan associated with the subscription. Null for one-time charges.
          example: "2e1a995c-73b6-413f-9c23-3633dffdc94c"
        period_start:
          type: string
          format: date-time
          nullable: true
          description: Start date of the billing period this line covers (ISO 8601). Null for one-time charges.
          example: "2024-06-01T00:00:00.000000Z"
        period_end:
          type: string
          format: date-time
          nullable: true
          description: End date of the billing period this line covers (ISO 8601). Null for one-time charges.
          example: "2024-06-30T00:00:00.000000Z"
        unit_price:
          type: integer
          nullable: true
          description: |
            Reserved for future per-unit overrides on subscription transactions. Currently `null` on most lines - use `price` for the unit price.
          example: null
        quantity:
          type: integer
          nullable: true
          description: |
            The underlying subscription transaction's quantity, when the line came from a subscription. May be `null` for one-off lines. Distinct from `amount` (which is the line-level quantity).
          example: 2
        is_proration:
          type: boolean
          description: |
            True when this invoice line represents a proration adjustment.

            Proration lines are created in two scenarios:
            - Quantity changes on `license_based` subscriptions mid-period
              (e.g., adding or removing seats)
            - Tier changes on `tiered` subscriptions mid-period (when reported
              parameters cross a tier threshold)

            The line amount is calculated based on days remaining in the
            current billing period and can be positive (upgrade charge) or
            negative (credit for downgrade). A percentage discount on the period
            scales it; a fixed discount does not, being a one-off amount already
            applied to the period's own line. No line is created when nothing is
            owed.

            Plan switches, interval changes, and permanent price changes on
            other pricing types do not generate proration lines today.
          example: false
      required:
        - uuid
        - text
        - description
        - amount
        - price
        - vat_rate
        - total
        - remaining_creditable_amount
        - type
        - fee_uuid
        - line_number
        - accounting_product_number
        - subscription_transaction
        - subscription_uuid
        - plan_uuid
        - period_start
        - period_end
        - unit_price
        - quantity
        - is_proration
    PaginatedInvoicesResponse:
      type: object
      description: Paginated response containing invoices
      properties:
        data:
          type: array
          description: Array of invoice objects
          items:
            $ref: '#/components/schemas/Invoice'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
              description: URL to the first page
            last:
              type: string
              format: uri
              nullable: true
              description: URL to the last page
            prev:
              type: string
              format: uri
              nullable: true
              description: URL to the previous page
            next:
              type: string
              format: uri
              nullable: true
              description: URL to the next page
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
              description: Current page number
            from:
              type: integer
              nullable: true
              description: Starting record number for current page
            last_page:
              type: integer
              description: Last page number
            path:
              type: string
              format: uri
              description: Base URL for pagination
            per_page:
              type: integer
              description: Number of items per page
            to:
              type: integer
              nullable: true
              description: Ending record number for current page
            total:
              type: integer
              description: Total number of items
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    Payment:
      type: object
      description: |
        A payment record representing money collected from a customer.

        A payment is either linked to a specific invoice (when it settles or partially settles that invoice) or standalone (e.g. a manual payment not yet applied to an invoice).

        All monetary fields are in the smallest currency unit (e.g. ore for DKK, cents for EUR).
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the payment (UUID format)
          example: "b3c4d5e6-f7a8-9012-bcde-f12345678901"
        price:
          type: integer
          description: Total payment amount including VAT, in the smallest currency unit.
          example: 125000
        price_excluding_vat:
          type: integer
          description: Payment amount excluding VAT, in the smallest currency unit.
          example: 100000
        vat_rate:
          type: number
          format: float
          description: VAT rate percentage applied to the payment.
          example: 25.5
        currency:
          type: string
          nullable: true
          description: Currency code in ISO 4217 format (e.g., "DKK", "USD", "EUR").
          example: "DKK"
        date:
          type: string
          format: date-time
          nullable: true
          description: Date the payment was recorded (ISO 8601).
          example: "2024-01-16T00:00:00.000000Z"
        payment_provider:
          type: string
          nullable: true
          description: |
            The provider that processed the payment.

            **Values:**
            - `stripe` - Collected via Stripe
            - `onpay` - Collected via OnPay
            - `quickpay` - Collected via QuickPay
            - `pensopay` - Collected via PensoPay
            - `vipps` - Collected via Vipps / MobilePay
            - `leverandorservice` - Collected via Leverandørservice
            - `invoice` - Recorded as a bank transfer against an invoice
            - `manual` - Recorded manually (cash, other)
          enum: [invoice, onpay, stripe, quickpay, pensopay, vipps, manual, leverandorservice]
          example: stripe
        provider_transaction_id:
          type: string
          nullable: true
          description: |
            The provider's identifier for this transaction.

            For Stripe this is the Payment Intent or Charge id; for OnPay/QuickPay it is the transaction id returned by that gateway.
          example: "pi_3OZ0aB2eZvKYlo2C0K3aBcDe"
        provider_charge_id:
          type: string
          nullable: true
          description: |
            Provider-specific charge identifier, when distinct from `provider_transaction_id` (e.g. QuickPay exposes a separate operation/charge id per capture).
          example: "1234567"
        memo:
          type: string
          nullable: true
          description: Free-form note attached to the payment, typically used on manual payments.
          example: "Paid by bank transfer, ref. 12345"
        method:
          type: string
          nullable: true
          description: |
            Human-readable label for how the payment was made.

            For manual payments this is the name of the linked `PaymentMethod` (e.g. "Bankoverførsel", "MobilePay") when one is set, falling back to the provider's default label. For gateway payments this is the provider's localized display name (e.g. "Credit card").
          example: "Credit card"
        customer:
          type: object
          nullable: true
          description: Customer the payment belongs to. Null if the payment is not linked to a customer.
          properties:
            uuid:
              type: string
              format: uuid
              description: Customer UUID
              example: "456e7890-e89b-12d3-a456-426614174001"
            name:
              type: string
              nullable: true
              description: Customer name
              example: "Example Company"
          required:
            - uuid
            - name
        invoice:
          type: object
          nullable: true
          description: Invoice this payment settles. Null for standalone payments not linked to an invoice.
          properties:
            uuid:
              type: string
              format: uuid
              description: Invoice UUID
              example: "789e0123-e89b-12d3-a456-426614174002"
            number:
              type: integer
              nullable: true
              description: Invoice number
              example: 1001
          required:
            - uuid
            - number
        created_at:
          type: string
          format: date-time
          nullable: true
          description: Date and time when the payment was created in Alunta (ISO 8601).
          example: "2024-01-16T08:41:00.000000Z"
      required:
        - uuid
        - price
        - price_excluding_vat
        - vat_rate
        - currency
        - date
        - payment_provider
        - provider_transaction_id
        - provider_charge_id
        - memo
        - method
        - customer
        - invoice
        - created_at
    PaginatedPaymentsResponse:
      type: object
      description: Paginated response containing payments
      properties:
        data:
          type: array
          description: Array of payment objects
          items:
            $ref: '#/components/schemas/Payment'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
              description: URL to the first page
            last:
              type: string
              format: uri
              nullable: true
              description: URL to the last page
            prev:
              type: string
              format: uri
              nullable: true
              description: URL to the previous page
            next:
              type: string
              format: uri
              nullable: true
              description: URL to the next page
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
              description: Current page number
            from:
              type: integer
              nullable: true
              description: Starting record number for current page
            last_page:
              type: integer
              description: Last page number
            path:
              type: string
              format: uri
              description: Base URL for pagination
            per_page:
              type: integer
              description: Number of items per page
            to:
              type: integer
              nullable: true
              description: Ending record number for current page
            total:
              type: integer
              description: Total number of items
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    Customer:
      type: object
      description: |
        A customer record representing a company or individual that can have subscriptions and invoices.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the customer (UUID format)
          example: "456e7890-e89b-12d3-a456-426614174001"
        name:
          type: string
          nullable: true
          description: Customer name (company name or individual's name)
          example: "Example Company"
        email:
          type: string
          nullable: true
          description: Email address of the customer
          example: "contact@example.com"
        phone:
          type: string
          nullable: true
          description: Phone number of the customer
          example: "12345678"
        address:
          type: string
          nullable: true
          description: Street address
          example: "Main Street 123"
        zip:
          type: string
          nullable: true
          description: Postal/ZIP code
          example: "2100"
        city:
          type: string
          nullable: true
          description: City name
          example: "Copenhagen"
        country:
          type: string
          nullable: true
          description: Country code (2-letter ISO code)
          example: "DK"
        reg_number:
          type: string
          nullable: true
          description: |
            Registration/VAT number. Typically used for company customers.

            For companies, this is usually the VAT registration number or company registration number.
          example: "12345678"
        customer_type:
          type: string
          nullable: true
          description: |
            Type of customer.

            **Values:**
            - `company` - Business/company customer
            - `individual` - Individual/person customer
          enum: [company, individual]
          example: "company"
        external_customer_id:
          type: string
          nullable: true
          description: |
            Stable identifier for this customer in the external system that created them (e.g., via a checkout session).

            Populated from the `external_customer_id` passed when creating a checkout session. Unique per team when not `null`, so you can rely on it to resolve back to the same Alunta customer across requests. `null` for customers that were not created via an external checkout flow.
          example: "12345"
        website:
          type: string
          nullable: true
          description: Website URL of the customer
          example: "https://example.com"
        ean:
          type: string
          nullable: true
          pattern: '^\d{13}$'
          description: |
            Recipient EAN/GLN used as the electronic invoice endpoint. When supplied, it must be exactly 13 digits with a valid GS1 check digit. A customer using EAN delivery may omit this only when `reg_number` contains a valid Danish CVR.
          example: "5790000000005"
        invoice_delivery_method:
          type: string
          enum: [email, ean]
          description: |
            How new invoices are normally delivered to this customer. `ean` causes eligible invoices to be sent through EAN/NemHandel when the team's EAN integration is active; otherwise delivery falls back according to Alunta's delivery safeguards.
          example: "ean"
        email_invoice_when_ean:
          type: boolean
          description: Whether an invoice delivered through EAN/NemHandel should also be sent by email.
          example: false
        their_reference:
          type: string
          nullable: true
          maxLength: 255
          description: The buyer's general reference. Snapshotted onto invoices and mapped to `cbc:BuyerReference` for electronic invoices.
          example: "PROPERTY-61"
        order_reference:
          type: string
          nullable: true
          maxLength: 255
          description: Purchase order or requisition number for EAN delivery. Snapshotted onto invoices and mapped to `cac:OrderReference/cbc:ID`.
          example: "PO-12345"
        payer_uuid:
          type: string
          format: uuid
          nullable: true
          description: |
            UUID of the customer that pays on this customer's behalf (its reseller/payer), or `null` when this customer pays for itself.

            When set, each of this customer's subscriptions is billed either to the payer (consolidated onto the payer's invoice) or to this customer directly, configured per subscription in the app; by default every subscription is paid by the payer. Set or clear the payer via `POST /customers/{uuid}/payer`. Only included on the single, list, and set-payer customer endpoints.
          example: "789e0123-e89b-12d3-a456-426614174099"
        payer_name:
          type: string
          nullable: true
          description: Name of the payer/reseller customer, or `null` when this customer has no payer. Only included on the single, list, and set-payer customer endpoints.
          example: "Reseller A/S"
        has_valid_payment_card:
          type: boolean
          description: |
            Whether the customer has at least one non-expired payment card registered.

            Use this to determine the appropriate call-to-action:
            - `true` — the customer has a valid payment card on file
            - `false` — no valid payment card exists (expired cards are excluded)
          example: true
        archived:
          type: boolean
          description: |
            Whether the customer has been archived (soft-deleted). Archived customers are hidden from list and detail responses by default — pass `?include_archived=true` to fetch them.

            The `external_customer_id` of an archived customer remains reserved on the team. Sending it to `POST /customers` restores that customer with the same UUID; use `archived` together with `archived_at` to inspect its state from the API.
          example: false
        archived_at:
          type: string
          format: date-time
          nullable: true
          description: ISO 8601 timestamp of when the customer was archived. `null` for live customers.
          example: null
        next_billing_date:
          type: string
          format: date
          nullable: true
          description: |
            Earliest upcoming billing-period start across the customer's own non-cancelled subscriptions, as an ISO 8601 date (`YYYY-MM-DD`). Returns `null` when no future subscription period is projected.

            This is a renewal/billing-period date, not the timestamp when an invoice will be created or sent. Invoice timing may differ because of the team's invoicing offset, the subscription's payment method, usage billed in arrears, or installments.
          example: "2026-09-01"
        active_subscriptions:
          type: array
          description: |
            List of currently active subscriptions for this customer. Included on both GET /customers and GET /customers/{uuid}.

            A subscription is considered active if its start_date has passed and it has no end_date or the end_date is in the future.
          items:
            type: object
            properties:
              uuid:
                type: string
                format: uuid
                description: Unique identifier for the subscription
              name:
                type: string
                nullable: true
                description: Custom name of the subscription, or null if using the plan name
              plan_uuid:
                type: string
                format: uuid
                nullable: true
                description: UUID of the plan
              plan_name:
                type: string
                nullable: true
                description: Name of the plan
              status:
                type: string
                enum: [pending, active, under_cancellation, cancelled]
                description: Current status of the subscription
              currency:
                type: string
                description: Currency code (ISO 4217)
              interval:
                type: integer
                description: Billing interval in months
              standard_price:
                type: integer
                description: Price per interval in smallest currency unit
        mrr_by_currency:
          type: object
          description: |
            Current monthly recurring revenue (MRR) for subscriptions that are billing now, grouped by currency. Included on both GET /customers and GET /customers/{uuid}.

            Run-rate MRR includes permanent discounts but excludes time-limited discounts. Annual and other multi-month prices are divided by their interval to produce a monthly value. Usage-based subscriptions have no fixed recurring component and contribute zero.

            Each key is an ISO 4217 currency code (e.g., "DKK", "EUR"), and each value is in the smallest currency unit (e.g., øre or cents). A missing currency key means zero. An empty result is returned as `{}`.
          additionalProperties:
            type: number
          example:
            DKK: 419000
        pending_mrr_by_currency:
          type: object
          description: |
            Monthly recurring revenue committed but not billing now because the subscription has a future start date, is in an active trial, or is actively paused. Included on both GET /customers and GET /customers/{uuid}.

            The value uses the billing terms at the subscription's first billable date. Like `mrr_by_currency`, this is run-rate MRR: permanent discounts are included, while time-limited discounts are excluded. Each value is in the smallest currency unit, a missing currency key means zero, and an empty result is returned as `{}`.
          additionalProperties:
            type: number
          example:
            DKK: 419000
        effective_mrr_by_currency:
          type: object
          description: |
            Current monthly recurring revenue for subscriptions that are billing now, including any time-limited discount active today. This corresponds to the "MRR incl. discounts" value in the customer CSV export.

            Each key is an ISO 4217 currency code, and each value is in the smallest currency unit. A missing currency key means zero, and an empty result is returned as `{}`.
          additionalProperties:
            type: number
          example:
            DKK: 104750
        effective_pending_mrr_by_currency:
          type: object
          description: |
            Monthly recurring revenue committed but not billing now, including any time-limited discount active on the subscription's first billable date. This corresponds to the "Pending MRR incl. discounts" value in the customer CSV export.

            Each key is an ISO 4217 currency code, and each value is in the smallest currency unit. A missing currency key means zero, and an empty result is returned as `{}`.
          additionalProperties:
            type: number
          example:
            DKK: 104750
        custom_fields:
          type: array
          description: |
            The customer's populated custom field values.

            Custom fields are defined team-wide under the custom fields module. Only fields that have a value set on this customer are returned, so this is an empty array when none are populated (it does not list defined-but-empty fields). Each item includes the field's unique key, display name, data type, and the value. Discover the full set of available field keys in the custom fields settings.
          items:
            type: object
            properties:
              key:
                type: string
                description: Unique machine-readable key for the field (derived from the field name)
                example: "vat-eu-number"
              name:
                type: string
                description: Human-readable display name of the field
                example: "EU VAT number"
              type:
                type: string
                description: |
                  Data type of the field.

                  **Possible values:**
                  - `string` - Free-form text
                  - `domain` - A domain name (e.g., example.com)
                  - `integer` - A whole number
                  - `date` - A date value
                enum: [string, domain, integer, date]
                example: "string"
              value:
                type: string
                nullable: true
                description: The value set for this field on this customer
                example: "DK12345678"
            required:
              - key
              - name
              - type
              - value
        created_at:
          type: string
          format: date-time
          nullable: true
          description: Date and time when the customer was created (ISO 8601 format)
          example: "2024-01-15T10:30:00.000000Z"
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: Date and time when the customer was last updated (ISO 8601 format)
          example: "2024-01-20T14:30:00.000000Z"
      required:
        - uuid
        - name
        - email
        - phone
        - address
        - zip
        - city
        - country
        - reg_number
        - customer_type
        - external_customer_id
        - website
        - ean
        - invoice_delivery_method
        - email_invoice_when_ean
        - their_reference
        - order_reference
        - has_valid_payment_card
        - next_billing_date
        - created_at
        - updated_at
    CreateCustomerRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          maxLength: 255
          description: Customer name (company name or individual's name)
          example: "Acme Corp"
        email:
          type: string
          format: email
          maxLength: 255
          nullable: true
          description: Email address of the customer
          example: "info@acme.com"
        phone:
          type: string
          maxLength: 255
          nullable: true
          description: Phone number of the customer
          example: "+4512345678"
        address:
          type: string
          maxLength: 255
          nullable: true
          description: Street address
          example: "Main Street 1"
        zip:
          type: string
          maxLength: 255
          nullable: true
          description: Postal/ZIP code
          example: "1000"
        city:
          type: string
          maxLength: 255
          nullable: true
          description: City name
          example: "Copenhagen"
        country:
          type: string
          maxLength: 255
          nullable: true
          description: "Country code as an uppercase ISO 3166-1 alpha-2 code (e.g. `DK`); lowercase is rejected. Defaults to the team's country if not provided."
          example: "DK"
        reg_number:
          type: string
          maxLength: 255
          nullable: true
          description: Registration/VAT number
          example: "DK12345678"
        customer_type:
          type: string
          enum: [company, individual]
          nullable: true
          description: "Type of customer. Defaults to `company` if not provided."
          example: "company"
        website:
          type: string
          maxLength: 255
          nullable: true
          description: Website URL
          example: "https://acme.com"
        ean:
          type: string
          nullable: true
          pattern: '^\d{13}$'
          description: |
            Recipient EAN/GLN. Must have a valid GS1 check digit. Required together with `invoice_delivery_method: ean` unless `reg_number` is a valid Danish CVR.
          example: "5790000000005"
        invoice_delivery_method:
          type: string
          nullable: true
          enum: [email, ean]
          default: email
          description: |
            Invoice delivery channel. Defaults to `email`. Selecting `ean` requires a valid `ean`/GLN or Danish CVR in `reg_number`.
          example: "ean"
        email_invoice_when_ean:
          type: boolean
          nullable: true
          default: false
          description: Send an email copy in addition to EAN/NemHandel delivery. Can only be enabled when `invoice_delivery_method` is `ean`.
          example: false
        their_reference:
          type: string
          maxLength: 255
          nullable: true
          description: The buyer's general reference, mapped to `cbc:BuyerReference` on electronic invoices.
          example: "PROPERTY-61"
        order_reference:
          type: string
          maxLength: 255
          nullable: true
          description: Purchase order or requisition number, mapped to `cac:OrderReference/cbc:ID`. Only accepted for EAN delivery.
          example: "PO-12345"
        external_customer_id:
          type: string
          maxLength: 255
          nullable: true
          description: |
            Stable identifier for this customer in your own system.

            When set, this enables idempotent lookups: subsequent calls that reference the same `external_customer_id` (for example, when creating a checkout session or resolving the customer via `GET /customers?external_customer_id=`) will resolve back to this customer.

            Must be unique per team when not `null`.
          example: "12345"
        custom_fields:
          type: object
          nullable: true
          description: |
            Custom field values to set on the customer, as an object keyed by each field's public `key` (find the available keys in the custom fields settings).

            Requires the custom fields module to be enabled. Unknown keys are rejected with `422`, and each value is validated against the field's type (`string`, `domain`, `integer`, `date`). Send every value as a string - including integer and date fields (e.g. `"42"`, `"2026-06-01"`); values are also returned as strings. All custom fields are optional via the API (a field the team marks as required is only enforced in the dashboard).
          additionalProperties:
            type: string
            nullable: true
          example:
            vat-eu-number: "DK12345678"
    UpdateCustomerRequest:
      type: object
      description: |
        Partial update of a customer. Include only the fields you want to change; omitted fields are left untouched. `external_customer_id` cannot be changed here - it is the stable identifier set once at creation.
      properties:
        name:
          type: string
          maxLength: 255
          description: Customer name (company name or individual's name)
          example: "Acme Corp"
        email:
          type: string
          format: email
          maxLength: 255
          nullable: true
          description: Email address of the customer
          example: "info@acme.com"
        phone:
          type: string
          maxLength: 255
          nullable: true
          description: Phone number of the customer
          example: "+4512345678"
        address:
          type: string
          maxLength: 255
          nullable: true
          description: Street address
          example: "New Street 2"
        zip:
          type: string
          maxLength: 255
          nullable: true
          description: Postal/ZIP code
          example: "8000"
        city:
          type: string
          maxLength: 255
          nullable: true
          description: City name
          example: "Aarhus"
        country:
          type: string
          maxLength: 2
          description: "Country code as an uppercase ISO 3166-1 alpha-2 code (e.g. `DK`). Lowercase is rejected. Changing it clears prior VAT validation."
          example: "DK"
        reg_number:
          type: string
          maxLength: 255
          nullable: true
          description: "Registration/VAT number. Changing it clears prior VAT validation and triggers re-validation."
          example: "DK87654321"
        customer_type:
          type: string
          enum: [company, individual]
          description: "Type of customer. Changing it clears prior VAT validation."
          example: "company"
        website:
          type: string
          maxLength: 255
          nullable: true
          description: Website URL
          example: "https://acme.com"
        ean:
          type: string
          nullable: true
          pattern: '^\d{13}$'
          description: |
            Recipient EAN/GLN. Must have a valid GS1 check digit. An EAN-delivery customer must retain either this field or a valid Danish CVR in `reg_number`.
          example: "5790000000005"
        invoice_delivery_method:
          type: string
          enum: [email, ean]
          description: |
            Invoice delivery channel. Selecting `ean` requires a valid `ean`/GLN or Danish CVR. Switching to `email` clears `order_reference` and disables `email_invoice_when_ean`.
          example: "ean"
        email_invoice_when_ean:
          type: boolean
          description: Send an email copy in addition to EAN/NemHandel delivery. Can only be enabled for EAN delivery.
          example: true
        their_reference:
          type: string
          maxLength: 255
          nullable: true
          description: The buyer's general reference, mapped to `cbc:BuyerReference` on electronic invoices.
          example: "PROPERTY-61"
        order_reference:
          type: string
          maxLength: 255
          nullable: true
          description: Purchase order or requisition number, mapped to `cac:OrderReference/cbc:ID`. Only accepted for EAN delivery.
          example: "PO-12345"
        custom_fields:
          type: object
          nullable: true
          description: |
            Custom field values to set on the customer, as an object keyed by each field's public `key` (find the available keys in the custom fields settings).

            Requires the custom fields module to be enabled. Unknown keys are rejected with `422`; each value is validated against the field's type. Send every value as a string - including integer and date fields (e.g. `"42"`, `"2026-06-01"`); values are also returned as strings. Only the keys you include are changed. Pass an empty string or `null` to clear a field. A PATCH that only changes `custom_fields` is valid. Updating only custom fields does not change the customer's accounting/VAT sync and does not emit a `customer.updated` webhook.
          additionalProperties:
            type: string
            nullable: true
          example:
            vat-eu-number: "DK12345678"
    SetCustomerPayerRequest:
      type: object
      required:
        - payer_uuid
      properties:
        payer_uuid:
          type: string
          format: uuid
          nullable: true
          description: |
            UUID of the customer that should pay on this customer's behalf (its reseller/payer). The payer must be another customer on the same team.

            Pass `null` to clear the payer.
          example: "789e0123-e89b-12d3-a456-426614174099"
    CreatePortalLinkRequest:
      type: object
      properties:
        expires_in_minutes:
          type: integer
          minimum: 1
          maximum: 1440
          default: 15
          nullable: true
          description: |
            How long the link should be valid, in minutes.
            Default: 15 minutes. Maximum: 1440 minutes (24 hours).
          example: 15
    PortalLinkResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            url:
              type: string
              format: uri
              description: The portal URL. Either a signed auto-login link (when a valid customer UUID was provided) or the standard portal login page.
              example: "https://app.example.com/portal/my-team/verify/123?expires=1709470500&signature=abc123"
            expires_at:
              type: string
              format: date-time
              nullable: true
              description: When the auto-login link expires (ISO 8601 format). Only present when a valid customer UUID was provided.
              example: "2026-03-03T12:15:00.000000Z"
          required:
            - url
      required:
        - data
    PaginatedCustomersResponse:
      type: object
      description: Paginated response containing customers
      properties:
        data:
          type: array
          description: Array of customer objects
          items:
            $ref: '#/components/schemas/Customer'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
              description: URL to the first page
            last:
              type: string
              format: uri
              nullable: true
              description: URL to the last page
            prev:
              type: string
              format: uri
              nullable: true
              description: URL to the previous page
            next:
              type: string
              format: uri
              nullable: true
              description: URL to the next page
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
              description: Current page number
            from:
              type: integer
              nullable: true
              description: Starting record number for current page
            last_page:
              type: integer
              description: Last page number
            path:
              type: string
              format: uri
              description: Base URL for pagination
            per_page:
              type: integer
              description: Number of items per page
            to:
              type: integer
              nullable: true
              description: Ending record number for current page
            total:
              type: integer
              description: Total number of items
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    BankAccount:
      type: object
      description: A bank account used for displaying payment information on invoices.
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the bank account
          example: "550e8400-e29b-41d4-a716-446655440000"
        label:
          type: string
          nullable: true
          description: Custom label for the bank account
          example: "Main Account"
        currency_code:
          type: string
          description: Currency code (3-letter ISO 4217)
          example: "DKK"
        country_code:
          type: string
          description: Country code (2-letter ISO 3166-1)
          example: "DK"
        account_holder_name:
          type: string
          description: Name of the account holder
          example: "Acme Corp"
        bank_name:
          type: string
          nullable: true
          description: Name of the bank
          example: "Danske Bank"
        iban:
          type: string
          nullable: true
          description: International Bank Account Number
          example: "DK5000400440116243"
        swift_bic:
          type: string
          nullable: true
          description: SWIFT/BIC code
          example: "DABADKKK"
        local_details:
          type: object
          nullable: true
          description: |
            Country-specific bank account details. Structure varies by country:
            - **DK/NO:** `reg_nr` (registration number), `account_nr` (account number)
            - **GB:** `sort_code`, `account_number`
            - **US:** `routing_number`, `account_number`
            - **SE:** `clearing_nr`, `account_nr`
          example:
            reg_nr: "0040"
            account_nr: "0440116243"
        is_default_for_currency:
          type: boolean
          description: Whether this is the default bank account for its currency. Only one bank account per currency per team can be the default.
          example: true
        is_active:
          type: boolean
          description: Whether the bank account is active
          example: true
        created_at:
          type: string
          format: date-time
          nullable: true
          description: Date and time when the bank account was created (ISO 8601 format)
          example: "2024-01-15T10:30:00.000000Z"
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: Date and time when the bank account was last updated (ISO 8601 format)
          example: "2024-01-20T14:30:00.000000Z"
      required:
        - uuid
        - currency_code
        - country_code
        - account_holder_name
        - is_default_for_currency
        - is_active
        - created_at
        - updated_at
    CreateBankAccountRequest:
      type: object
      required:
        - currency_code
        - country_code
        - account_holder_name
      properties:
        label:
          type: string
          nullable: true
          description: Custom label for the bank account
          example: "Main Account"
        currency_code:
          type: string
          minLength: 3
          maxLength: 3
          description: Currency code (3-letter ISO 4217)
          example: "DKK"
        country_code:
          type: string
          minLength: 2
          maxLength: 2
          description: Country code (2-letter ISO 3166-1). Must be a valid country code.
          example: "DK"
        account_holder_name:
          type: string
          maxLength: 255
          description: Name of the account holder
          example: "Acme Corp"
        bank_name:
          type: string
          nullable: true
          maxLength: 255
          description: Name of the bank
          example: "Danske Bank"
        iban:
          type: string
          nullable: true
          maxLength: 34
          description: International Bank Account Number
          example: "DK5000400440116243"
        swift_bic:
          type: string
          nullable: true
          maxLength: 11
          description: SWIFT/BIC code
          example: "DABADKKK"
        local_details:
          type: object
          nullable: true
          description: Country-specific bank account details (e.g., registration number, account number)
          example:
            reg_nr: "0040"
            account_nr: "0440116243"
        is_default_for_currency:
          type: boolean
          nullable: true
          description: Set as default bank account for this currency (default false)
          example: true
        is_active:
          type: boolean
          nullable: true
          description: Whether the bank account is active (default true)
          example: true
    UpdateBankAccountRequest:
      type: object
      description: All fields are optional. Only provided fields will be updated.
      properties:
        label:
          type: string
          nullable: true
          description: Custom label for the bank account
          example: "Updated Label"
        currency_code:
          type: string
          minLength: 3
          maxLength: 3
          description: Currency code (3-letter ISO 4217)
          example: "EUR"
        country_code:
          type: string
          minLength: 2
          maxLength: 2
          description: Country code (2-letter ISO 3166-1)
          example: "DK"
        account_holder_name:
          type: string
          maxLength: 255
          description: Name of the account holder
          example: "New Corp Name"
        bank_name:
          type: string
          nullable: true
          maxLength: 255
          description: Name of the bank
          example: "Nordea"
        iban:
          type: string
          nullable: true
          maxLength: 34
          description: International Bank Account Number
          example: "DK5000400440116243"
        swift_bic:
          type: string
          nullable: true
          maxLength: 11
          description: SWIFT/BIC code
          example: "DABADKKK"
        local_details:
          type: object
          nullable: true
          description: Country-specific bank account details
          example:
            reg_nr: "0040"
            account_nr: "0440116243"
        is_default_for_currency:
          type: boolean
          description: Set as default bank account for this currency
          example: true
        is_active:
          type: boolean
          description: Whether the bank account is active
          example: true
    PaginatedBankAccountsResponse:
      type: object
      description: Paginated response containing bank accounts
      properties:
        data:
          type: array
          description: Array of bank account objects
          items:
            $ref: '#/components/schemas/BankAccount'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
              description: URL to the first page
            last:
              type: string
              format: uri
              nullable: true
              description: URL to the last page
            prev:
              type: string
              format: uri
              nullable: true
              description: URL to the previous page
            next:
              type: string
              format: uri
              nullable: true
              description: URL to the next page
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
              description: Current page number
            from:
              type: integer
              nullable: true
              description: Starting record number for current page
            last_page:
              type: integer
              description: Last page number
            path:
              type: string
              format: uri
              description: Base URL for pagination
            per_page:
              type: integer
              description: Number of items per page
            to:
              type: integer
              nullable: true
              description: Ending record number for current page
            total:
              type: integer
              description: Total number of items
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
    PaginatedCustomerUsageRecordsResponse:
      type: object
      description: Paginated response containing customer usage records
      properties:
        data:
          type: array
          description: Array of customer usage record objects
          items:
            $ref: '#/components/schemas/CustomerUsageRecord'
        links:
          type: object
          description: Pagination links
          properties:
            first:
              type: string
              format: uri
              nullable: true
              description: URL to the first page
            last:
              type: string
              format: uri
              nullable: true
              description: URL to the last page
            prev:
              type: string
              format: uri
              nullable: true
              description: URL to the previous page
            next:
              type: string
              format: uri
              nullable: true
              description: URL to the next page
          required:
            - first
            - last
            - prev
            - next
        meta:
          type: object
          description: Pagination metadata
          properties:
            current_page:
              type: integer
              description: Current page number
            from:
              type: integer
              nullable: true
              description: Starting record number for current page
            last_page:
              type: integer
              description: Last page number
            path:
              type: string
              format: uri
              description: Base URL for pagination
            per_page:
              type: integer
              description: Number of items per page
            to:
              type: integer
              nullable: true
              description: Ending record number for current page
            total:
              type: integer
              description: Total number of items
          required:
            - current_page
            - from
            - last_page
            - path
            - per_page
            - to
            - total
      required:
        - data
        - links
        - meta
  responses:
    UnauthorizedError:
      description: Authentication credentials were not provided or are invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Unauthenticated.
    PaymentRequiredError:
      description: The Team's free-tier payment deadline has passed and management access is restricted
      content:
        application/json:
          schema:
            type: object
            required: [message, code, account_closes_at]
            properties:
              message:
                type: string
              code:
                type: string
                enum: [team_payment_required]
              account_closes_at:
                type: string
                format: date-time
          example:
            message: Payment is required to restore management access to this team.
            code: team_payment_required
            account_closes_at: '2026-10-10T12:00:00Z'
    ForbiddenError:
      description: Access forbidden - API module may not be enabled or user lacks permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: API module is not enabled for this team.
webhooks:
  '{your-webhook-url}':
    post:
      summary: Webhook endpoint
      description: |
        Configure webhook URLs in your settings to receive real-time notifications about events in your account.
        Webhooks are sent via HTTP POST requests to your configured URL.

        ## Secret Key Verification

        We strongly recommend enabling webhook secret key verification to ensure webhook requests are authentic.
        When enabled, all webhook requests include a `Signature` header containing an HMAC-SHA256 signature of the request body.

        To verify a webhook signature:

        1. Get the signature from the `Signature` header
        2. Get the raw JSON payload from the request body
        3. Calculate the expected signature using HMAC-SHA256: `hash_hmac('sha256', $payloadJson, $secretKey)`
        4. Compare signatures using a constant-time comparison function (e.g., `hash_equals()` in PHP) to prevent timing attacks

        **Important:** Always use the raw request body (before JSON parsing) for signature verification.

        Example verification code:
        ```php
        $signature = $request->header('Signature');
        $payloadJson = $request->getContent();
        $expectedSignature = hash_hmac('sha256', $payloadJson, $secretKey);

        if (hash_equals($expectedSignature, $signature)) {
            // Signature is valid - process webhook
        } else {
            // Signature is invalid - reject request
            abort(401, 'Invalid webhook signature');
        }
        ```
      operationId: webhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookPayload'
            examples:
              subscriptionCreated:
                summary: subscription.created
                value:
                  event: subscription.created
                  timestamp: "2024-01-15T10:30:00Z"
                  data:
                    subscription:
                      uuid: "123e4567-e89b-12d3-a456-426614174000"
                      standard_price: 99900
                      currency: "DKK"
                      interval: "monthly"
                      start_date: "2024-01-15T00:00:00Z"
                      end_date: "2025-01-15T23:59:59Z"
                    plan:
                      uuid: "123e4567-e89b-12d3-a456-426614174001"
                      name: "Premium Plan"
                      slug: "premium-plan"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                      phone: "12345678"
                      address: "Example Street 123"
                      zip: "1234"
                      city: "Copenhagen"
                      country: "DK"
                      reg_number: "12345678"
                    team:
                      slug: "my-team"
              subscriptionCancelled:
                summary: subscription.cancelled
                value:
                  event: subscription.cancelled
                  timestamp: "2024-01-20T14:30:00Z"
                  data:
                    subscription:
                      uuid: "123e4567-e89b-12d3-a456-426614174000"
                      standard_price: 99900
                      currency: "DKK"
                      interval: "monthly"
                      start_date: "2024-01-15T00:00:00Z"
                      end_date: "2025-01-15T23:59:59Z"
                    plan:
                      uuid: "123e4567-e89b-12d3-a456-426614174001"
                      name: "Premium Plan"
                      slug: "premium-plan"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    team:
                      slug: "my-team"
              subscriptionResumed:
                summary: subscription.resumed
                value:
                  event: subscription.resumed
                  timestamp: "2024-02-01T09:15:00Z"
                  data:
                    subscription:
                      uuid: "123e4567-e89b-12d3-a456-426614174000"
                      standard_price: 99900
                      currency: "DKK"
                      interval: "monthly"
                      start_date: "2024-01-15T00:00:00Z"
                      end_date: null
                    plan:
                      uuid: "123e4567-e89b-12d3-a456-426614174001"
                      name: "Premium Plan"
                      slug: "premium-plan"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    team:
                      slug: "my-team"
              subscriptionEnded:
                summary: subscription.ended
                value:
                  event: subscription.ended
                  team_uuid: "123e4567-e89b-12d3-a456-426614174010"
                  test_mode: false
                  timestamp: "2025-01-15T23:59:59Z"
                  data:
                    event_id: "223e4567-e89b-12d3-a456-426614174022"
                    subscription:
                      uuid: "123e4567-e89b-12d3-a456-426614174000"
                      standard_price: 99900
                      currency: "DKK"
                      interval: "monthly"
                      start_date: "2024-01-15T00:00:00Z"
                      end_date: "2025-01-15T23:59:59Z"
                      end_reason: "payment_failure"
                    plan:
                      uuid: "123e4567-e89b-12d3-a456-426614174001"
                      name: "Premium Plan"
                      slug: "premium-plan"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    team:
                      slug: "my-team"
                    reason: "payment_failure"
              subscriptionStarted:
                summary: subscription.started
                value:
                  event: subscription.started
                  team_uuid: "123e4567-e89b-12d3-a456-426614174010"
                  test_mode: false
                  timestamp: "2024-01-15T10:30:00Z"
                  data:
                    event_id: "323e4567-e89b-12d3-a456-426614174033"
                    subscription:
                      uuid: "123e4567-e89b-12d3-a456-426614174000"
                      standard_price: 99900
                      currency: "DKK"
                      interval: "monthly"
                      start_date: "2024-01-15T00:00:00Z"
                      end_date: "2025-01-15T23:59:59Z"
                    plan:
                      uuid: "123e4567-e89b-12d3-a456-426614174001"
                      name: "Premium Plan"
                      slug: "premium-plan"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    team:
                      slug: "my-team"
              subscriptionPaymentFailed:
                summary: subscription.payment_failed
                value:
                  event: subscription.payment_failed
                  team_uuid: "123e4567-e89b-12d3-a456-426614174010"
                  test_mode: false
                  timestamp: "2024-03-01T08:00:00Z"
                  data:
                    subscription:
                      uuid: "123e4567-e89b-12d3-a456-426614174000"
                      standard_price: 99900
                      currency: "DKK"
                      interval: "monthly"
                      start_date: "2024-01-15T00:00:00Z"
                      end_date: "2024-03-01T00:00:00Z"
                    plan:
                      uuid: "123e4567-e89b-12d3-a456-426614174001"
                      name: "Premium Plan"
                      slug: "premium-plan"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                      external_customer_id: "customer_12345"
                    external_customer_id: "customer_12345"
                    team:
                      slug: "my-team"
              subscriptionPaymentSucceeded:
                summary: subscription.payment_succeeded
                value:
                  event: subscription.payment_succeeded
                  team_uuid: "123e4567-e89b-12d3-a456-426614174010"
                  test_mode: false
                  timestamp: "2024-03-02T08:00:00Z"
                  data:
                    event_id: "123e4567-e89b-12d3-a456-426614174099"
                    subscription:
                      uuid: "123e4567-e89b-12d3-a456-426614174000"
                      standard_price: 99900
                      currency: "DKK"
                      interval: "monthly"
                      start_date: "2024-01-15T00:00:00Z"
                    plan:
                      uuid: "123e4567-e89b-12d3-a456-426614174001"
                      name: "Premium Plan"
                      slug: "premium-plan"
                    payment:
                      uuid: "999e4567-e89b-12d3-a456-426614174999"
                      price: 124875
                      currency: "DKK"
                      date: "2024-03-02T00:00:00.000000Z"
                      payment_provider: "stripe"
                      provider_transaction_id: "pi_3OxyzABC123"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      external_customer_id: "customer_12345"
                    external_customer_id: "customer_12345"
                    team:
                      slug: "my-team"
              customerCreated:
                summary: customer.created
                value:
                  event: customer.created
                  timestamp: "2024-01-15T10:30:00Z"
                  data:
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                      phone: "12345678"
                      address: "Example Street 123"
                      zip: "1234"
                      city: "Copenhagen"
                      country: "DK"
                      reg_number: "12345678"
                    team:
                      slug: "my-team"
              customerUpdated:
                summary: customer.updated
                value:
                  event: customer.updated
                  timestamp: "2024-01-16T14:30:00Z"
                  data:
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Updated Company Name"
                      email: "newemail@example.com"
                      phone: "12345678"
                      address: "New Address 456"
                      zip: "5678"
                      city: "Aarhus"
                      country: "DK"
                      reg_number: "12345678"
                    team:
                      slug: "my-team"
              customerDeleted:
                summary: customer.deleted
                value:
                  event: customer.deleted
                  timestamp: "2024-01-20T10:30:00Z"
                  data:
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    team:
                      slug: "my-team"
              invoiceCreated:
                summary: invoice.created
                value:
                  event: invoice.created
                  timestamp: "2024-01-15T10:30:00Z"
                  data:
                    invoice:
                      uuid: "123e4567-e89b-12d3-a456-426614174004"
                      number: "INV-2024-001"
                      date: "2024-01-15T00:00:00Z"
                      currency: "DKK"
                      total: 99900
                      total_with_vat: 124875
                      vat: 24975
                      status: "issued"
                      paid_at: null
                      pay_url: "https://app.alunta.com/pay-invoice/abc123def456"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                      phone: "12345678"
                      address: "Example Street 123"
                      zip: "1234"
                      city: "Copenhagen"
                      country: "DK"
                      reg_number: "12345678"
                    external_customer_id: "customer_12345"
                    team:
                      slug: "my-team"
              invoicePaid:
                summary: invoice.paid
                value:
                  event: invoice.paid
                  team_uuid: "123e4567-e89b-12d3-a456-426614174001"
                  test_mode: false
                  timestamp: "2024-01-16T08:41:00Z"
                  data:
                    event_id: "777e4567-e89b-12d3-a456-426614174777"
                    invoice:
                      uuid: "123e4567-e89b-12d3-a456-426614174004"
                      number: "INV-2024-001"
                      currency: "DKK"
                      total_with_vat: 124875
                      status: "paid"
                      paid_at: "2024-01-16T08:41:00Z"
                    payment:
                      uuid: "999e4567-e89b-12d3-a456-426614174999"
                      price: 124875
                      currency: "DKK"
                      date: "2024-01-16"
                      payment_provider: "stripe"
                      provider_transaction_id: "pi_3OxyzABC123"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    external_customer_id: "customer_12345"
                    team:
                      slug: "my-team"
              invoicePaymentFailed:
                summary: invoice.payment_failed
                value:
                  event: invoice.payment_failed
                  timestamp: "2024-01-16T08:35:00Z"
                  data:
                    invoice:
                      uuid: "123e4567-e89b-12d3-a456-426614174004"
                      number: "INV-2024-001"
                      currency: "DKK"
                      total_with_vat: 124875
                      status: "issued"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    external_customer_id: "customer_12345"
                    team:
                      slug: "my-team"
                    error:
                      message: "Your card was declined."
              invoiceRefundedWithCreditNote:
                summary: invoice.refunded with a credit note
                description: The normal provider-refund flow sends the newly created credit note as `data.invoice`. Its totals are negative, and a partial refund's credit note covers only that partial amount. Follow `data.refund.original_invoice_uuid` to the original invoice and deduplicate on `data.refund.uuid`.
                value:
                  event: invoice.refunded
                  timestamp: "2024-02-10T11:20:00Z"
                  data:
                    invoice:
                      uuid: "321e4567-e89b-12d3-a456-426614174321"
                      number: 1002
                      currency: "DKK"
                      total: -99900
                      total_with_vat: -124875
                      status: "creditnote"
                      paid_at: null
                    refund:
                      uuid: "5b95c8b4-1a17-4c3a-9e5c-1b7c9b8a4d3f"
                      amount: 124875
                      currency: "DKK"
                      kind: "provider_refund"
                      status: "completed"
                      reason: "Customer requested refund"
                      provider: "stripe"
                      provider_refund_id: "re_3OxyzABC123"
                      refunded_at: "2024-02-10T11:20:00Z"
                      created_at: "2024-02-10T11:20:00Z"
                      original_invoice_uuid: "123e4567-e89b-12d3-a456-426614174004"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    external_customer_id: "customer_12345"
                    team:
                      slug: "my-team"
              invoiceRefundedAfterPaymentReversal:
                summary: invoice.refunded after a locally recorded provider-payment reversal
                description: |
                  Deleting a non-manual provider payment in Alunta records a `local_reversal` and sends the original positive invoice as `data.invoice`, but it does not call the gateway. A Leverandørservice chargeback uses `kind: chargeback`. Neither path creates a credit note. Follow `original_invoice_uuid`, branch on `kind`, and deduplicate on `data.refund.uuid`.
                value:
                  event: invoice.refunded
                  timestamp: "2024-02-10T11:20:00Z"
                  data:
                    invoice:
                      uuid: "123e4567-e89b-12d3-a456-426614174004"
                      number: 1001
                      currency: "DKK"
                      total: 99900
                      total_with_vat: 124875
                      status: "issued"
                      paid_at: null
                    refund:
                      uuid: "6c06d9c5-2b28-4d4b-af6d-2c8d0c9b5e40"
                      amount: 124875
                      currency: "DKK"
                      kind: "local_reversal"
                      status: "completed"
                      reason: "Payment deleted by user"
                      provider: "stripe"
                      provider_refund_id: null
                      refunded_at: "2024-02-10T11:20:00Z"
                      created_at: "2024-02-10T11:20:00Z"
                      original_invoice_uuid: "123e4567-e89b-12d3-a456-426614174004"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    external_customer_id: "customer_12345"
                    team:
                      slug: "my-team"
              checkoutCompleted:
                summary: checkout.completed (subscription)
                value:
                  event: checkout.completed
                  timestamp: "2024-01-15T10:30:00Z"
                  data:
                    type: "subscription"
                    quantity: 25
                    metadata:
                      booking_id: "bk_98765"
                    subscription:
                      uuid: "123e4567-e89b-12d3-a456-426614174000"
                      standard_price: 99900
                      pricing_type: "license_based"
                      quantity: 25
                      unit_price: 3996
                      currency: "DKK"
                      interval: "monthly"
                      start_date: "2024-01-15T00:00:00Z"
                    plan:
                      uuid: "123e4567-e89b-12d3-a456-426614174001"
                      name: "Premium Plan"
                      slug: "premium-plan"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    team:
                      slug: "my-team"
                    external_customer_id: "customer_12345"
                    deal:
                      uuid: "880e8400-e29b-41d4-a716-446655440003"
                      custom_price: 7500
                      discount:
                        name: "Special offer"
                        type: "percent"
                        value: 20
                        end_date: null
                        duration:
                          value: 1
                          unit: "years"
                      trial: null
                      notes: "As agreed upon in our meeting"
              checkoutCompletedOneOffInvoice:
                summary: checkout.completed (one-off invoice)
                value:
                  event: checkout.completed
                  timestamp: "2024-01-15T10:30:00Z"
                  data:
                    type: "one_off_invoice"
                    metadata:
                      booking_id: "bk_98765"
                    invoice:
                      uuid: "660e8400-e29b-41d4-a716-446655440001"
                      number: "1042"
                      currency: "DKK"
                      total: 50000
                      vat: 12500
                      total_with_vat: 62500
                      status: "paid"
                    payment:
                      uuid: "5b95c8b4-1a17-4c3a-9e5c-1b7c9b8a4d3f"
                      price: 62500
                      currency: "DKK"
                      payment_provider: "stripe"
                      provider_charge_id: "ch_3OxyzABC123"
                    customer:
                      uuid: "123e4567-e89b-12d3-a456-426614174002"
                      name: "Example Company"
                      email: "contact@example.com"
                    team:
                      slug: "my-team"
                    external_customer_id: "customer_12345"
      headers:
        Signature:
          description: |
            HMAC-SHA256 signature of the webhook payload. Only present when webhook secret key is enabled.

            The signature is calculated as: `hash_hmac('sha256', $rawRequestBody, $secretKey)`

            **Important:** Use the raw request body (before JSON parsing) for signature verification.
            Always use constant-time comparison (e.g., `hash_equals()` in PHP) to prevent timing attacks.
          schema:
            type: string
          required: false
        Content-Type:
          description: Content type of the request
          schema:
            type: string
            enum: [application/json]
          required: true
      responses:
        '200':
          description: Webhook received successfully. Your endpoint should return 200 to acknowledge receipt.
        '400':
          description: Bad request
        '500':
          description: Server error
