openapi: 3.0.3
info:
  title: Payments Integration API
  version: 1.0.0
  description: |
    API contract for external payment integrations.

    Third-party integrators implement this API as a **standalone REST service**.
    LoyaltyPlant (payments-service) calls these endpoints to delegate payment
    operations to your integration; your service in turn talks to your PSP
    (payment service provider). You never call payments-service to run a
    payment — payments-service drives the flow by calling you.
externalDocs:
  description: See integration guides for a full walkthrough of the interation.
  url: /payments/
servers:
  - url: https://{host}
    description: |
      Your integration service base URL — the value LoyaltyPlant registers for
      you during onboarding. payments-service appends the version segment and
      operation path (e.g. https://{host}/v1/authorize).
    variables:
      host:
        default: payments.your-domain.example
        description: >-
          Public host of your integration service, reachable from
          payments-service over TLS.
tags:
  - name: Payment Operations
    description: Core payment lifecycle operations
  - name: Integration Management
    description: Capability declaration and health monitoring
paths:
  /v1/authorize:
    post:
      summary: Authorize Payment
      description: |
        Place a hold on the customer's funds without capturing them (first leg
        of a two-phase payment). payments-service calls this when your
        integration declares the `CAPTURE` capability; settle later with
        `/v1/capture`, or release with `/v1/reverse`.

        Return `SUCCESS` with a `transactionReference` on an immediate approval,
        `PENDING` + `redirectUrl` if 3DS/redirect is required (see the
        `integrationResult` callback), or `DECLINED`/`RETRIABLE`/`UNKNOWN`.
        Idempotent on `Idempotency-Key`.
      tags:
        - Payment Operations
      operationId: authorize
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntegrationAuthorizeRequest'
            examples:
              authorize:
                $ref: '#/components/examples/AuthorizeRequestExample'
      responses:
        '200':
          description: Authorization result (business outcome in the `status` field)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationOperationResponse'
              examples:
                success:
                  $ref: '#/components/examples/SuccessResponseExample'
                pending3ds:
                  $ref: '#/components/examples/Pending3dsResponseExample'
                declined:
                  $ref: '#/components/examples/DeclinedResponseExample'
                retriable:
                  $ref: '#/components/examples/RetriableResponseExample'
                unknown:
                  $ref: '#/components/examples/UnknownResponseExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
        '501':
          $ref: '#/components/responses/NotImplemented'
      callbacks:
        integrationResult:
          $ref: '#/components/callbacks/IntegrationResultCallback'
  /v1/capture:
    post:
      summary: Capture Authorized Payment
      description: |
        Settle (capture) funds previously held by `/v1/authorize`. Requires the
        `CAPTURE` capability. `transactionReference` is the value you returned
        from `authorize`. `amount` is optional — omit it to capture the full
        authorized amount, or pass a smaller value for a partial capture.
        Idempotent on `Idempotency-Key`.
      tags:
        - Payment Operations
      operationId: capture
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntegrationCaptureRequest'
            examples:
              capture:
                $ref: '#/components/examples/CaptureRequestExample'
      responses:
        '200':
          description: Capture result (business outcome in the `status` field)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationOperationResponse'
              examples:
                success:
                  $ref: '#/components/examples/SuccessResponseExample'
                declined:
                  $ref: '#/components/examples/DeclinedResponseExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
        '501':
          $ref: '#/components/responses/NotImplemented'
  /v1/sale:
    post:
      summary: Sale Payment (Authorize + Capture in one step)
      description: |
        Authorize and capture in a single call (single-phase payment).
        payments-service calls this when your integration declares the `SALE`
        capability. Return `SUCCESS` with a `transactionReference`, `PENDING` +
        `redirectUrl` for 3DS/redirect (see the `integrationResult` callback),
        or `DECLINED`/`RETRIABLE`/`UNKNOWN`. Idempotent on `Idempotency-Key`.
      tags:
        - Payment Operations
      operationId: sale
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntegrationSaleRequest'
            examples:
              sale:
                $ref: '#/components/examples/SaleRequestExample'
      responses:
        '200':
          description: Sale result (business outcome in the `status` field)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationOperationResponse'
              examples:
                success:
                  $ref: '#/components/examples/SuccessResponseExample'
                pending3ds:
                  $ref: '#/components/examples/Pending3dsResponseExample'
                declined:
                  $ref: '#/components/examples/DeclinedResponseExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
        '501':
          $ref: '#/components/responses/NotImplemented'
      callbacks:
        integrationResult:
          $ref: '#/components/callbacks/IntegrationResultCallback'
  /v1/refund:
    post:
      summary: Refund Payment
      description: |
        Return funds for a captured payment. Requires the `DIRECT_REFUND`
        capability. `transactionReference` identifies the original payment;
        `amount` is the amount to refund (supports partial refunds).
        Idempotent on `Idempotency-Key`.
      tags:
        - Payment Operations
      operationId: refund
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntegrationRefundRequest'
            examples:
              refund:
                $ref: '#/components/examples/RefundRequestExample'
      responses:
        '200':
          description: Refund result (business outcome in the `status` field)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationOperationResponse'
              examples:
                success:
                  $ref: '#/components/examples/SuccessResponseExample'
                declined:
                  $ref: '#/components/examples/DeclinedResponseExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
        '501':
          $ref: '#/components/responses/NotImplemented'
  /v1/reverse:
    post:
      summary: Reverse/Void Payment
      description: |
        Void an authorization before it is captured (releases the hold).
        `transactionReference` is the value returned from `authorize`.
        Idempotent on `Idempotency-Key`.
      tags:
        - Payment Operations
      operationId: reverse
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntegrationReverseRequest'
            examples:
              reverse:
                $ref: '#/components/examples/ReverseRequestExample'
      responses:
        '200':
          description: Reverse result (business outcome in the `status` field)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationOperationResponse'
              examples:
                success:
                  $ref: '#/components/examples/SuccessResponseExample'
                declined:
                  $ref: '#/components/examples/DeclinedResponseExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
        '501':
          $ref: '#/components/responses/NotImplemented'
  /v1/inquire-status:
    post:
      summary: Inquire Payment Status from PSP
      description: >
        Queries the PSP for the current status of a previously initiated
        payment.

        Used for reconciliation and manual status checks (e.g. after a timeout
        or

        an `UNKNOWN` result, or when an async callback never arrived).

        Requires STATUS_POLLING capability. This is a read-only operation and

        carries no `Idempotency-Key`.
      tags:
        - Payment Operations
      operationId: inquireStatus
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntegrationStatusInquiryRequest'
            examples:
              inquiry:
                $ref: '#/components/examples/StatusInquiryRequestExample'
      responses:
        '200':
          description: Status inquiry result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationStatusInquiryResponse'
              examples:
                captured:
                  $ref: '#/components/examples/StatusInquiryResponseExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
        '501':
          $ref: '#/components/responses/NotImplemented'
  /v1/capabilities:
    get:
      summary: Get Supported Capabilities
      description: |
        Returns the list of payment capabilities this integration supports.
        Payments-service caches the result and validates capabilities
        before invoking optional operations — it will never call an operation
        whose capability you did not declare. Declare only what you have
        actually implemented and tested.
      tags:
        - Integration Management
      operationId: getCapabilities
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
      responses:
        '200':
          description: List of supported capabilities
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationCapabilitiesResponse'
              examples:
                twoPhase:
                  $ref: '#/components/examples/CapabilitiesResponseExample'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/health:
    get:
      summary: Health Check
      description: |
        Returns the health status of the integration service.
        Payments-service polls this endpoint periodically to decide whether to
        route traffic to your integration. Return `DOWN` (or `503`) when your
        PSP connection is unavailable so payments-service can stop sending you
        live payments until you recover.
      tags:
        - Integration Management
      operationId: getHealth
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
      responses:
        '200':
          description: Integration is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationHealthResponse'
              examples:
                up:
                  $ref: '#/components/examples/HealthUpExample'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Integration is unhealthy
components:
  parameters:
    AuthorizationHeader:
      name: Authorization
      in: header
      required: true
      schema:
        type: string
      description: |
        Bearer token authenticating payments-service to your integration
        (the **outbound token** LoyaltyPlant provisions for you during
        onboarding). Validate it on every request (constant-time compare) and
        reject anything else with 401. Treat the token as a secret; rotate it
        via LoyaltyPlant. Example: "Bearer sk_live_abc123".
    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        format: uuid
      description: >
        UUID for idempotent request handling, unique per logical operation.

        Carried by all mutating operations (authorize, sale, capture, refund,

        reverse); the read-only operations (inquire-status, capabilities,
        health)

        do not carry it. Your integration MUST return the same response for

        repeated requests carrying the same key (payments-service retries

        transient/network failures and must never double-charge).
  schemas:
    IntegrationAuthorizeRequest:
      type: object
      required:
        - paymentId
        - amount
        - currency
      properties:
        paymentId:
          type: string
          description: >-
            Unique payment identifier from payments-service. Echo it back in
            logs; use it to correlate retries.
        amount:
          type: number
          description: >-
            Payment amount in major currency units (e.g. 49.99 = 49 dollars 99
            cents). Convert to your PSP's required unit (e.g. minor units /
            cents) yourself.
        currency:
          type: string
          description: ISO 4217 currency code (e.g. "USD", "EUR", "SAR")
        settings:
          type: object
          additionalProperties:
            type: string
          description: |
            Partner/outlet-specific configuration provisioned by LoyaltyPlant
            (NOT chosen by the integrator). Common keys: `currency` (always
            present), `merchantId`. Any extra keys your PSP needs are agreed
            during onboarding and passed through verbatim.
    IntegrationSaleRequest:
      type: object
      required:
        - paymentId
        - amount
        - currency
      properties:
        paymentId:
          type: string
          description: Unique payment identifier from payments-service
        amount:
          type: number
          description: >-
            Payment amount in major currency units (e.g. 49.99). Convert to your
            PSP's required unit yourself.
        currency:
          type: string
          description: ISO 4217 currency code (e.g. "USD", "EUR", "SAR")
        settings:
          type: object
          additionalProperties:
            type: string
          description: |
            Partner/outlet-specific configuration provisioned by LoyaltyPlant
            (NOT chosen by the integrator). Common keys: `currency` (always
            present), `merchantId`.
    IntegrationCaptureRequest:
      type: object
      required:
        - paymentId
        - transactionReference
      properties:
        paymentId:
          type: string
          description: Unique payment identifier from payments-service
        transactionReference:
          type: string
          description: Transaction reference you returned from authorize
        amount:
          type: number
          description: >-
            Capture amount (optional, for partial capture; if omitted, captures
            the full authorized amount)
        settings:
          type: object
          additionalProperties:
            type: string
          description: >-
            Partner/outlet-specific configuration provisioned by LoyaltyPlant
            (NOT chosen by the integrator)
    IntegrationRefundRequest:
      type: object
      required:
        - paymentId
        - transactionReference
        - amount
      properties:
        paymentId:
          type: string
          description: Unique payment identifier from payments-service
        transactionReference:
          type: string
          description: Transaction reference of the payment to refund
        amount:
          type: number
          description: Refund amount in major currency units (supports partial refunds)
        reason:
          type: string
          description: Reason for the refund (free text, for your records / PSP)
        settings:
          type: object
          additionalProperties:
            type: string
          description: >-
            Partner/outlet-specific configuration provisioned by LoyaltyPlant
            (NOT chosen by the integrator)
    IntegrationReverseRequest:
      type: object
      required:
        - paymentId
        - transactionReference
      properties:
        paymentId:
          type: string
          description: Unique payment identifier from payments-service
        transactionReference:
          type: string
          description: Transaction reference of the payment to reverse/void
        settings:
          type: object
          additionalProperties:
            type: string
          description: >-
            Partner/outlet-specific configuration provisioned by LoyaltyPlant
            (NOT chosen by the integrator)
    IntegrationOperationResponse:
      type: object
      description: >
        Business result of a payment operation. Business outcomes are **always**

        returned as HTTP `200` with this body; the `status` field carries the

        outcome (`SUCCESS` / `PENDING` / `DECLINED` / `RETRIABLE` / `UNKNOWN`).

          * HTTP `4xx` / `5xx` signal **infrastructure** errors only (bad
            request, auth failure, your service crashed). payments-service treats
            `5xx` and timeouts as retriable.
          * HTTP `501` means "this operation is not implemented by my
            integration" (e.g. `sale` called on an authorize+capture-only
            integration). payments-service treats it as a non-retriable failure.

        A business decline (insufficient funds, fraud, expired card, …) is

        **not** an HTTP error — return `200` with `status: DECLINED` and an

        `errorCode`.


        How payments-service maps your HTTP status to an outcome:


        | Your HTTP status | Interpreted as | Retried? |

        |---|---|---|

        | `200` + `status` body | the business outcome you sent | per `status` |

        | `400`, `401`, `403`, `404` | non-retriable failure (`UNKNOWN`) | no |

        | `408`, `429`, `500`, `502`, `503`, `504` | transient failure
        (`RETRIABLE`) | yes |

        | `501` | operation not implemented (`UNKNOWN`) | no |


        So for a transient failure use a `5xx` or `200 RETRIABLE` — never a
        `4xx`.
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/IntegrationStatus'
        transactionReference:
          type: string
          description: >
            Your transaction reference for this operation. **Required on SUCCESS

            and PENDING** — payments-service stores it and uses it to match the

            async result callback back to the suspended payment, and as the

            reference for later capture/refund/reverse. Make it stable and
            unique.
        errorCode:
          allOf:
            - $ref: '#/components/schemas/PaymentErrorCode'
          description: |
            Error code for DECLINED/RETRIABLE/UNKNOWN results.
            Use standard PaymentErrorCode values when possible.
            If the PSP returns a proprietary code that doesn't map to any
            standard code, pass it as-is — payments-service will fall back
            to GATEWAY_ERROR.
        reason:
          type: string
          description: >-
            Human-readable description of the result (safe to log; do not
            include PAN/secrets)
        redirectUrl:
          type: string
          format: uri
          description: >-
            URL the customer should be redirected to (for 3DS/redirect flows).
            Only present when status is PENDING.
        expectedTtlSeconds:
          type: integer
          description: >-
            Expected time-to-live in seconds for pending operations (how long
            before timeout). Only present when status is PENDING.
    IntegrationStatus:
      type: string
      enum:
        - SUCCESS
        - PENDING
        - DECLINED
        - RETRIABLE
        - UNKNOWN
      description: >
        Operation outcome status:

        - SUCCESS: Operation completed successfully

        - PENDING: Operation requires async completion (redirect/webhook);
        include `redirectUrl`

        - DECLINED: Business-level decline (insufficient funds, card blocked,
        etc.) — terminal, do not retry

        - RETRIABLE: Transient error, safe to retry (network blip, PSP timeout)

        - UNKNOWN: Ambiguous outcome, manual investigation / reconciliation may
        be needed
    IntegrationCapability:
      type: string
      enum:
        - TOKENIZATION
        - CAPTURE
        - SALE
        - DIRECT_REFUND
        - REDIRECT_FLOW
        - DIRECT_WEBHOOK
        - STATUS_POLLING
        - APPLE_PAY
        - GOOGLE_PAY
        - SBP
      description: >
        Payment capability that the integration supports. Declare a subset via

        `/v1/capabilities`. Each capability gates which operations
        payments-service

        will call:

        - TOKENIZATION: store cards for future one-click payments (handled
        inside authorize/sale via `settings`; no dedicated endpoint)

        - CAPTURE: two-phase payments — payments-service calls `authorize` then
        `capture`

        - SALE: single-phase payments — payments-service calls `sale`

        - DIRECT_REFUND: `refund` by transaction reference

        - REDIRECT_FLOW: authorize/sale may return PENDING + redirectUrl (3DS /
        hosted page)

        - DIRECT_WEBHOOK: your PSP webhooks your service directly; you POST the
        result callback

        - STATUS_POLLING: `inquire-status` is supported for reconciliation

        - APPLE_PAY / GOOGLE_PAY: wallet payments (handled inside authorize/sale
        via `settings`; no dedicated endpoint)

        - SBP: Russia's Faster Payments System (СБП)
    IntegrationCapabilitiesResponse:
      type: object
      required:
        - capabilities
      properties:
        capabilities:
          type: array
          items:
            $ref: '#/components/schemas/IntegrationCapability'
          description: List of capabilities this integration supports
    IntegrationHealthResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - UP
            - DOWN
          description: Health status of the integration service
    PaymentErrorCode:
      type: string
      description: >
        Standardized payment error codes. Integrations SHOULD return these codes

        in the `errorCode` field for DECLINED, RETRIABLE, and UNKNOWN results.


        Responsible parties:

        - **User**: Cardholder input or account issue

        - **Issuer Bank**: Issuing bank declined or system error

        - **Payment Gateway**: PSP-level error

        - **LP**: Internal system / configuration error


        | ID | Code | Responsible | Description |

        |----|------|-------------|-------------|

        | 3 | CARD_EXPIRED | User | Card expired |

        | 4 | INVALID_CARD_STATUS | Issuer Bank | Card is inactive, blocked, or
        otherwise invalid |

        | 5 | DECLINED_TRANSACTION_BY_ISSUER | Issuer Bank | Transaction
        declined by issuer |

        | 6 | NOT_ENOUGH_MONEY | User | Insufficient funds |

        | 7 | EXCEEDED_CARD_LIMIT | User | Card usage limit exceeded |

        | 8 | ANTIFRAUD | Issuer Bank | Declined due to issuer's antifraud
        system |

        | 9 | SYSTEM_ERROR | LP | Internal payment processing error |

        | 10 | FAILURE_3DS_CHECK | User | 3DS authentication failure |

        | 11 | FAILURE_SECRET_CODE_CHECK | User | Wrong CVV2/CVC2 |

        | 12 | TIMEOUT_GATEWAY | Payment Gateway | Timeout in payment gateway |

        | 13 | BLACKLIST | Payment Gateway | Card or BIN is blacklisted |

        | 15 | BAD_REQUEST | LP | Malformed request |

        | 16 | GATEWAY_ERROR | Payment Gateway | Generic payment gateway error |

        | 17 | INVALID_AMOUNT | LP | Invalid transaction amount |

        | 19 | WRONG_CARD_NUMBER | User | Wrong card number |

        | 20 | TRANSACTION_NOT_PERMITTED | Issuer Bank | Transaction not
        permitted |

        | 21 | INVALID_EXPIRY_DATE | User | Invalid expiration date |

        | 22 | INVALID_CARDHOLDER_NAME | User | Invalid cardholder name |

        | 23 | ISSUER_ERROR | Issuer Bank | Issuer bank system error |

        | 24 | TIMEOUT_ISSUER | Issuer Bank | Timeout during communication with
        issuer bank |

        | 25 | UNSUPPORTED_CARD_TYPE | Payment Gateway | Card type not supported
        |

        | 26 | DUPLICATE_TRANSACTION | LP | Duplicate transaction detected |

        | 27 | INVALID_SETTINGS | LP | Invalid configuration (e.g. Merchant ID)
        |

        | 28 | CURRENCY_NOT_SUPPORTED | Payment Gateway | Unsupported
        transaction currency |

        | 29 | RESTRICTED_COUNTRY | Payment Gateway | Restricted card country or
        IP |

        | 30 | CARD_NOT_SUPPORTED_FOR_MERCHANT | Payment Gateway | Card scheme
        or type not supported by merchant configuration |


        If your PSP returns a proprietary code that doesn't map to any of the
        above,

        pass it as-is — payments-service will fall back to GATEWAY_ERROR.


        NOTE on naming: the wire value is `FAILURE_3DS_CHECK`; the generated
        Java

        enum constant is `PaymentErrorCode.FAILURE_3_DS_CHECK` (the value string
        is

        still `FAILURE_3DS_CHECK`). Always send the wire value.
      enum:
        - CARD_EXPIRED
        - INVALID_CARD_STATUS
        - DECLINED_TRANSACTION_BY_ISSUER
        - NOT_ENOUGH_MONEY
        - EXCEEDED_CARD_LIMIT
        - ANTIFRAUD
        - SYSTEM_ERROR
        - FAILURE_3DS_CHECK
        - FAILURE_SECRET_CODE_CHECK
        - BLACKLIST
        - BAD_REQUEST
        - GATEWAY_ERROR
        - INVALID_AMOUNT
        - WRONG_CARD_NUMBER
        - TRANSACTION_NOT_PERMITTED
        - UNSUPPORTED_CARD_TYPE
        - TIMEOUT_GATEWAY
        - TIMEOUT_ISSUER
        - INVALID_EXPIRY_DATE
        - INVALID_CARDHOLDER_NAME
        - ISSUER_ERROR
        - DUPLICATE_TRANSACTION
        - INVALID_SETTINGS
        - CURRENCY_NOT_SUPPORTED
        - RESTRICTED_COUNTRY
        - CARD_NOT_SUPPORTED_FOR_MERCHANT
    IntegrationStatusInquiryRequest:
      type: object
      properties:
        transactionReference:
          type: string
          description: >-
            Transaction reference returned by a previous authorize/sale/capture
            operation
        paymentId:
          type: string
          description: Payment ID from payments-service (alternative lookup key)
    IntegrationStatusInquiryResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - AUTHORIZED
            - CAPTURED
            - VOIDED
            - REFUNDED
            - DECLINED
            - EXPIRED
            - PENDING
            - NOT_FOUND
          description: |
            Current payment status at the PSP:
            - AUTHORIZED: Funds held, not yet captured
            - CAPTURED: Payment completed (settled)
            - VOIDED: Authorization reversed
            - REFUNDED: Payment refunded
            - DECLINED: Payment declined by PSP/issuer
            - EXPIRED: Authorization expired
            - PENDING: Payment still in progress (3DS, async)
            - NOT_FOUND: Transaction not found at PSP
        transactionReference:
          type: string
          description: PSP transaction reference
        rawStatus:
          type: string
          description: Raw status string from the PSP (for debugging)
    IntegrationErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Error message
        details:
          type: string
          description: Additional error details
  examples:
    AuthorizeRequestExample:
      summary: Authorize 49.99 USD
      value:
        paymentId: 8e2b7d6a-1f3c-4e9a-9c2b-77a0e3d4f111
        amount: 49.99
        currency: USD
        settings:
          currency: USD
          merchantId: acct_3092f1
    SaleRequestExample:
      summary: Single-phase sale of 49.99 USD
      value:
        paymentId: 8e2b7d6a-1f3c-4e9a-9c2b-77a0e3d4f111
        amount: 49.99
        currency: USD
        settings:
          currency: USD
          merchantId: acct_3092f1
    CaptureRequestExample:
      summary: Capture the full authorized amount
      value:
        paymentId: 8e2b7d6a-1f3c-4e9a-9c2b-77a0e3d4f111
        transactionReference: psp_txn_7Hagain2
        amount: 49.99
        settings:
          currency: USD
    RefundRequestExample:
      summary: Partial refund of 10.00 USD
      value:
        paymentId: 8e2b7d6a-1f3c-4e9a-9c2b-77a0e3d4f111
        transactionReference: psp_txn_7Hagain2
        amount: 10
        reason: Item out of stock
        settings:
          currency: USD
    ReverseRequestExample:
      summary: Void an uncaptured authorization
      value:
        paymentId: 8e2b7d6a-1f3c-4e9a-9c2b-77a0e3d4f111
        transactionReference: psp_txn_7Hagain2
        settings:
          currency: USD
    StatusInquiryRequestExample:
      summary: Look up status by transaction reference
      value:
        transactionReference: psp_txn_7Hain2
        paymentId: 8e2b7d6a-1f3c-4e9a-9c2b-77a0e3d4f111
    SuccessResponseExample:
      summary: Approved
      value:
        status: SUCCESS
        transactionReference: psp_txn_7HagaIn2
    Pending3dsResponseExample:
      summary: 3DS / redirect required
      value:
        status: PENDING
        transactionReference: psp_txn_7HagaIn2
        redirectUrl: https://acs.issuer-bank.example/3ds/challenge?token=eyJhbGciOi
        expectedTtlSeconds: 300
    DeclinedResponseExample:
      summary: Declined — insufficient funds
      value:
        status: DECLINED
        errorCode: NOT_ENOUGH_MONEY
        reason: Insufficient funds
    RetriableResponseExample:
      summary: Transient PSP error — safe to retry
      value:
        status: RETRIABLE
        errorCode: TIMEOUT_GATEWAY
        reason: PSP timed out; retry
    UnknownResponseExample:
      summary: Ambiguous outcome — reconcile via inquire-status
      value:
        status: UNKNOWN
        errorCode: GATEWAY_ERROR
        reason: No definitive response from PSP
    CapabilitiesResponseExample:
      summary: Two-phase integration with 3DS, webhooks and refunds
      value:
        capabilities:
          - CAPTURE
          - DIRECT_REFUND
          - REDIRECT_FLOW
          - DIRECT_WEBHOOK
          - STATUS_POLLING
    HealthUpExample:
      summary: Healthy
      value:
        status: UP
    StatusInquiryResponseExample:
      summary: Payment captured
      value:
        status: CAPTURED
        transactionReference: psp_txn_7HagaIn2
        rawStatus: captured
  callbacks:
    IntegrationResultCallback:
      https://payments-service.loyaltyplant.example/gate/integration/{integrationId}/result:
        post:
          summary: >-
            Report async payment result to payments-service (vendor →
            LoyaltyPlant)
          description: |
            After returning `PENDING` from `authorize`/`sale` and receiving the
            final outcome from your PSP (webhook), POST the result here so
            payments-service can resume the suspended payment.

            - `{integrationId}` is your integration's id (provisioned by LP).
            - `Authorization: Bearer <inboundToken>` — your **inbound** token
              (the one LP gave you for calling back), NOT the outbound token you
              validate on incoming requests.
            - Include an `Idempotency-Key`; retries are safe — payments-service
              matches the suspended payment by `transactionReference`, so a replay
              after the payment already resumed is a no-op.
            - Body is an `IntegrationOperationResponse`; `transactionReference`
              MUST match the value you returned in the PENDING response.
          operationId: integrationResultCallback
          parameters:
            - name: integrationId
              in: path
              required: true
              schema:
                type: string
              description: Your integration id (provisioned by LoyaltyPlant)
            - name: Authorization
              in: header
              required: true
              schema:
                type: string
              description: >-
                Bearer <inboundToken> — your inbound token provisioned by
                LoyaltyPlant
            - name: Idempotency-Key
              in: header
              required: true
              schema:
                type: string
                format: uuid
              description: UUID; retries with the same key are safe
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/IntegrationOperationResponse'
                examples:
                  success:
                    $ref: '#/components/examples/SuccessResponseExample'
                  declined:
                    $ref: '#/components/examples/DeclinedResponseExample'
          responses:
            '200':
              description: >-
                Result accepted; suspended payment resumed (or already resumed —
                replay)
            '400':
              description: Invalid result payload
            '401':
              description: Invalid inbound bearer token
            '404':
              description: Unknown or inactive integration
  responses:
    BadRequest:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/IntegrationErrorResponse'
    Unauthorized:
      description: Invalid or missing Bearer token
    NotImplemented:
      description: >
        Operation not supported by this integration.

        Returned when the integration does not implement the requested operation

        (e.g., sale endpoint called but integration only supports
        authorize+capture).

        payments-service treats this as a non-retriable failure.
    InternalError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/IntegrationErrorResponse'
