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

# Initiate reverse QR payment (counter scan)

> Called by a POS terminal after scanning the user's QR code. Creates a payment intent with `status=pending_approval` targeted at the scanned user's wallet and publishes a `payment.approval_requested` NATS event so the user's mobile app can display the approval screen. **Requires a merchant JWT** (client credentials flow with the merchant's `client_id`/`client_secret`). `counter_id` is passed in the request body and validated against the DB to confirm it belongs to the authenticated merchant.



## OpenAPI

````yaml /swagger.json post /merchant/api/v1/scan-payment
openapi: 3.0.3
info:
  title: Merchant API
  description: >-
    API for merchants to create and manage payment intents, POS counters, and
    transaction history in the FEX Payment Platform. Authentication uses
    Keycloak JWT (human merchants via password flow, POS terminals via client
    credentials).
  version: 1.0.0
  contact:
    name: FEX Payments Platform
servers:
  - url: http://api-sandbox.fexpayments.com
    description: Merchant API sandbox server
security: []
tags:
  - name: Payment Intents
    description: Create and manage payment intents
  - name: Counters
    description: >-
      POS terminal (counter) management — each counter has its own Keycloak
      client credentials and webhook endpoint
  - name: Admin
    description: Back-office endpoints restricted to platform admin role (`admin`)
  - name: Public
    description: Public endpoints accessible without authentication (for QR code payments)
  - name: Onboarding
    description: Merchant registration — requires platform admin JWT (realm role `admin`)
paths:
  /merchant/api/v1/scan-payment:
    post:
      tags:
        - Counters
      summary: Initiate reverse QR payment (counter scan)
      description: >-
        Called by a POS terminal after scanning the user's QR code. Creates a
        payment intent with `status=pending_approval` targeted at the scanned
        user's wallet and publishes a `payment.approval_requested` NATS event so
        the user's mobile app can display the approval screen. **Requires a
        merchant JWT** (client credentials flow with the merchant's
        `client_id`/`client_secret`). `counter_id` is passed in the request body
        and validated against the DB to confirm it belongs to the authenticated
        merchant.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScanPaymentRequest'
            examples:
              basic:
                summary: Basic scan payment
                value:
                  scan_token: 8566959d-f366-45d9-9e6e-241ba189988f
                  counter_id: c1d2e3f4-a5b6-7890-abcd-ef1234567890
                  amount: 35.5
                  currency: USD
                  description: Lunch combo
              withReference:
                summary: With merchant order reference
                value:
                  scan_token: 8566959d-f366-45d9-9e6e-241ba189988f
                  counter_id: c1d2e3f4-a5b6-7890-abcd-ef1234567890
                  amount: 120
                  currency: USD
                  description: Grocery order
                  reference: ORD-9001
                  expires_in: 300
      responses:
        '201':
          description: Payment intent created — awaiting user approval on mobile app
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  payment_intent:
                    $ref: '#/components/schemas/PaymentIntent'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
      security:
        - BearerAuth: []
components:
  schemas:
    ScanPaymentRequest:
      type: object
      required:
        - scan_token
        - counter_id
        - amount
      description: >-
        Request body for counter-initiated (reverse QR) payment. `counter_id` is
        required and validated server-side to confirm it belongs to the
        authenticated merchant.
      properties:
        scan_token:
          type: string
          format: uuid
          description: >-
            Token read from the user's QR code (currently equals the user's
            wallet ID).
          example: 8566959d-f366-45d9-9e6e-241ba189988f
        counter_id:
          type: string
          format: uuid
          description: POS terminal identity — must belong to the authenticated merchant.
          example: c1d2e3f4-a5b6-7890-abcd-ef1234567890
        amount:
          type: number
          format: double
          example: 35.5
          minimum: 0.01
        currency:
          type: string
          example: USD
          default: USD
        description:
          type: string
          example: Lunch combo
        expires_in:
          type: integer
          description: 'Seconds until approval expires (default: 300 — 5 minutes)'
          example: 300
          default: 300
          minimum: 30
        reference:
          type: string
          nullable: true
          description: Merchant's own order or invoice ID.
          example: ORD-9001
        callback_url:
          type: string
          format: uri
          nullable: true
          description: Per-intent webhook override URL.
          example: https://pos.example.com/webhooks/ORD-9001
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: Arbitrary JSON metadata forwarded verbatim in the webhook payload.
          example:
            table: T12
            cashier: Jane
    PaymentIntent:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
        merchant_id:
          type: string
          format: uuid
          example: 74815c83-e47b-4dc5-aefe-abe4484dd280
        merchant_name:
          type: string
          example: Whole Foods Market
        amount:
          type: number
          format: double
          example: 99.99
        currency:
          type: string
          example: USD
          default: USD
        description:
          type: string
          example: Coffee and pastry
        qr_code_url:
          type: string
          description: Base64-encoded QR code image (data URI)
          example: data:image/png;base64,iVBORw0KGgo...
        qr_data:
          type: string
          description: Raw QR code payload JSON
          example: >-
            {"payment_intent_id":"abc123","amount":99.99,"currency":"USD","merchant_id":"...","merchant_name":"Whole
            Foods Market"}
        status:
          type: string
          enum:
            - pending
            - pending_approval
            - completed
            - expired
            - cancelled
          example: pending
          description: >-
            `pending` — standard QR intent waiting for user scan.
            `pending_approval` — reverse QR intent created by counter, waiting
            for user to approve on mobile app.
        target_wallet_id:
          type: string
          format: uuid
          nullable: true
          description: >-
            For reverse QR intents (`pending_approval`): the wallet ID of the
            user whose QR was scanned. Null for standard (merchant-created) QR
            intents.
          example: 8566959d-f366-45d9-9e6e-241ba189988f
        completed_by:
          type: string
          format: uuid
          nullable: true
          description: >-
            Wallet user_id of the customer who completed the payment. Present
            only when status is `completed`.
          example: d9f3a2b1-c4e5-6789-abcd-ef0123456789
        refund_status:
          type: string
          enum:
            - pending
            - completed
          nullable: true
          description: >-
            Refund state. `null` means no refund has been issued. `pending`
            means refund was submitted to ledger. `completed` means ledger has
            settled the refund.
          example: pending
        refund_ledger_tx_id:
          type: string
          nullable: true
          description: >-
            Idempotency key / ledger transaction reference for the refund
            transaction.
          example: f47ac10b-58cc-4372-a567-0e02b2c3d479
        counter_id:
          type: string
          format: uuid
          nullable: true
          description: POS terminal that created this intent.
          example: c1d2e3f4-a5b6-7890-abcd-ef1234567890
        reference:
          type: string
          nullable: true
          description: Merchant's own order or invoice ID.
          example: ORD-12345
        callback_url:
          type: string
          format: uri
          nullable: true
          description: Per-intent webhook override URL.
          example: https://shop.example.com/orders/ORD-12345/payment-callback
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: Arbitrary JSON metadata set by the merchant at creation time.
          example:
            customer_email: alice@example.com
            cart_id: cart-abc123
        expires_at:
          type: string
          format: date-time
          example: '2026-02-24T14:30:00Z'
        created_at:
          type: string
          format: date-time
          example: '2026-02-24T14:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-02-24T14:00:00Z'
      required:
        - id
        - merchant_id
        - amount
        - currency
        - status
        - expires_at
    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          example: Invalid request body
      required:
        - success
        - error
  responses:
    BadRequest:
      description: Bad request — invalid input
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error: Amount must be greater than 0
    Unauthorized:
      description: Unauthorized — missing or invalid JWT
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error: Unauthorized
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error: Failed to create payment intent
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Keycloak JWT. Roles: `admin` (platform admin — can register merchants,
        act on behalf of any merchant), `merchant` (dashboard user — scoped to
        their own `merchant_id` via user attribute mapper), POS terminals use
        client credentials flow with `merchant_id` injected via protocol mapper.

````