> ## Documentation Index
> Fetch the complete documentation index at: https://docs.feddi.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Authorize (and immediately debit) a wallet payment via a credential

> Present a customer credential (`otp`, rotating QR nonce, 5-char `short_code`, or `pass_tap`) and debit the wallet atomically. There is no reservation or hold: the debit is immediate. Promotional grants are spent **first** (FIFO by expiry) then actual balance; the result reports the split as `debited_promo_minor` and `debited_actual_minor`. When `credential.type` is `otp`, the credential carries the `otp_session_id` returned by `POST /payments/otp/send` (the trigger leg) plus the code the customer received; the OTP is consumed atomically with the debit, so a failed debit (for example `INSUFFICIENT_FUNDS`) does **not** consume the OTP and it remains retryable. QR nonces are single-use; a replayed nonce returns `CREDENTIAL_EXPIRED_OR_REPLAYED`. The credential is a discriminated union on `credential.type`, so additional types extend the enum without changing the route. Idempotent on the `Idempotency-Key` header: a same-key, same-payload retry replays the original result byte-identical, and a same-key, different-payload request returns `IDEMPOTENCY_KEY_REUSED`. Cross-tenant calls (a `merchant_id` not authorized for the integration) resolve to `FORBIDDEN`. Terminal and cashier-driven flows (OTP, QR) authenticate with the `PosTerminalJWT` scheme. Identity-verified wallets only: a `pending_proof` wallet may not pay. **This resource returns the debit result, never a standalone debit or authorization token.**



## OpenAPI

````yaml /api-reference/openapi.yaml post /payments
openapi: 3.1.0
info:
  title: Feddi Partner API
  version: '2026-06-01'
  description: >-
    Operate a Feddi closed-loop wallet and loyalty program at the point of sale:
    identify customers, read balances, accept wallet payments, run top-ups, and
    reconcile transactions.


    All amounts are integer minor units with an explicit ISO-4217 currency.
    Balances are merchant-held and closed-loop.


    Every response uses a typed envelope (`ok` / `data` / `error` / `meta`) with
    string error codes (never a bare HTTP number) and an idempotency-replay
    flag. Authenticate with the `x-api-key` header; exchange it for a
    short-lived POS terminal JWT for hot-path calls.


    Self-validate against this document (`GET /openapi`) and read `GET
    /capabilities` for the credential types, currencies, and features enabled
    for your integration before assuming any enum.


    Operations are tagged `x-feddi-availability: ga` (stable) or `beta`
    (callable, contract may still change additively); confirm what is enabled
    for your credentials via `GET /capabilities`.


    Sandbox access is granted per partner agreement; request credentials from
    your Feddi contact.
servers:
  - url: https://api.feddi.io/v1/partner
    description: production
  - url: https://api.dev.feddi.io/v1/partner
    description: dev
security:
  - ApiKeyAuth: []
tags:
  - name: platform
    description: >-
      Platform cross-cutting: health, capabilities, openapi self-serve, merchant
      provisioning, settlement, reconciliation.
  - name: checkout_session
    description: >-
      Checkout sessions: every interaction opens a session, then identity and
      basket attach to it, and payment, top-up, and offers run against it.
  - name: auth
    description: >-
      Partner authentication + onboarding: API key lifecycle, POS terminal JWT
      exchange, terminal heartbeat.
  - name: customers
    description: >-
      Customer lookup + identification: resolve identity, cashier-panel
      summaries, preferences, GDPR export/erase.
  - name: enrollment
    description: >-
      Enrollment + signup: OTP enroll, cashback claims, identity/consent,
      customer correction + merge.
  - name: payments
    description: >-
      Payments + redemption: debit wallet (promo-first), balance-check, void,
      refund, QR mint.
  - name: topup
    description: >-
      Wallet top-up: 2-step prepare/confirm, reload-bonus grants, SKU top-up,
      settlement + reconciliation.
  - name: incentives
    description: >-
      Incentives + offers: offer feeds, apply/redeem/release locks, proposals,
      budget envelopes, points, grant clawback.
  - name: transactions
    description: >-
      Transactions + receipts: transaction detail, void, receipts, disputes,
      settlements, reconciliation, exports.
  - name: webhooks
    description: >-
      Webhooks + events: subscriptions, delivery history + retry, event catalog,
      polling fallback.
paths:
  /payments:
    post:
      tags:
        - payments
      summary: Authorize (and immediately debit) a wallet payment via a credential
      description: >-
        Present a customer credential (`otp`, rotating QR nonce, 5-char
        `short_code`, or `pass_tap`) and debit the wallet atomically. There is
        no reservation or hold: the debit is immediate. Promotional grants are
        spent **first** (FIFO by expiry) then actual balance; the result reports
        the split as `debited_promo_minor` and `debited_actual_minor`. When
        `credential.type` is `otp`, the credential carries the `otp_session_id`
        returned by `POST /payments/otp/send` (the trigger leg) plus the code
        the customer received; the OTP is consumed atomically with the debit, so
        a failed debit (for example `INSUFFICIENT_FUNDS`) does **not** consume
        the OTP and it remains retryable. QR nonces are single-use; a replayed
        nonce returns `CREDENTIAL_EXPIRED_OR_REPLAYED`. The credential is a
        discriminated union on `credential.type`, so additional types extend the
        enum without changing the route. Idempotent on the `Idempotency-Key`
        header: a same-key, same-payload retry replays the original result
        byte-identical, and a same-key, different-payload request returns
        `IDEMPOTENCY_KEY_REUSED`. Cross-tenant calls (a `merchant_id` not
        authorized for the integration) resolve to `FORBIDDEN`. Terminal and
        cashier-driven flows (OTP, QR) authenticate with the `PosTerminalJWT`
        scheme. Identity-verified wallets only: a `pending_proof` wallet may not
        pay. **This resource returns the debit result, never a standalone debit
        or authorization token.**
      operationId: paymentAuthorize
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentRequest'
            example:
              meta:
                partner_request_id: a1b2c3d4-0001-0001-0001-000000000001
                occurred_at: '2026-06-05T10:00:00Z'
                sent_at: '2026-06-05T10:00:01Z'
                api_version: '2026-06-01'
              context:
                merchant_id: 11111111-1111-1111-1111-111111111111
                branch_id: 22222222-2222-2222-2222-222222222222
                terminal_id: POS-360-0007
                cashier_id: cashier-42
                partner_session_id: ord-88812
              credential:
                type: otp
                phone: '+97433001122'
                otp_code: '483921'
                otp_session_id: otps_cc01
              amount_minor: 3500
              currency: QAR
              order_ref: ord-88812
              basket:
                lines:
                  - sku: COFFEE-001
                    quantity: 2
                    unit_price_minor: 1500
                    extended_price_minor: 3000
                  - sku: PASTRY-007
                    quantity: 1
                    unit_price_minor: 500
                    extended_price_minor: 500
      responses:
        '200':
          description: Wallet debited. `meta.idempotency_replayed` is true on a replay.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PaymentResult'
              example:
                ok: true
                data:
                  payment_id: pay_77c1
                  status: completed
                  wallet_id: wal_5512
                  amount_minor: 3500
                  debited_promo_minor: 500
                  debited_actual_minor: 3000
                  currency: QAR
                  order_ref: ord-88812
                  balance_after:
                    actual_minor: 9500
                    promo_available_minor: 0
                    promo_locked_minor: 200
                    pending_topups_minor: 0
                    currency: QAR
                    promo_grants: []
                  completed_at: '2026-06-05T10:00:01Z'
                error: null
                meta:
                  request_id: req_p1
                  idempotency_replayed: false
                  api_version: '2026-06-01'
                  data_completeness_score: 80
        '400':
          description: >-
            `VALIDATION_ERROR`, malformed body, missing required field, expired
            OTP (`OTP_EXPIRED`), replayed QR nonce
            (`CREDENTIAL_EXPIRED_OR_REPLAYED`), invalid short-code, or wrong OTP
            (typed `OTP_INVALID`). `details` carries field errors.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '401':
          description: >-
            `INVALID_API_KEY`, missing/invalid POS terminal JWT (expired,
            malformed, or wrong audience).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '402':
          description: >-
            `INSUFFICIENT_FUNDS`, the wallet balance (actual + released promo
            combined) is less than `amount_minor`. Includes `shortfall_minor`
            and the current balance breakdown. The credential is NOT consumed;
            the caller may retry with a lower amount or trigger a top-up.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
              example:
                ok: false
                data: null
                error:
                  code: INSUFFICIENT_FUNDS
                  message: Wallet balance is insufficient for this payment.
                  details:
                    shortfall_minor: 1200
                    available_actual_minor: 1800
                    available_promo_minor: 500
                    currency: QAR
                meta:
                  request_id: req_p2
                  idempotency_replayed: false
                  api_version: '2026-06-01'
        '403':
          description: >-
            `FORBIDDEN`, the terminal JWT is valid but not authorized for this
            merchant/branch, or the customer's wallet identity state is
            `PENDING_PROOF` (payments blocked until verified).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '404':
          description: >-
            `NOT_FOUND`, wallet not found for this identity in this enterprise.
            Cross-tenant lookups also resolve to 404 (no PII leak).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '422':
          description: >-
            `IDEMPOTENCY_KEY_REUSED` (same key, different payload) or
            `WALLET_PROGRAM_AMBIGUOUS` (customer maps to multiple programs;
            candidates listed) or `CREDENTIAL_TYPE_UNSUPPORTED` (credential type
            not enabled for this integration; lists supported types).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '429':
          description: >-
            `RATE_LIMITED`, OTP send rate limit (3 sends per order within 5
            minutes) or OTP verify attempt limit (3 wrong attempts locks the
            session). `Retry-After` header is set.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
      security:
        - PosTerminalJWT: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: >-
        Partner-generated UUID; the ONE dedup key for all mutating endpoints.
        24h TTL. Same key + same payload -> byte-identical replay; same key +
        different payload -> IDEMPOTENCY_KEY_REUSED (422).
      schema:
        type: string
        format: uuid
  schemas:
    PaymentRequest:
      type: object
      required:
        - meta
        - context
        - credential
        - amount_minor
        - currency
      properties:
        meta:
          $ref: '#/components/schemas/RequestMeta'
        context:
          $ref: '#/components/schemas/RequestContext'
        credential:
          $ref: '#/components/schemas/PaymentCredential'
        amount_minor:
          type: integer
          minimum: 1
          description: >-
            Amount to debit in the currency's minor unit (e.g. fils for QAR,
            halalas for SAR). Server-authoritative; the debit is exactly this
            value.
          example: 3500
        currency:
          type: string
          description: ISO-4217 currency code.
          example: QAR
        order_ref:
          type: string
          description: >-
            Partner's order or receipt reference, stored for reconciliation.
            Maps to `context.partner_session_id` if absent. For an `otp`
            credential this effective value (order_ref, or partner_session_id
            when absent) must equal the `order_ref` the OTP session was minted
            with at `POST /payments/otp/send`; a mismatch fails the anti-replay
            check.
          example: ord-88812
        basket:
          type: object
          description: >-
            Basket lines. Optional but increases `data_completeness_score`. Line
            totals are captured but NOT the debit basis, `amount_minor` is.
          properties:
            lines:
              type: array
              items:
                type: object
                required:
                  - sku
                  - quantity
                  - unit_price_minor
                  - extended_price_minor
                properties:
                  sku:
                    type: string
                  quantity:
                    type: number
                  unit_price_minor:
                    type: integer
                  extended_price_minor:
                    type: integer
                  category:
                    type: string
                  discount_minor:
                    type: integer
    ResponseEnvelope:
      type: object
      description: >-
        The standard response envelope that every enveloped endpoint serializes
        through. `ok` is a boolean discriminator: when `ok` is `true`, the typed
        result is carried in `data`; when `ok` is `false`, a typed error object
        is returned instead. The `meta` object is uniform across the entire API
        surface.
      required:
        - ok
        - meta
      properties:
        ok:
          type: boolean
        data:
          type:
            - object
            - 'null'
        error:
          oneOf:
            - $ref: '#/components/schemas/Error'
            - type: 'null'
        meta:
          $ref: '#/components/schemas/Meta'
    PaymentResult:
      type: object
      required:
        - payment_id
        - status
        - wallet_id
        - amount_minor
        - debited_promo_minor
        - debited_actual_minor
        - currency
        - balance_after
      properties:
        payment_id:
          type: string
          description: Feddi-assigned payment id.
          example: pay_77c1
        status:
          type: string
          enum:
            - completed
            - voided
            - pending
          description: >-
            `completed` = debit landed; `voided` = reversed; `pending` =
            RESERVED for v2 provider-async settlement legs, v1 NEVER returns it
            (v1 debits are atomic: you get `completed` or a typed error). Handle
            defensively as forward-compat by polling `GET
            /{integrationId}/transactions/{transactionId}`.
        wallet_id:
          type: string
          example: wal_5512
        amount_minor:
          type: integer
          description: Total amount debited (promo + actual).
          example: 3500
        debited_promo_minor:
          type: integer
          description: >-
            Portion of the debit drawn from released promo grants (FIFO by
            expiry). Zero if no promo was available.
          example: 500
        debited_actual_minor:
          type: integer
          description: Portion of the debit drawn from the actual (real-money) balance.
          example: 3000
        currency:
          type: string
          example: QAR
        order_ref:
          type: string
          example: ord-88812
        balance_after:
          $ref: '#/components/schemas/Balance'
        completed_at:
          type: string
          format: date-time
    Error:
      type: object
      description: >-
        The typed error object returned with every non-2xx response. `code` is a
        string enum (for example `WALLET_PROGRAM_AMBIGUOUS`,
        `CREDENTIAL_TYPE_UNSUPPORTED`, `INSUFFICIENT_FUNDS`,
        `IDEMPOTENCY_KEY_REUSED`, `CURRENCY_NOT_SUPPORTED`), **never a bare HTTP
        status number**. Inspect `code` for programmatic branching, not the HTTP
        status. Some codes echo the valid set in `details` so clients can
        present or reconcile the accepted values: for example
        `CREDENTIAL_TYPE_UNSUPPORTED` lists the supported credential types,
        `CURRENCY_NOT_SUPPORTED` lists the supported ISO-4217 currencies, and
        `WALLET_PROGRAM_AMBIGUOUS` lists the candidate `wallet_program_id`
        values that matched the request.
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - INSUFFICIENT_FUNDS
            - INVALID_API_KEY
            - CREDENTIAL_TYPE_UNSUPPORTED
            - CURRENCY_NOT_SUPPORTED
            - IDEMPOTENCY_KEY_REUSED
            - WALLET_PROGRAM_AMBIGUOUS
            - REQUIRES_DYNAMIC_CREDENTIAL
            - NOT_FOUND
            - FORBIDDEN
            - VALIDATION_ERROR
            - RATE_LIMITED
            - CONFLICT
            - INTERNAL_SERVER_ERROR
        message:
          type: string
        details:
          type: object
          additionalProperties: true
    RequestMeta:
      type: object
      description: >-
        Common request metadata shared across all Partner API domains.
        `partner_request_id` is a correlation identifier only: it appears in
        logs and responses for tracing but is **never used as the deduplication
        key**. Request deduplication is keyed exclusively on the
        `Idempotency-Key` header.
      required:
        - partner_request_id
        - api_version
      properties:
        partner_request_id:
          type: string
          format: uuid
          description: Partner's own correlation id. NOT the idempotency basis.
        occurred_at:
          type: string
          format: date-time
          description: >-
            When the event happened at the POS (RFC3339). Clock-drift signal vs
            sent_at.
        sent_at:
          type: string
          format: date-time
          description: When the partner sent the request (RFC3339).
        api_version:
          type: string
          description: The single API version field.
          example: '2026-06-01'
    RequestContext:
      type: object
      description: >-
        Canonical request context, shared across all partner domains.
        `merchant_id` is key-enforced to a merchant the calling credential is
        authorized for (a platform-scoped credential acts only on its own
        tenants), so it is **never a free-text trust field**: a `merchant_id`
        outside the credential's authority is rejected rather than honored.
      required:
        - merchant_id
      properties:
        merchant_id:
          type: string
          format: uuid
          description: Resolved Feddi tenant (enterprise).
        branch_id:
          type:
            - string
            - 'null'
          format: uuid
        terminal_id:
          type:
            - string
            - 'null'
        cashier_id:
          type:
            - string
            - 'null'
          description: Server-trusted only when signed into the terminal JWT.
        partner_session_id:
          type:
            - string
            - 'null'
          description: The partner's own order/session ref.
        feddi_session_id:
          type:
            - string
            - 'null'
          format: uuid
          description: >-
            The checkout_session this call anchors to. On session sub-resource
            calls it is redundant with the path `{id}` and must match it.
    PaymentCredential:
      type: object
      required:
        - type
      discriminator:
        propertyName: type
        mapping:
          otp:
            $ref: '#/components/schemas/OtpCredential'
          qr:
            $ref: '#/components/schemas/QrCredential'
          short_code:
            $ref: '#/components/schemas/ShortCodeCredential'
          pass_tap:
            $ref: '#/components/schemas/PassTapCredential'
      oneOf:
        - $ref: '#/components/schemas/OtpCredential'
        - $ref: '#/components/schemas/QrCredential'
        - $ref: '#/components/schemas/ShortCodeCredential'
        - $ref: '#/components/schemas/PassTapCredential'
    Meta:
      type: object
      description: >-
        The uniform response metadata block returned on every response.
        `data_completeness_score` is computed per call and reports the
        completeness of the returned data. `decision_trace_id` is present on
        responses that carry decision or insight output and can be used to
        correlate the response with its reasoning. `capabilities` is an
        additive, response-level array of hints advertising features the caller
        may use, and may be extended over time without notice.
      required:
        - request_id
        - api_version
      properties:
        request_id:
          type: string
          description: Feddi-issued correlation id for this response.
        idempotency_replayed:
          type: boolean
          description: True when this response was replayed from the idempotency store.
        api_version:
          type: string
          description: The single API version field.
          example: '2026-06-01'
        data_completeness_score:
          type: integer
          minimum: 0
          maximum: 100
          description: >-
            0-100, computed per call (basket/identity/tax/category/consent
            presence).
        decision_trace_id:
          type:
            - string
            - 'null'
          description: Opaque trace id on intelligence-bearing responses; one per response.
        capabilities:
          type: object
          additionalProperties: true
          description: Additive response-level capability hints.
    Balance:
      type: object
      required:
        - actual_minor
        - promo_available_minor
        - promo_locked_minor
        - pending_topups_minor
        - currency
      description: >-
        Three-class wallet balance per the resolved money model. Actual = real
        money (deferred-revenue liability). Promo = separate contingent
        promotional class (expirable, locked until activation condition met,
        promo-first debit, clawback-able, NOT a real liability).
      properties:
        wallet_id:
          type: string
          description: Feddi wallet id (omitted when nested in PaymentResult).
          example: wal_5512
        wallet_program_id:
          type: string
          description: >-
            Which wallet program this balance is against. First-class to prevent
            multi-program ambiguity.
          example: wp_77
        actual_minor:
          type: integer
          description: >-
            Real-money balance, funded by customer top-ups. A real
            deferred-revenue liability the merchant tracks carefully. Spent on
            purchases after promo is exhausted.
          example: 12000
        promo_available_minor:
          type: integer
          description: >-
            Sum of the `remaining_minor` across all released, non-expired
            promotional grants: promotional credits the customer can spend
            immediately (promo-first). Not real money; a contingent promotional
            class.
          example: 500
        promo_locked_minor:
          type: integer
          description: >-
            Sum of the `remaining_minor` across all locked promotional grants:
            promotional credits gated on an activation condition (for example
            wallet signup or first top-up). The customer cannot spend these
            until they are released.
          example: 200
        pending_topups_minor:
          type: integer
          description: >-
            In-flight top-ups not yet confirmed (PENDING state). Informational
            only; not spendable.
          example: 0
        currency:
          type: string
          example: QAR
        customer_state:
          type: string
          enum:
            - verified
            - pending_proof
          description: >-
            Wallet identity state. `pending_proof` = customer not yet verified
            (actual balance is credited; cashback-class promo stays `LOCKED`;
            payments blocked).
        promo_grants:
          type: array
          description: >-
            Itemized promotional grant breakdown. Present when reading balance;
            may be an empty array on tight payment result objects.
          items:
            $ref: '#/components/schemas/PromoGrantSummary'
    OtpCredential:
      type: object
      required:
        - type
        - phone
        - otp_code
        - otp_session_id
      properties:
        type:
          type: string
          enum:
            - otp
        phone:
          type: string
          description: Customer phone in E.164 format. Used to resolve the wallet.
          example: '+97433001122'
        otp_code:
          type: string
          description: The 6-digit OTP the customer received via SMS.
          example: '483921'
        otp_session_id:
          type: string
          description: >-
            The OTP session id returned by `POST /payments/otp/send` (bound to
            this order, amount, and integration). Prevents cross-order OTP
            replay.
          example: otps_cc01
    QrCredential:
      type: object
      required:
        - type
        - qr_payload
      properties:
        type:
          type: string
          enum:
            - qr
        qr_payload:
          type: string
          description: >-
            The raw QR payload string (the feddi:// URI or the signed nonce JWT)
            scanned from the customer's wallet app.
          example: feddi://pay?nonce=eyJhbGciOiJFUzI1NiJ9...&wallet=wal_5512
    ShortCodeCredential:
      type: object
      required:
        - type
        - short_code
      properties:
        type:
          type: string
          enum:
            - short_code
        short_code:
          type: string
          description: >-
            The 5-character alphanumeric short-code displayed in the customer's
            wallet app (alternative to QR scan for manual entry on the cashier
            terminal).
          minLength: 5
          maxLength: 5
          example: A7X3Q
    PassTapCredential:
      type: object
      required:
        - type
        - pass_token
      properties:
        type:
          type: string
          enum:
            - pass_tap
        pass_token:
          type: string
          description: >-
            NFC/contactless pass token from a physical or digital pass tap.
            Reserved for v2; returns `CREDENTIAL_TYPE_UNSUPPORTED` if not
            enabled for the integration.
          example: tap_eyJhbGciOiJFUzI1NiJ9...
    PromoGrantSummary:
      type: object
      required:
        - source
        - state
        - remaining_minor
        - expires_at
      properties:
        promo_grant_id:
          type: string
          example: pg_aa01
        source:
          type: string
          enum:
            - CASHBACK
            - RELOAD_BONUS
            - SKU_TOPUP_BONUS
            - GATEWAY_BONUS
            - SIGNUP_BONUS
          description: >-
            Origin of the promo grant, maps to the promotional-grant source
            enum.
        state:
          type: string
          enum:
            - RELEASED
            - LOCKED
            - CLAWED_BACK
            - EXPIRED
          description: >-
            `RELEASED` = spendable; `LOCKED` = gated on activation condition;
            `CLAWED_BACK` = reversal clawed it back; `EXPIRED` = past TTL. Grant
            state is UPPERCASE everywhere grants appear; claim states are a
            separate (lowercase) machine.
        remaining_minor:
          type: integer
          example: 500
        expires_at:
          type: string
          format: date-time
          example: '2026-09-01T00:00:00Z'
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Partner API key (platform- or merchant-scoped). Contract key-enforces
        context.merchant_id.
    PosTerminalJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        POS terminal JWT minted by /auth/token; carries integrationId +
        terminal/cashier claims (server-trusted).

````