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 retried up to **8 times** with the following schedule between attempts (cumulative window ~24 hours):

    | 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}`.

    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.
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'
        '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'
        '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'
        '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'
        '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-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`. 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}`.
        - `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

        ## 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`
        - 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"
                  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
              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"
                  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'
        '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`:
          - `pending`: Subscription hasn't started yet (`start_date` is in the future)
          - `active`: Subscription is currently active and billing (`start_date` has passed, no `end_date`)
          - `under_cancellation`: Subscription is cancelled but still active until `end_date` (will stop billing after `end_date`)
          - `cancelled`: Subscription has ended (`end_date` has passed)

        - **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)
            - `active` - Currently active subscriptions (`start_date` has passed, no `end_date`)
            - `under_cancellation` - Cancelled subscriptions that are still active until `end_date` (`end_date` is set to a future date)
            - `cancelled` - Subscriptions that have ended (`end_date` has passed)

            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"
      responses:
        '200':
          description: Successful response with paginated subscriptions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSubscriptionsResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '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.

        ## 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"
              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"
      responses:
        '201':
          description: Subscription created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Subscription'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan or customer not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error or plan has no pricing configured
          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'
        '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'
        '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}/cancel:
    post:
      summary: Cancel a subscription
      description: |
        Cancel a subscription. By default, cancellation takes effect at the end of the current billing period - the subscription's `end_date` is set to the day before the next renewal date and the subscription remains active (status `under_cancellation`) until that date.

        Pass `immediate: true` in the request body to cancel immediately instead - the `end_date` is set to today and the subscription becomes `cancelled` right away.

        **Important:** End-of-period cancellation only works on active subscriptions (those without an `end_date`). Attempting to cancel a subscription that is already cancelled or under cancellation will return a 422 error.

        Subscriptions that are part of a bundle cannot be cancelled on their own - cancel the bundle instead.

        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`, the subscription is cancelled immediately (end date set to today). When `false` or omitted, the subscription is cancelled at the end of the current billing period.
                  default: false
                  example: false
      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'
        '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'
        '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'
        '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'
        '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:
                oneOf:
                  - $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.
        - Set `invoice_immediately` to `true` to generate a standalone proration invoice right away.

        **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'
        '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:
                oneOf:
                  - $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}/price:
    put:
      summary: Change a subscription's price
      description: |
        Change the standard price of a flat-rate subscription.

        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 subscriptions.** Attempting to change the price on a license-based, usage-based, or tiered subscription will return a 422 error - those subscriptions derive their price from quantity, usage, or tier configuration respectively.

        **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
              required:
                - price
              properties:
                price:
                  type: integer
                  minimum: 0
                  description: The new standard price in the subscription's currency, expressed in the smallest currency unit (e.g. øre for DKK, cents for EUR/USD).
                  example: 12000
                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'
        '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:
                oneOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/Error'
              examples:
                notFlatRate:
                  summary: Not a flat-rate subscription
                  value:
                    message: "Price changes via this endpoint are only supported for flat-rate subscriptions."
                validationError:
                  summary: Validation error
                  value:
                    message: "The price field is required."
                    errors:
                      price: ["The price field is required."]
                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'
        '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 (1-100). For example, a value of `15` means 15% 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.

        **Date requirements:**
        - `from_date` must correspond to an unlocked (not yet invoiced or charged) transaction period start date on the subscription.
        - `to_date` (optional) must also correspond to an unlocked transaction period start date after `from_date`. If omitted, the discount applies indefinitely.

        **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'
        '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:
                oneOf:
                  - $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'
        '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 change from a subscription
      description: |
        Delete a pending (not yet applied) scheduled price change from a flat-rate subscription.

        Use this to undo a permanent or temporary price change that was scheduled with a future `effective_date` (see `PUT /subscriptions/{uuid}/price`). The scheduled changes available to delete are returned in the subscription's `scheduled_price_changes` array.

        Only pending changes can be deleted; a change that has already been applied returns 422. For a temporary price change, deleting the start automatically removes its corresponding end change.

        After deletion, the subscription's transactions are regenerated so future periods revert to the previous price. 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 change to delete (returned as `uuid` in `scheduled_price_changes`)
          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'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Subscription or scheduled price 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'
        '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'
        '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'
        '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
                  tier_locked:
                    type: boolean
                    description: Whether the tier is locked (prevents API-driven changes)
                    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'
        '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."
  /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.
                  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'
        '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
                      action:
                        type: string
                        enum: [none, immediate, scheduled, manual_required, locked]
                      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'
        '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'
        '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.
      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"
      responses:
        '201':
          description: Customer created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '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 another 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"
                externalCustomerIdArchived:
                  summary: external_customer_id reserved by an archived customer
                  value:
                    message: "The given data was invalid."
                    errors:
                      external_customer_id:
                        - "A customer with this external_customer_id exists but is archived. Existing customer UUID: 456e7890-e89b-12d3-a456-426614174001. Restore via the dashboard before reusing this id."
  /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'
        '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.
      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"
      responses:
        '200':
          description: Customer updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '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, this customer is never invoiced directly - its subscriptions are consolidated onto the payer's invoice (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'
        '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 - exactly as the dashboard and the automatic run do. Conversely, a customer that is paid for by a reseller cannot be invoiced directly; calling this endpoint for such a customer returns `422` with `error: customer_paid_by_reseller`.

        ## 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'
        '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). Such a customer is never invoiced directly; its subscriptions are consolidated onto the payer's invoice. In that case `error` is `customer_paid_by_reseller` and `payer_uuid` identifies the paying customer.
          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'
        '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). Preview shares the create endpoint's eligibility, so such a customer cannot be previewed directly; `error` is `customer_paid_by_reseller` and `payer_uuid` identifies the paying customer.
          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'
        '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). Preview shares the create endpoint's eligibility, so such a customer cannot be previewed directly; `error` is `customer_paid_by_reseller` and `payer_uuid` identifies the paying customer.
          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'
        '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'
        '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'
        '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'
        '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'
        '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'
        '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'
        '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.
      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'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Plan not found
          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'
        '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'
        '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'
        '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'
        '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'
        '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'
        '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, the response includes a `pay_url` you can redirect the customer to for online card payment. 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'
        '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'
        '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'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /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 covering every line's remaining uncredited amount, 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. Lines that have already been partially credited (e.g. via the in-app credit flow) contribute only their remaining amount; 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.
        - 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 always credits every line's full remaining amount. Per-line partial credits are available in the in-app flow but not via this endpoint.
        - 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.
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '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, `overpaid` when the payment exceeds it.
        - Fires the `invoice.paid` webhook when the invoice transitions to `paid`.

        ### 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'
        '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.
            - 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'
        '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.

        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'
        '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'
        '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'
        '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'
        '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'
        '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'
        '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.
      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'
        '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'
        '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'
        '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'
        '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'
        '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'
        '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'
        '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'
        '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'
        '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 URL.
        This is useful for recovering from failed deliveries caused by downtime or deployment issues.

        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`).

        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'
        '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: Webhook settings not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "Webhook settings not found."
  /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'
        '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'
        '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'
        '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'
        '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. Refused with 422 when there are active customers on the bundle - cancel their bundle instances first.
      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'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          description: Bundle has active customers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /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
      responses:
        '200':
          description: Successful response with the customer's bundle instances
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/MembershipBundleInstance'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
    post:
      summary: Activate a subscription bundle for a customer
      description: |
        Creates a `MembershipBundle` instance for the customer, with one child `Membership` per plan in the bundle. All children share `payment_card_id` so they renew together via the existing card-charge job.
        On the first invoice generated for the bundle, all children's transactions plus the bundle's one-off charges are consolidated onto a single invoice.
        Per-membership `subscription.created` webhooks fire for each child, and a single `subscription_bundle.created` webhook fires for the bundle as a whole.
      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'
        '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'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
    delete:
      summary: Cancel a customer's bundle instance
      description: |
        Cancels the bundle and cascades cancellation to every child membership. Each child fires its normal `subscription.cancelled` webhook, plus a single consolidated `subscription_bundle.cancelled` webhook fires.
        End dates are set per child according to the existing per-membership cancellation logic; the bundle's `ends_at` is set to the latest child end date.
      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'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          description: Bundle is already cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /customers/{customerUuid}/subscription-bundles/{instanceUuid}/resume:
    post:
      summary: Resume a customer's cancelled bundle instance
      description: |
        Resumes a bundle that is currently under cancellation and cascades the resume to every child membership, clearing each child's `end_date` and the bundle's `ends_at`. Each child fires its normal `subscription.resumed` webhook, plus a single consolidated `subscription_bundle.resumed` webhook fires.

        Bundles are all-or-nothing: if any child membership has already ended (and therefore cannot be resumed), the whole request is refused with a 422 error rather than partially resuming the bundle. This effectively undoes a previous bundle cancellation.

        **Important:** This endpoint only works on bundles that are `under_cancellation` (have a future `ends_at`). Resuming a bundle that is not cancelled, whose `ends_at` is in the past, or that contains an already-ended child membership, returns a 422 error.
      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 successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MembershipBundleInstance'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          description: Bundle cannot be resumed (not cancelled, already ended, or has an ended child)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notCancelled:
                  summary: Bundle is not cancelled
                  value:
                    message: "This bundle is not cancelled."
                alreadyEnded:
                  summary: Bundle has already ended
                  value:
                    message: "This bundle has already ended and cannot be resumed."
                endedChild:
                  summary: A child subscription has already ended
                  value:
                    message: "This bundle cannot be resumed because one or more of its subscriptions has already ended."
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"
        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}`.
            - `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
            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`

            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: When the discount expires (ISO 8601). Null for indefinite discounts.
              example: "2026-06-01T00:00:00+00:00"
        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"
        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"
        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.
          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_end_date:
          type: string
          format: date
          nullable: true
          description: Date when the discount expires. Must be in the future. Omit for an indefinite discount.
          example: "2026-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` should be between 1 and 100.
            - `fixed` - A fixed amount discount. `discount_value` should be in the smallest currency unit (e.g., cents, øre).
          example: "percent"
        discount_value:
          type: integer
          minimum: 1
          description: |
            The discount amount. Interpretation depends on `discount_type`:

            - For `percent`: The percentage to discount (1-100). Example: `15` means 15% off.
            - For `fixed`: The amount in the smallest currency unit (e.g., cents, øre). Example: `5000` means 50.00 off.
          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.

            If provided, must correspond to an unlocked transaction period start date after `from_date`. 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: integer
          description: |
            The discount amount. For `percent` type, this is a percentage (1-100). For `fixed` type, this is an amount in the smallest currency unit.
          example: 15
        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"
        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 is `0` until the billing period ends and usage is finalized.

            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
        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. `standard_price` is 0 until the period ends and usage is finalized. Billed in arrears.
            - `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 price charged per unit of consumption. Total is calculated at period end based on actual usage.
            - 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 is active and will continue indefinitely (no cancellation date set)
            - Set to a future date - The subscription is "under_cancellation" and will end on this date
            - Set to a past date - The subscription is "cancelled" and has ended

            When a subscription is cancelled, this field is set to the date when the subscription should end. The subscription will continue to be active and billable until this date is reached.

            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 the subscription directly
            - `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.
            - `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 is still active until `end_date`. `end_date` is set to a future date.
            - `cancelled` - The subscription has ended. `end_date` has passed.

            **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.

            Returns `null` if no custom metadata was provided.
          additionalProperties:
            type: string
          example:
            founding_tier: "gold"
            referral_code: "PARTNER_ABC"
        available_price_change_dates:
          type: array
          description: |
            The dates from which a permanent price change may take effect for this flat-rate 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"]
        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`.
          properties:
            name:
              type: string
              description: Name of the discount (e.g., "Summer Sale", "Loyalty Discount")
              example: "Summer Sale"
            value:
              type: integer
              description: |
                Discount amount. Interpretation depends on `type`:
                - For `Percent`: percentage (1-100), e.g. 20 means 20% off
                - For `Fixed`: amount in smallest currency unit (e.g., cents, øre)
              example: 20
            type:
              type: string
              description: Type of discount
              enum: [Percent, Fixed]
              example: "Percent"
            end_date:
              type: string
              format: date-time
              nullable: true
              description: When the discount expires (ISO 8601). Null for indefinite discounts.
              example: "2024-09-01T00:00:00.000000Z"
          required:
            - name
            - value
            - type
            - end_date
        scheduled_price_changes:
          type: array
          description: |
            Pending (not yet applied) scheduled price changes on this flat-rate 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.

            Only included when the subscription's scheduled changes are loaded (the detail endpoint `GET /subscriptions/{uuid}` and the list endpoint `GET /subscriptions`).
          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 price in the smallest currency unit (e.g., cents, øre).
                example: 15000
              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
              - currency
        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_failed
            - subscription.tier_changed
            - subscription.transferred
            - subscription_bundle.created
            - subscription_bundle.cancelled
            - subscription_bundle.resumed
            - customer.created
            - customer.updated
            - customer.deleted
            - invoice.created
            - invoice.paid
            - invoice.payment_failed
            - checkout.completed
        team_id:
          type: integer
          description: The Alunta team ID 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: 123
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the event occurred (in your team's timezone)
        data:
          type: object
          description: |
            Event-specific data payload. The structure varies by event type:

            - **subscription.\*** events: Contains `subscription`, `plan`, `customer`, and `team` objects
            - **subscription.payment_failed**: Contains `subscription`, `plan`, `customer`, and `team` objects. Fires on every failed card collection attempt for the subscription (card declined or expired), regardless of what the plan's payment-failure policy then does (nothing / cancel / retry). You may receive it more than once for the same subscription across retry runs. Cancellation, when it happens, is reported separately via `subscription.cancelled` / `subscription.ended`.
            - **subscription.ended**: Also includes a top-level `reason` field indicating why the subscription ended (e.g., `cancellation`, `customer_cancellation`, `payment_failure`, `membership_deleted`, `customer_deleted`)
            - **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.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 `invoice`, `payment`, `customer`, and `team` objects. Fires when a Payment brings the invoice to `paid` status — covers hosted online payment, auto-charge on saved card, manual registration via the UI, and subscription payments.
            - **invoice.payment_failed**: Contains `invoice`, `customer`, `team`, and an `error` object. Fires when an online payment attempt on an invoice fails.
            - **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); 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.
          additionalProperties: true
      required:
        - event
        - team_id
        - 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 an invoice has been created for this transaction.

            - `true` - An invoice has been created for this transaction
            - `false` - No invoice has been created yet for this transaction
          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`).
          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
      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
      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
    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: Whether the tier actually changed as a result of this evaluation
          example: true
        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 and a proration adjustment was created
            - `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
            - `locked` - The subscription's tier is locked; parameters were stored but the tier was not changed
          enum: [none, immediate, scheduled, manual_required, locked]
          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
        - 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"
        payment_providers:
          type: array
          nullable: true
          description: |
            Payment methods accepted for subscriptions on this plan.

            Returns `null` if no payment providers have been configured for the plan yet (in which case the plan cannot be subscribed to until configured in the Alunta UI or via the API).
          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_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 to create a plan without payment methods configured (it can be configured later in the Alunta UI or via the API).
          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
    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 refund issued against a provider 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"
        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: [stripe, onpay, quickpay, manual, invoice]
          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"
    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: integer
          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
        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`

            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
          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 payment methods unchanged.
          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.

            **Required for non-tiered plans.** For tiered plans, use `interval` and `currency` instead — tiered plans have no renewal intervals.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        interval:
          type: integer
          nullable: true
          description: |
            Billing interval in months. **Used for tiered plans** where `renewal_interval_uuid` is not applicable.

            **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). **Used for tiered plans** where `renewal_interval_uuid` is not applicable.

            Must match a currency configured in the plan's tier prices.
          example: "DKK"
        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"
      required:
        - plan_uuid
        - customer_uuid
    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
        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`

            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, grouping their child memberships with shared lifecycle.
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        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
        end_reason:
          type: string
          nullable: true
        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 memberships - one per plan in the bundle.
          items:
            $ref: '#/components/schemas/Subscription'
    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
              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`

                  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
              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`

                  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
        - payment_provider
      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
          example: invoice
        payment_card_uuid:
          type: string
          format: uuid
          nullable: true
    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: Invoice number (sequential identifier)
          example: 1001
        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"
        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
        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"
        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`, and the balance is positive. 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
        - date
        - currency
        - total
        - vat
        - total_with_vat
        - 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 description of the line item (sometimes called `description` in other systems).
          example: "Premium Plan - Monthly Subscription"
        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: integer
          description: VAT rate percentage applied to this line item
          example: 25
        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
        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).

            Plan switches, interval changes, and permanent price changes on
            other pricing types do not generate proration lines today.
          example: false
      required:
        - uuid
        - text
        - amount
        - price
        - vat_rate
        - total
        - 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: integer
          description: VAT rate percentage applied to the payment.
          example: 25
        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
            - `invoice` - Recorded as a bank transfer against an invoice
            - `manual` - Recorded manually (cash, other)
          enum: [stripe, onpay, quickpay, invoice, manual]
          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"
        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, this customer is never invoiced directly - its subscriptions are consolidated onto the payer's invoice. Set or clear 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 and cannot be reused on a new customer; use `archived` together with `archived_at` to detect this 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
        active_subscriptions:
          type: array
          description: |
            List of currently active subscriptions for this customer. Only included on the single customer endpoint (GET /customers/{uuid}), not on the list endpoint.

            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
          nullable: true
          description: |
            Monthly recurring revenue (MRR) for this customer, grouped by currency. Only included on the single customer endpoint (GET /customers/{uuid}), not on the list endpoint.

            Each key is a currency code (e.g., "DKK", "EUR") and the value is the calculated MRR in the smallest currency unit.
          additionalProperties:
            type: number
          example:
            DKK: 10000
        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
        - has_valid_payment_card
        - 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"
        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"
    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"
    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.
    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
                  timestamp: "2025-01-15T23:59:59Z"
                  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"
                      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
                  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"
                    team:
                      slug: "my-team"
              subscriptionPaymentFailed:
                summary: subscription.payment_failed
                value:
                  event: subscription.payment_failed
                  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"
                    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
                  timestamp: "2024-01-16T08:41:00Z"
                  data:
                    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."
              invoiceRefunded:
                summary: invoice.refunded
                value:
                  event: invoice.refunded
                  timestamp: "2024-02-10T11:20:00Z"
                  data:
                    invoice:
                      uuid: "123e4567-e89b-12d3-a456-426614174004"
                      number: "INV-2024-001"
                      currency: "DKK"
                      total: 99900
                      total_with_vat: 124875
                      status: "credited"
                      paid_at: "2024-01-16T08:41:00Z"
                    refund:
                      uuid: "5b95c8b4-1a17-4c3a-9e5c-1b7c9b8a4d3f"
                      amount: 124875
                      currency: "DKK"
                      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"
                    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"
                    metadata:
                      booking_id: "bk_98765"
                    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"
                    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: "2026-06-01T00:00:00+00:00"
                      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

