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

# Confirm an online-gateway top-up: credit wallet + accrue reload bonus

> Confirms a paid online-gateway top-up session and credits the customer's wallet, the second step of the two-step online top-up flow. On confirmation, the customer's cashable `balance` is credited and any configured reload bonus is accrued as a promotional grant in the same atomic operation, so the two effects either both apply or neither does.

The credited amount is server-authoritative: it is derived from the finalized provider settlement event, never from POS-submitted totals.

The cashable credit (`balance`) is the customer's actual, spendable money. The reload bonus is accrued as a `RELEASED` promotional grant: promotional balance that can expire and is never cashable. Because the customer is already identified at top-up time, the gateway reload bonus accrues directly to `RELEASED` with no register-to-claim gate.

**This endpoint credits exactly once.** Confirmation flips the deposit transaction from `PENDING` to `COMPLETED` atomically, so concurrent confirmations (for example a duplicate provider webhook and a partner poll arriving together) still credit a single time. Idempotency is enforced on two axes: the `Idempotency-Key` header and, defensively, the provider settlement reference (`provider_payment_ref`). A fresh `partner_request_id` that reuses a prior `Idempotency-Key` replays the original credit without double-crediting. A reused `Idempotency-Key` paired with a different request payload returns `IDEMPOTENCY_KEY_REUSED`.



## OpenAPI

````yaml /api-reference/openapi.yaml post /topup/confirm
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:
  /topup/confirm:
    post:
      tags:
        - topup
      summary: 'Confirm an online-gateway top-up: credit wallet + accrue reload bonus'
      description: >-
        Confirms a paid online-gateway top-up session and credits the customer's
        wallet, the second step of the two-step online top-up flow. On
        confirmation, the customer's cashable `balance` is credited and any
        configured reload bonus is accrued as a promotional grant in the same
        atomic operation, so the two effects either both apply or neither does.


        The credited amount is server-authoritative: it is derived from the
        finalized provider settlement event, never from POS-submitted totals.


        The cashable credit (`balance`) is the customer's actual, spendable
        money. The reload bonus is accrued as a `RELEASED` promotional grant:
        promotional balance that can expire and is never cashable. Because the
        customer is already identified at top-up time, the gateway reload bonus
        accrues directly to `RELEASED` with no register-to-claim gate.


        **This endpoint credits exactly once.** Confirmation flips the deposit
        transaction from `PENDING` to `COMPLETED` atomically, so concurrent
        confirmations (for example a duplicate provider webhook and a partner
        poll arriving together) still credit a single time. Idempotency is
        enforced on two axes: the `Idempotency-Key` header and, defensively, the
        provider settlement reference (`provider_payment_ref`). A fresh
        `partner_request_id` that reuses a prior `Idempotency-Key` replays the
        original credit without double-crediting. A reused `Idempotency-Key`
        paired with a different request payload returns
        `IDEMPOTENCY_KEY_REUSED`.
      operationId: topupConfirm
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TopupConfirmRequest'
            example:
              meta:
                partner_request_id: f3a1...
                occurred_at: '2026-06-05T09:15:00Z'
                sent_at: '2026-06-05T09:15: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
                partner_session_id: ord-99812
              session_ref: sadad-sess-7f3a91
              provider: SADAD
              provider_payment_ref: sadad-trans-0099812
              amount_minor: 5000
              currency: QAR
      responses:
        '200':
          description: >-
            Wallet credited (or idempotent replay of a prior credit).
            `meta.idempotency_replayed` is true on a replay.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/TopupResult'
              example:
                ok: true
                data:
                  transaction_id: tx_77c1
                  status: completed
                  wallet_id: wal_5512
                  credited_minor: 5000
                  bonus_minor: 500
                  balance_after_minor: 12500
                  promo_balance_after_minor: 500
                  currency: QAR
                  reload_bonus:
                    applied: true
                    bonus_minor: 500
                    promo_grant_id: pg_aa01
                    promo_grant_state: RELEASED
                    source: GATEWAY_BONUS
                    expires_at: '2026-09-05T00:00:00Z'
                error: null
                meta:
                  request_id: req_c4d5
                  idempotency_replayed: false
                  api_version: '2026-06-01'
                  data_completeness_score: 70
        '400':
          description: >-
            `VALIDATION_ERROR` (HTTP 400), malformed body, missing required
            field, or a domain rule violated. `details` carries the field
            errors.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '401':
          description: >-
            `INVALID_API_KEY` (HTTP 401), missing/invalid `x-api-key` or 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'
        '403':
          description: >-
            `FORBIDDEN` (HTTP 403), the key is valid but not authorized for this
            merchant/branch scope or this operation (cross-tenant or
            insufficient RBAC).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '404':
          description: >-
            `NOT_FOUND`, the prepared session / provider payment reference does
            not resolve to a pending deposit for this integration.
          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),
            `CURRENCY_NOT_SUPPORTED`, or `CONFLICT` when the POS-submitted
            `amount_minor` disagrees with the finalized provider event (the
            provider event is the source of truth; on mismatch NO credit is
            applied, the POS reconciles and retries with the corrected amount).
          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:
    TopupConfirmRequest:
      type: object
      description: >-
        Body for POST /topup/confirm. Idempotent on the Idempotency-Key header
        and defensively on provider_payment_ref.
      required:
        - context
        - provider
        - provider_payment_ref
        - currency
      properties:
        meta:
          $ref: '#/components/schemas/RequestMeta'
        context:
          $ref: '#/components/schemas/RequestContext'
        session_ref:
          type:
            - string
            - 'null'
          description: The prepared provider session reference.
        provider:
          type: string
          enum:
            - SADAD
            - STRIPE
          description: Provider that processed the charge.
        provider_payment_ref:
          type: string
          description: >-
            The provider's settled payment reference (Stripe PaymentIntent id /
            Sadad trans_no). Secondary idempotency anchor.
        amount_minor:
          type:
            - integer
            - 'null'
          minimum: 1
          description: >-
            Expected amount; the credited amount is derived server-side from the
            finalized provider event and reconciled against this.
        currency:
          type: string
          minLength: 3
          maxLength: 3
          example: QAR
    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'
    TopupResult:
      type: object
      description: >-
        Result of a completed top-up credit (confirm or SKU sale). Carries both
        money classes after the credit.
      required:
        - transaction_id
        - status
        - wallet_id
        - credited_minor
        - currency
      properties:
        transaction_id:
          type: string
          description: The DEPOSIT transaction id.
        status:
          type: string
          enum:
            - pending
            - completed
            - failed
            - escrowed
          description: >-
            completed=credited; escrowed=held for a PENDING_PROOF customer until
            they register.
        wallet_id:
          type: string
        credited_minor:
          type: integer
          description: Actual (cashable) balance credited, minor units.
        bonus_minor:
          type: integer
          description: Promotional bonus accrued, minor units (0 if none).
        balance_after_minor:
          type:
            - integer
            - 'null'
          description: Actual balance after the credit (null when escrowed).
        promo_balance_after_minor:
          type:
            - integer
            - 'null'
          description: RELEASED promo balance after the accrual.
        currency:
          type: string
        customer_state:
          type:
            - string
            - 'null'
          enum:
            - verified
            - pending_proof
            - null
          description: Resolved customer identity state at credit time.
        reload_bonus:
          $ref: '#/components/schemas/ReloadBonusResult'
    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.
    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.
    ReloadBonusResult:
      type: object
      description: >-
        The reload bonus accrued by a top-up, if any. The bonus is a RELEASED
        promotional grant (promo balance, expires, never cashable) for an
        identified customer, or a LOCKED cashback-class grant for a
        PENDING_PROOF customer.
      required:
        - applied
      properties:
        applied:
          type: boolean
          description: Whether a reload bonus was accrued.
        bonus_minor:
          type: integer
          description: Bonus amount in minor units.
        promo_grant_id:
          type:
            - string
            - 'null'
          description: The promotional grant ledger row id.
        promo_grant_state:
          type:
            - string
            - 'null'
          enum:
            - LOCKED
            - RELEASED
            - CLAWED_BACK
            - EXPIRED
            - null
          description: >-
            Initial grant state: RELEASED for identified top-ups, LOCKED for
            PENDING_PROOF cashback-class. Same casing as promotional
            grant.state`.
        source:
          type:
            - string
            - 'null'
          enum:
            - RELOAD_BONUS
            - SKU_TOPUP_BONUS
            - GATEWAY_BONUS
            - CASHBACK
            - SIGNUP_BONUS
            - null
          description: 'promotional grant source: which lane accrued this grant.'
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When the promo grant expires if unspent.
  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).

````