openapi: 3.1.0
info:
  title: Subscription Manager Public API
  version: 1.0.0
  description: >-
    Public API for subscription CRUD automation through Developer API keys. The
    operation descriptions include AI-oriented task semantics, risk levels, and
    confirmation guidance for agent integrations.
servers:
  - url: https://your-site.example
    description: Replace with your deployed Subscription Manager site URL.
security:
  - bearerApiKey: []
tags:
  - name: Subscriptions
    description: Create, read, update, and delete subscriptions for the API key owner.
    x-ai:
      domain: subscription_management
      ownerScope: authenticated_api_key_owner
      writeSafety: >-
        Confirm create, update, cancel, pause, resume, and delete operations
        with the user before calling them from an AI agent. Write operations
        require an API key with the write scope.
  - name: Analytics
    description: >-
      Computed spend summaries, duplicate detection, and optimization
      candidates. Read-only; money is reported per currency and never converted.
  - name: Audit
    description: Read-only audit trail of write operations performed through the API.
paths:
  /api/v1/subscriptions:
    get:
      tags:
        - Subscriptions
      summary: List subscriptions
      description: >-
        Returns subscriptions owned by the authenticated API key owner, newest
        first. Use the limit and offset query parameters to page through
        results.
      operationId: listSubscriptions
      x-ai:
        purpose: >-
          Find and inspect the authenticated user's subscriptions before
          planning analysis, update, or delete tasks.
        naturalLanguageTriggers:
          - Show my subscriptions.
          - Which subscriptions are renewing soon?
          - Find the Netflix subscription before I change it.
        riskLevel: low
        requiresConfirmation: false
        useWhen: >-
          The user wants to view, search, summarize, compare, or identify
          subscriptions.
        doNotUseWhen: >-
          The user provides a specific subscription UUID and wants only that
          record; use getSubscription instead.
      x-mint:
        href: /api-reference/subscriptions/list
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of subscriptions to return (1-100, default 50).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          description: >-
            Number of subscriptions to skip from the start of the list (default
            0).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: status
          in: query
          required: false
          description: Filter by lifecycle state.
          schema:
            $ref: '#/components/schemas/SubscriptionStatus'
        - name: category
          in: query
          required: false
          description: Exact category match, for example "Streaming".
          schema:
            type: string
        - name: period
          in: query
          required: false
          description: Filter by billing period.
          schema:
            $ref: '#/components/schemas/SubscriptionPeriod'
        - name: q
          in: query
          required: false
          description: Case-insensitive search over the subscription name.
          schema:
            type: string
        - name: expiringBefore
          in: query
          required: false
          description: >-
            Return subscriptions whose nextPaymentDate is on or before this
            date. Combine with status=active to find renewals due soon.
          schema:
            $ref: '#/components/schemas/DateOnly'
        - name: sort
          in: query
          required: false
          description: >-
            Sort order; prefix with - for descending, for example
            -nextPaymentDate.
          schema:
            type: string
            default: '-createdAt'
            enum:
              - createdAt
              - '-createdAt'
              - nextPaymentDate
              - '-nextPaymentDate'
              - amount
              - '-amount'
              - name
              - '-name'
      responses:
        '200':
          description: Subscriptions returned.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionListResponse'
              examples:
                subscriptions:
                  value:
                    data:
                      - id: 33333333-3333-4333-8333-333333333333
                        name: Netflix
                        category: Streaming
                        amount: 15.99
                        currency: USD
                        period: monthly
                        lastPaymentDate: '2026-06-01'
                        nextPaymentDate: '2026-07-01'
                        notificationEnabled: true
                        status: active
                        createdAt: '2026-06-16T00:00:00.000Z'
                        updatedAt: '2026-06-16T00:00:00.000Z'
                    pagination:
                      limit: 50
                      offset: 0
                      hasMore: false
                    query:
                      sort: '-createdAt'
                      filters: {}
                    requestId: request-1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Subscriptions
      summary: Create a subscription
      description: >-
        Creates a subscription for the authenticated API key owner.
        Server-managed fields are rejected if supplied.
      operationId: createSubscription
      x-ai:
        purpose: >-
          Record a new subscription that the user currently uses or wants to
          track.
        naturalLanguageTriggers:
          - Add ChatGPT Plus, 20 USD monthly, last paid today.
          - Record a yearly GitHub subscription.
          - I just subscribed to Netflix, track it for me.
        riskLevel: medium
        requiresConfirmation: true
        useWhen: >-
          The user asks to add or record a subscription that does not already
          exist.
        doNotUseWhen: >-
          The user is changing an existing subscription; identify the existing
          record and use updateSubscription.
      x-mint:
        href: /api-reference/subscriptions/create
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubscriptionWrite'
            examples:
              monthly:
                value:
                  name: Netflix
                  category: Streaming
                  amount: 15.99
                  currency: USD
                  period: monthly
                  lastPaymentDate: '2026-06-01'
                  notificationEnabled: true
              custom:
                value:
                  name: Domain renewal
                  category: Infrastructure
                  amount: 12
                  currency: USD
                  period: custom
                  lastPaymentDate: '2026-06-01'
                  customDate: '45'
                  notificationEnabled: true
      responses:
        '201':
          description: Subscription created.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    options:
      tags:
        - Subscriptions
      summary: Preflight subscriptions collection
      description: Returns CORS headers for browser preflight requests.
      operationId: optionsSubscriptions
      security: []
      x-mint:
        href: /api-reference/subscriptions/options
      responses:
        '204':
          description: Preflight accepted.
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: string
                example: '*'
            Access-Control-Allow-Headers:
              schema:
                type: string
                example: Authorization, Content-Type
            Access-Control-Allow-Methods:
              schema:
                type: string
                example: GET, POST, PATCH, DELETE, OPTIONS
  /api/v1/subscriptions/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Subscription UUID.
        schema:
          type: string
          format: uuid
        example: 33333333-3333-4333-8333-333333333333
    get:
      tags:
        - Subscriptions
      summary: Get a subscription
      description: >-
        Returns one subscription if it belongs to the authenticated API key
        owner.
      operationId: getSubscription
      x-ai:
        purpose: >-
          Retrieve one known subscription by UUID after it has already been
          identified.
        naturalLanguageTriggers:
          - Show details for this subscription id.
          - Verify the record before updating it.
        riskLevel: low
        requiresConfirmation: false
        useWhen: >-
          A subscription UUID is already available and the agent needs exact
          current state.
        doNotUseWhen: >-
          The user only provides a name such as Netflix; use listSubscriptions
          and match by name/category first.
      x-mint:
        href: /api-reference/subscriptions/get
      responses:
        '200':
          description: Subscription returned.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags:
        - Subscriptions
      summary: Update a subscription
      description: >-
        Updates a non-empty subset of writable fields. Only the supplied fields
        are validated. nextPaymentDate is recalculated only when
        lastPaymentDate, period, or customDate changes; otherwise the stored
        value is kept.
      operationId: updateSubscription
      x-ai:
        purpose: >-
          Change writable fields on an existing subscription, such as amount,
          category, period, last payment date, custom interval, or
          notifications.
        naturalLanguageTriggers:
          - Change Netflix to yearly billing.
          - Update ChatGPT Plus to 20 USD.
          - Turn off renewal reminders for this subscription.
        riskLevel: medium
        requiresConfirmation: true
        useWhen: >-
          The target subscription has been identified and the user wants to
          modify tracked details.
        doNotUseWhen: >-
          The user wants to remove the subscription completely; use
          deleteSubscription after explicit confirmation.
      x-mint:
        href: /api-reference/subscriptions/update
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubscriptionPatch'
            examples:
              changePeriod:
                value:
                  period: yearly
              disableNotifications:
                value:
                  notificationEnabled: false
      responses:
        '200':
          description: Subscription updated.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags:
        - Subscriptions
      summary: Delete a subscription
      description: >-
        Deletes one subscription if it belongs to the authenticated API key
        owner.
      operationId: deleteSubscription
      x-ai:
        purpose: >-
          Permanently remove a subscription record from tracking. This cannot be
          undone.
        naturalLanguageTriggers:
          - Delete the Netflix subscription permanently.
          - Remove this duplicate subscription record.
        riskLevel: high
        requiresConfirmation: true
        useWhen: >-
          The user clearly wants the tracked record removed permanently and has
          confirmed the exact subscription.
        doNotUseWhen: >-
          The user only wants to stop billing but keep the record; set status to
          cancelled or paused with updateSubscription instead.
      x-mint:
        href: /api-reference/subscriptions/delete
      responses:
        '200':
          description: Subscription deleted.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - deleted
                  - requestId
                properties:
                  deleted:
                    type: boolean
                    const: true
                  requestId:
                    type: string
                additionalProperties: false
              examples:
                deleted:
                  value:
                    deleted: true
                    requestId: request-1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    options:
      tags:
        - Subscriptions
      summary: Preflight subscription item
      description: Returns CORS headers for browser preflight requests.
      operationId: optionsSubscription
      security: []
      x-mint:
        href: /api-reference/subscriptions/options-item
      responses:
        '204':
          description: Preflight accepted.
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: string
                example: '*'
            Access-Control-Allow-Headers:
              schema:
                type: string
                example: Authorization, Content-Type
            Access-Control-Allow-Methods:
              schema:
                type: string
                example: GET, POST, PATCH, DELETE, OPTIONS
  /api/v1/analytics/summary:
    get:
      tags:
        - Analytics
      summary: Spend summary
      description: >-
        Returns subscription counts by status, monthly and yearly totals grouped
        per currency (never converted across currencies), spend by category, and
        upcoming renewals within a horizon.
      operationId: getSpendSummary
      x-ai:
        purpose: >-
          Answer how much the user spends, where the money goes, and what renews
          soon, without listing and summing records client-side.
        naturalLanguageTriggers:
          - How much am I spending per month?
          - What renews in the next two weeks?
          - Break down my spending by category.
        riskLevel: low
        requiresConfirmation: false
        useWhen: >-
          The user wants computed spend totals, category breakdowns, or upcoming
          renewals.
        doNotUseWhen: The user wants the raw subscription records; use listSubscriptions.
      parameters:
        - name: horizonDays
          in: query
          required: false
          description: Window in days for the upcomingRenewals list (default 30).
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 30
      responses:
        '200':
          description: Spend summary computed.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpendSummaryResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/analytics/duplicates:
    get:
      tags:
        - Analytics
      summary: Duplicate subscriptions
      description: >-
        Returns groups of non-cancelled subscriptions that share a normalized
        name, indicating likely duplicate tracking or double billing.
      operationId: findDuplicateSubscriptions
      x-ai:
        purpose: >-
          Detect likely double-billing or duplicate tracking by matching
          normalized subscription names.
        naturalLanguageTriggers:
          - Am I paying for anything twice?
          - Find duplicate subscriptions.
        riskLevel: low
        requiresConfirmation: false
        useWhen: The user asks whether they pay for the same service more than once.
        doNotUseWhen: >-
          The user wants overlapping but differently named services; only names
          are matched.
      responses:
        '200':
          description: Duplicate groups returned.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DuplicatesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/analytics/optimizations:
    get:
      tags:
        - Analytics
      summary: Optimization candidates
      description: >-
        Returns factual optimization candidates - active monthly subscriptions
        with their current annualized cost, and subscriptions whose monthly cost
        exceeds twice the per-currency average. No discount rates are invented.
      operationId: getOptimizationSuggestions
      x-ai:
        purpose: >-
          Surface what the user could cut or switch to annual, using current
          costs only.
        naturalLanguageTriggers:
          - My subscription spending is too high, what can I cut?
          - Which subscriptions could save money on annual billing?
        riskLevel: low
        requiresConfirmation: false
        useWhen: The user asks how to save money or which subscriptions are expensive.
        doNotUseWhen: >-
          The user wants a guaranteed savings figure; the API reports current
          costs, not negotiated discounts.
      responses:
        '200':
          description: Optimization candidates returned.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptimizationsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/audit:
    get:
      tags:
        - Audit
      summary: List audit log
      description: >-
        Returns the audit trail of write operations performed through the API,
        newest first, with before/after state in metadata.
      operationId: listAuditLog
      x-ai:
        purpose: >-
          Show what was changed, added, or removed, or reconstruct prior state
          to offer an undo.
        naturalLanguageTriggers:
          - What changes have been made to my subscriptions?
          - Show the history for this subscription.
        riskLevel: low
        requiresConfirmation: false
        useWhen: The user asks what changed or the agent needs prior state for an undo.
        doNotUseWhen: The user wants current subscription state; use listSubscriptions.
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of audit entries to return (1-100, default 50).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of entries to skip from the start.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: subscriptionId
          in: query
          required: false
          description: Only return audit entries for this subscription.
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Audit entries returned.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      description: >-
        Developer API key generated in the app. Keys carry scopes; a read key
        can call list, get, analytics, and audit endpoints, while a write key
        additionally allows create, update, cancel, pause, resume, and delete.
      x-default: subm_your_key_here
  headers:
    XRateLimitLimit:
      description: Total allowed requests in the current fixed window.
      schema:
        type: integer
        example: 60
    XRateLimitRemaining:
      description: Requests remaining in the current fixed window.
      schema:
        type: integer
        example: 59
    XRateLimitReset:
      description: Unix timestamp in seconds when the fixed window resets.
      schema:
        type: integer
        example: 1781690400
    RetryAfter:
      description: >-
        Seconds to wait before retrying. Sent with 429 responses, including when
        too many invalid API key attempts are throttled.
      schema:
        type: integer
        example: 2700
  responses:
    BadRequest:
      description: >-
        The request payload or path parameter is invalid. Validation failures
        are rejected before the hourly quota is consumed, so they do not carry
        rate-limit headers.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalidField:
              value:
                error:
                  code: invalid_subscription_field
                  message: 'Field is not writable: nextPaymentDate'
                  field: nextPaymentDate
                  suggestedFix: >-
                    Remove server-managed fields such as id, nextPaymentDate,
                    createdAt, and updatedAt before retrying.
                  writableFields:
                    - amount
                    - category
                    - currency
                    - customDate
                    - lastPaymentDate
                    - name
                    - notificationEnabled
                    - period
                    - status
                requestId: request-1
            invalidPeriod:
              value:
                error:
                  code: invalid_subscription
                  message: Invalid option
                  field: period
                  suggestedFix: >-
                    Use one of the supported billing periods: monthly, yearly,
                    custom.
                  allowedValues:
                    - monthly
                    - yearly
                    - custom
                requestId: request-1
            invalidPagination:
              value:
                error:
                  code: invalid_pagination
                  message: limit cannot exceed 100
                  field: limit
                  suggestedFix: >-
                    Use limit between 1 and 100 and a non-negative offset, for
                    example ?limit=50&offset=0.
                requestId: request-1
    Unauthorized:
      description: The API key is missing, invalid, or revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalidApiKey:
              value:
                error:
                  code: invalid_api_key
                  message: Invalid API key
                requestId: request-1
    Forbidden:
      description: The API key lacks the scope required for this operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            insufficientScope:
              value:
                error:
                  code: insufficient_scope
                  message: This API key does not have the required 'write' scope
                  suggestedFix: >-
                    Use an API key created with the write scope. Read-only keys
                    can only list and read subscriptions.
                requestId: request-1
    NotFound:
      description: The subscription was not found for the API key owner.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            missing:
              value:
                error:
                  code: subscription_not_found
                  message: Subscription not found
                requestId: request-1
    RateLimited:
      description: The user request limit or invalid-auth attempt limit was exceeded.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            rateLimited:
              value:
                error:
                  code: rate_limit_exceeded
                  message: Rate limit exceeded
                requestId: request-1
            tooManyInvalidKeys:
              value:
                error:
                  code: rate_limit_exceeded
                  message: Too many invalid API key attempts
                requestId: request-1
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            internal:
              value:
                error:
                  code: internal_error
                  message: Internal server error
                requestId: request-1
  schemas:
    SubscriptionPeriod:
      type: string
      description: Billing cadence used to calculate nextPaymentDate.
      enum:
        - monthly
        - yearly
        - custom
    SubscriptionStatus:
      type: string
      description: >-
        Lifecycle state. active means billing; paused means temporarily stopped;
        cancelled means no longer billing but kept for history.
      enum:
        - active
        - paused
        - cancelled
    Currency:
      type: string
      description: ISO-like currency code supported by the app.
      enum:
        - CNY
        - USD
        - EUR
        - JPY
        - GBP
        - AUD
        - CAD
        - CHF
        - HKD
        - SGD
    DateOnly:
      type: string
      description: Calendar date without time, always in YYYY-MM-DD format.
      pattern: ^\d{4}-\d{2}-\d{2}$
      example: '2026-06-01'
    Subscription:
      type: object
      required:
        - id
        - name
        - category
        - amount
        - currency
        - period
        - lastPaymentDate
        - nextPaymentDate
        - notificationEnabled
        - status
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
          description: Server-generated subscription identifier.
        name:
          type: string
          description: Human-readable service or product name.
          minLength: 1
          maxLength: 120
        category:
          type: string
          description: User-visible category label used for grouping and analytics.
          minLength: 1
          maxLength: 80
        amount:
          type: number
          description: >-
            Payment amount in major currency units, with at most 2 decimal
            places.
          minimum: 0
          maximum: 999999.99
          multipleOf: 0.01
        currency:
          $ref: '#/components/schemas/Currency'
        period:
          $ref: '#/components/schemas/SubscriptionPeriod'
        lastPaymentDate:
          $ref: '#/components/schemas/DateOnly'
        nextPaymentDate:
          description: Server-calculated next payment date.
          $ref: '#/components/schemas/DateOnly'
        customDate:
          type: string
          pattern: ^[1-9]\d*$
          description: >-
            Required only when period is custom. Despite the legacy name, this
            is a positive whole-number string of days, not a calendar date.
        notificationEnabled:
          type: boolean
        status:
          $ref: '#/components/schemas/SubscriptionStatus'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      additionalProperties: false
    SubscriptionWrite:
      type: object
      required:
        - name
        - category
        - amount
        - currency
        - period
        - lastPaymentDate
      properties:
        name:
          type: string
          description: Human-readable service or product name.
          minLength: 1
          maxLength: 120
        category:
          type: string
          description: User-visible category label used for grouping and analytics.
          minLength: 1
          maxLength: 80
        amount:
          type: number
          description: >-
            Payment amount in major currency units, with at most 2 decimal
            places.
          minimum: 0
          maximum: 999999.99
          multipleOf: 0.01
        currency:
          $ref: '#/components/schemas/Currency'
        period:
          $ref: '#/components/schemas/SubscriptionPeriod'
        lastPaymentDate:
          description: >-
            Most recent payment date. The server calculates nextPaymentDate from
            this value and period.
          $ref: '#/components/schemas/DateOnly'
        customDate:
          type: string
          pattern: ^[1-9]\d*$
          description: >-
            Required only when period is custom. Despite the legacy name, this
            is a positive whole-number string of days, not a calendar date.
        notificationEnabled:
          type: boolean
          default: true
        status:
          $ref: '#/components/schemas/SubscriptionStatus'
      additionalProperties: false
    SubscriptionPatch:
      type: object
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        category:
          type: string
          minLength: 1
          maxLength: 80
        amount:
          type: number
          minimum: 0
          maximum: 999999.99
          multipleOf: 0.01
        currency:
          $ref: '#/components/schemas/Currency'
        period:
          $ref: '#/components/schemas/SubscriptionPeriod'
        lastPaymentDate:
          $ref: '#/components/schemas/DateOnly'
        customDate:
          type: string
          pattern: ^[1-9]\d*$
          description: >-
            Required only when period is custom. Despite the legacy name, this
            is a positive whole-number string of days, not a calendar date.
        notificationEnabled:
          type: boolean
        status:
          $ref: '#/components/schemas/SubscriptionStatus'
      additionalProperties: false
    SubscriptionResponse:
      type: object
      required:
        - data
        - requestId
      properties:
        data:
          $ref: '#/components/schemas/Subscription'
        requestId:
          type: string
      additionalProperties: false
    SubscriptionListResponse:
      type: object
      required:
        - data
        - pagination
        - requestId
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Subscription'
        pagination:
          $ref: '#/components/schemas/Pagination'
        query:
          type: object
          description: Echo of the sort and filters applied to this response.
          properties:
            sort:
              type: string
            filters:
              type: object
              additionalProperties: true
          additionalProperties: false
        requestId:
          type: string
      additionalProperties: false
    Pagination:
      type: object
      required:
        - limit
        - offset
        - hasMore
      properties:
        limit:
          type: integer
          minimum: 1
          maximum: 100
          description: Page size that was applied to this response.
        offset:
          type: integer
          minimum: 0
          description: Number of records skipped before this page.
        hasMore:
          type: boolean
          description: >-
            True when at least one more record exists past this page. Fetch the
            next page with offset + limit.
      additionalProperties: false
    SpendSummary:
      type: object
      properties:
        asOf:
          type: string
          format: date-time
        horizonDays:
          type: integer
        counts:
          type: object
          properties:
            total:
              type: integer
            active:
              type: integer
            paused:
              type: integer
            cancelled:
              type: integer
        byCurrency:
          type: array
          items:
            type: object
            properties:
              currency:
                type: string
              activeSubscriptions:
                type: integer
              monthlyTotal:
                type: number
              yearlyTotal:
                type: number
        byCategory:
          type: array
          items:
            type: object
            properties:
              category:
                type: string
              activeSubscriptions:
                type: integer
              monthlyTotalsByCurrency:
                type: object
                additionalProperties:
                  type: number
        upcomingRenewals:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              category:
                type: string
              amount:
                type: number
              currency:
                type: string
              period:
                $ref: '#/components/schemas/SubscriptionPeriod'
              nextPaymentDate:
                $ref: '#/components/schemas/DateOnly'
              daysUntilRenewal:
                type: integer
    SpendSummaryResponse:
      type: object
      required:
        - summary
        - requestId
      properties:
        summary:
          $ref: '#/components/schemas/SpendSummary'
        requestId:
          type: string
      additionalProperties: false
    DuplicatesResponse:
      type: object
      required:
        - duplicates
        - requestId
      properties:
        duplicates:
          type: array
          items:
            type: object
            properties:
              normalizedName:
                type: string
              subscriptions:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                    name:
                      type: string
                    category:
                      type: string
                    amount:
                      type: number
                    currency:
                      type: string
                    period:
                      $ref: '#/components/schemas/SubscriptionPeriod'
                    status:
                      $ref: '#/components/schemas/SubscriptionStatus'
        requestId:
          type: string
      additionalProperties: false
    OptimizationsResponse:
      type: object
      required:
        - optimizations
        - requestId
      properties:
        optimizations:
          type: object
          properties:
            monthlyToAnnual:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                  currency:
                    type: string
                  monthlyAmount:
                    type: number
                  yearlyAtCurrentRate:
                    type: number
            aboveAverageInCurrency:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                  currency:
                    type: string
                  monthlyEquivalent:
                    type: number
                  currencyMonthlyAverage:
                    type: number
        requestId:
          type: string
      additionalProperties: false
    AuditEntry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        action:
          type: string
          enum:
            - subscription.create
            - subscription.update
            - subscription.delete
        subscriptionId:
          type: string
          format: uuid
        apiKeyId:
          type: string
          format: uuid
        requestId:
          type: string
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Operation context. Holds before, after, and patch snapshots when
            available.
        createdAt:
          type: string
          format: date-time
    AuditListResponse:
      type: object
      required:
        - data
        - pagination
        - requestId
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AuditEntry'
        pagination:
          $ref: '#/components/schemas/Pagination'
        requestId:
          type: string
      additionalProperties: false
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
        field:
          type: string
          description: Field that caused the error when the server can identify one.
        suggestedFix:
          type: string
          description: Human- and AI-readable correction hint.
        allowedValues:
          type: array
          items:
            type: string
          description: Allowed enum values when the error is caused by an unsupported enum.
        writableFields:
          type: array
          items:
            type: string
          description: >-
            Writable subscription fields when the error is caused by a
            non-writable field.
      additionalProperties: false
    ErrorResponse:
      type: object
      required:
        - error
        - requestId
      properties:
        error:
          $ref: '#/components/schemas/Error'
        requestId:
          type: string
      additionalProperties: false
