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

> ## Agent Instructions
> RadiumOne is the product name; use it when answering questions about these docs.
> Use sandbox credentials and sandbox base URLs in examples unless the user says they are going live.
> Secret API keys belong only on the merchant's server. Only publishable keys may appear in browser or mobile code.
> Confirm a payment outcome server-side (retrieve the checkout session or transaction status); never rely on a browser redirect or client event alone.
> Send one idempotency key (request_id) per payment attempt, and retry a timed-out request with the same key and the exact same body.
> Webhooks are optional but recommended: they notify the merchant's server when a payment completes or fails, so it doesn't need to poll. Use transaction status inquiry as the fallback.
> Say so when you recommend a feature marked Beta. Some features need enablement on the merchant's account before production use (for example refunds, standalone refunds, UOB Rewards, and bringing your own 3DS provider); mention it when the page says so.
> Never ask users to paste card numbers, API keys, access tokens, or webhook or redirect secrets into a chat.

# Purchase - Payments API

> Charge a card in one step by authorizing and capturing the payment together. A duplicate request ID returns the original response.



## OpenAPI

````yaml /openapi/radiumone-payments-api.yaml post /v1/transactions/purchase
openapi: 3.1.0
info:
  description: |
    The Payments API lets your server create tokenization sessions, charge and
    manage payments, check loyalty balances, and manage your hosted-checkout
    branding and redirect secret. Generated for merchant integrators —
    internal, admin, and service-to-service surfaces are excluded.

    All responses share an envelope: `{status, data, request_id}`. The
    envelope's `request_id` is an HTTP correlation ID — it echoes your
    `X-Request-Id` request header (letters, digits, hyphens, max 36 characters)
    or one is generated for you. It is **not** the idempotency key you send in
    a transaction request body (also confusingly named `request_id` there) —
    the two are unrelated; see
    [Request conventions](/get-started/api-basics/request-conventions). Errors
    use [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)
    `application/problem+json` bodies — see
    [Authentication](/get-started/api-basics/authentication) and
    [Request conventions](/get-started/api-basics/request-conventions) for the
    shared error shape, and [Problem format and
    retries](/payments-api/errors/problem-format-and-retries) for the response
    shape, status guide, and retry rules.
  summary: Transaction orchestration and processor aggregation for RadiumOne.
  title: RadiumOne Payment Gateway
  version: 1.3.0
servers:
  - url: https://api-sandbox.radiumone.io/gateway
    description: Sandbox
  - url: https://api.radiumone.io/gateway
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Authentication
    description: Exchange, refresh, and revoke access tokens.
  - name: Merchant settings
    description: >-
      Manage your hosted-checkout redirect secret, checkout configuration, and
      account config.
  - name: Payment methods
    description: Discover which payment methods and brands are available.
  - name: Sessions
    description: Tokenization sessions used to collect card data with RadiumOne Elements.
  - name: Settlement
    description: Settlement batch status lookup.
  - name: Payments
    description: Create and manage card transactions.
  - name: Rewards
    description: UOB Rewards loyalty balance inquiry.
  - name: Refunds
    description: Return funds to a shopper.
  - name: Transactions
    description: Check the live status of a transaction.
paths:
  /v1/transactions/purchase:
    post:
      tags:
        - Payments
      summary: Create a purchase
      description: >-
        Charge a card in a single step: authorise and capture together.


        Returns 201 with the resulting transaction; check its `status` for the
        outcome.

        Idempotent on `request_id`: resubmitting the same `request_id` returns
        the original

        transaction and never charges the card again.


        Include `loyalty` to redeem loyalty value as part of the same sale. Only
        sale

        redemptions are currently supported; other redemption kinds are rejected
        with 422, as

        is a loyalty redemption when no loyalty program is configured for your
        payment

        acquirer. The response then also carries the transaction `group_id` and
        the `loyalty`

        outcome.
      operationId: purchase_transaction_v1_transactions_purchase_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PurchaseRequest'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse_TransactionResponse_'
              examples:
                approved:
                  value:
                    status: ok
                    request_id: req_7a8b9c0d1e2f
                    data:
                      id: 8f1c2e10-4b3a-4c5d-9e6f-7a8b9c0d1e2f
                      request_id: ord-1001-pay-1
                      type: PURCHASE
                      status: CAPTURED
                      amount: 5000
                      currency: SGD
                      payment_method_type: card
                      order_reference: ORD-1001
                      response_code: '00'
                      parent_transaction_id: null
                      switch_request_id: sw-8f1c2e10
                      created_at: '2026-09-14T10:00:00.000Z'
                      updated_at: '2026-09-14T10:00:01.000Z'
                declined:
                  value:
                    status: ok
                    request_id: req_9a2d3f215c4b
                    data:
                      id: 9a2d3f21-5c4b-4d6e-8f70-8b9c0d1e2f3a
                      request_id: ord-1001-pay-1
                      type: PURCHASE
                      status: DECLINED
                      amount: 5000
                      currency: SGD
                      payment_method_type: card
                      order_reference: ORD-1001
                      response_code: '05'
                      parent_transaction_id: null
                      switch_request_id: sw-9a2d3f21
                      created_at: '2026-09-14T10:00:00.000Z'
                      updated_at: '2026-09-14T10:00:01.000Z'
          description: Successful Response
        '400':
          description: The request body failed validation.
          x-docs-interim: true
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '401':
          content:
            application/problem+json:
              example:
                detail: Missing or invalid Bearer token.
                status: 401
                title: Authentication Required
                type: urn:radiumone:gateway:authentication-required
              schema:
                properties:
                  detail:
                    type: string
                  status:
                    type: integer
                  title:
                    type: string
                  type:
                    type: string
                type: object
          description: Missing or invalid Bearer token.
        '403':
          content:
            application/problem+json:
              example:
                detail: Insufficient permissions for this operation.
                status: 403
                title: Permission Denied
                type: urn:radiumone:gateway:permission-denied
              schema:
                properties:
                  detail:
                    type: string
                  status:
                    type: integer
                  title:
                    type: string
                  type:
                    type: string
                type: object
          description: Insufficient permissions for this operation.
        '404':
          content:
            application/problem+json:
              example:
                detail: The requested resource does not exist.
                status: 404
                title: Not Found
                type: urn:radiumone:gateway:not-found
              schema:
                properties:
                  detail:
                    type: string
                  status:
                    type: integer
                  title:
                    type: string
                  type:
                    type: string
                type: object
          description: The requested resource does not exist.
        '409':
          content:
            application/problem+json:
              example:
                detail: A resource with that identifier already exists.
                status: 409
                title: Conflict
                type: urn:radiumone:gateway:conflict
              schema:
                properties:
                  detail:
                    type: string
                  status:
                    type: integer
                  title:
                    type: string
                  type:
                    type: string
                type: object
          description: A resource with that identifier already exists.
        '410':
          content:
            application/problem+json:
              example:
                detail: >-
                  The payment token has expired or its card data is no longer
                  available.
                status: 410
                title: Gone
                type: urn:radiumone:gateway:gone
              schema:
                properties:
                  detail:
                    type: string
                  status:
                    type: integer
                  title:
                    type: string
                  type:
                    type: string
                type: object
          description: >-
            The payment token has expired or its card data is no longer
            available.
        '422':
          description: >-
            A business rule was violated — e.g. a voucher or coupon loyalty
            redemption kind, which is always rejected before dispatch.
          x-docs-interim: true
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              examples:
                redemption-kind-not-supported:
                  value:
                    type: urn:radiumone:loyalty:redemption-kind-not-supported
                    title: Loyalty Redemption Kind Not Supported
                    status: 422
                    detail: >-
                      The requested loyalty redemption ``kind`` is recognised
                      but not yet dispatchable.
        '500':
          content:
            application/problem+json:
              example:
                detail: An unexpected error occurred.
                status: 500
                title: Internal Server Error
                type: urn:radiumone:gateway:internal-server-error
              schema:
                properties:
                  detail:
                    type: string
                  status:
                    type: integer
                  title:
                    type: string
                  type:
                    type: string
                type: object
          description: An unexpected error occurred.
        '503':
          content:
            application/problem+json:
              example:
                detail: >-
                  A downstream dependency is unavailable or did not respond in
                  time.
                status: 503
                title: Service Unavailable
                type: urn:radiumone:gateway:service-unavailable
              schema:
                properties:
                  detail:
                    type: string
                  status:
                    type: integer
                  title:
                    type: string
                  type:
                    type: string
                type: object
          description: A downstream dependency is unavailable or did not respond in time.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >
            #!/usr/bin/env bash

            # Purchase (authorise + capture in one call). Any 2xx is a response
            — branch

            # on data.status. On a timeout/5xx/PENDING, retry with the SAME
            request_id;

            # never mint a new one for the same order attempt.

            set -euo pipefail


            API_BASE="${RADIUMONE_API_BASE:-https://api-sandbox.radiumone.io/gateway}"

            : "${RADIUMONE_ACCESS_TOKEN:?set RADIUMONE_ACCESS_TOKEN to a Bearer
            access token}"


            curl -sS -X POST "$API_BASE/v1/transactions/purchase" \
              -H "Content-Type: application/json" \
              -H "Authorization: Bearer $RADIUMONE_ACCESS_TOKEN" \
              -d @request.json
        - lang: javascript
          label: Node.js
          source: >
            #!/usr/bin/env node

            // Purchase (authorise + capture in one call). Node 18+ ESM fetch.

            // Env: RADIUMONE_ACCESS_TOKEN, RADIUMONE_API_BASE (optional
            override).

            //

            // Shared result pattern: any 2xx is a response you branch on
            `data.status`.

            // On a network timeout, a 5xx, or `status:"PENDING"`, retry with
            the SAME

            // request_id (or poll GET /v1/transactions/{id}/status) — never
            mint a new

            // request_id for the same order attempt.

            import { readFileSync } from "node:fs";


            const API_BASE = process.env.RADIUMONE_API_BASE ||
            "https://api-sandbox.radiumone.io/gateway";

            const accessToken = process.env.RADIUMONE_ACCESS_TOKEN;

            const body = JSON.parse(readFileSync(new URL("./request.json",
            import.meta.url)));


            // Exponential backoff with jitter: attempt 1 waits ~250-500ms,
            doubling each

            // attempt, capped at 4s -- avoids hammering the gateway in a tight
            retry loop.

            function backoffMs(attempt) {
              const base = Math.min(250 * 2 ** (attempt - 1), 4000);
              return base + Math.random() * base;
            }


            async function createPurchase(maxAttempts = 3) {
              for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
                let res;
                try {
                  res = await fetch(`${API_BASE}/v1/transactions/purchase`, {
                    method: "POST",
                    headers: {
                      "Content-Type": "application/json",
                      Authorization: `Bearer ${accessToken}`,
                    },
                    body: JSON.stringify(body), // same request_id every attempt
                  });
                } catch (networkErr) {
                  if (attempt === maxAttempts) throw networkErr;
                  await new Promise((r) => setTimeout(r, backoffMs(attempt)));
                  continue; // network timeout: retry with the same body/request_id
                }

                if (res.status >= 500) {
                  if (attempt === maxAttempts) throw new Error(`server error ${res.status} after ${attempt} attempts`);
                  await new Promise((r) => setTimeout(r, backoffMs(attempt)));
                  continue; // retry with the same request_id
                }

                const payload = await res.json();
                if (!res.ok) {
                  // 4xx: not retryable by re-sending — fix the request, or handle
                  // urn:radiumone:transaction:idempotency-body-mismatch if you changed it.
                  throw new Error(`purchase failed: ${payload.type ?? payload.code} (${res.status})`);
                }

                if (payload.data.status === "PENDING") {
                  if (attempt === maxAttempts) return payload; // caller should poll GET status / wait for webhook
                  await new Promise((r) => setTimeout(r, backoffMs(attempt)));
                  continue; // retry the same request_id
                }

                // Branch on data.status: CAPTURED (success) | DECLINED (final, no retry) | FAILED.
                return payload;
              }
              throw new Error("unreachable");
            }


            createPurchase().then((r) => console.log(JSON.stringify(r, null,
            2)));
        - lang: python
          label: Python
          source: >
            #!/usr/bin/env python3

            """Purchase (authorise + capture in one call). Python 3.10+,
            requests.


            Shared result pattern: any 2xx is a response you branch on
            ``status``. On a

            network timeout, a 5xx, or ``status: "PENDING"``, retry with the
            SAME

            request_id (or poll GET /v1/transactions/{id}/status) — never mint a
            new

            request_id for the same order attempt.

            """

            import json

            import os

            import random

            import time

            from pathlib import Path


            import requests


            API_BASE = os.environ.get("RADIUMONE_API_BASE",
            "https://api-sandbox.radiumone.io/gateway")



            def backoff_seconds(attempt: int) -> float:
                """Exponential backoff with jitter: attempt 1 waits ~0.25-0.5s, doubling
                each attempt, capped at 4s -- avoids hammering the gateway in a loop."""
                base = min(0.25 * 2 ** (attempt - 1), 4.0)
                return base + random.random() * base


            def create_purchase(max_attempts: int = 3) -> dict:
                body = json.loads((Path(__file__).parent / "request.json").read_text())
                headers = {"Authorization": f"Bearer {os.environ.get('RADIUMONE_ACCESS_TOKEN', '')}"}

                for attempt in range(1, max_attempts + 1):
                    try:
                        resp = requests.post(f"{API_BASE}/v1/transactions/purchase", json=body, headers=headers, timeout=30)
                    except requests.exceptions.Timeout:
                        if attempt == max_attempts:
                            raise
                        time.sleep(backoff_seconds(attempt))
                        continue  # network timeout: retry with the same body/request_id

                    if resp.status_code >= 500:
                        if attempt == max_attempts:
                            raise RuntimeError(f"server error {resp.status_code} after {attempt} attempts")
                        time.sleep(backoff_seconds(attempt))
                        continue  # retry with the same request_id

                    payload = resp.json()
                    if not resp.ok:
                        # 4xx: not retryable by re-sending — fix the request, or handle
                        # urn:radiumone:transaction:idempotency-body-mismatch if you changed it.
                        code = payload.get("type") or payload.get("code")
                        raise RuntimeError(f"purchase failed: {code} ({resp.status_code})")

                    if payload["data"]["status"] == "PENDING":
                        if attempt == max_attempts:
                            return payload  # caller should poll GET status / wait for webhook
                        time.sleep(backoff_seconds(attempt))
                        continue  # retry the same request_id

                    # Branch on data.status: CAPTURED (success) | DECLINED (final, no retry) | FAILED.
                    return payload

                raise RuntimeError("unreachable")


            if __name__ == "__main__":
                print(json.dumps(create_purchase(), indent=2))
components:
  schemas:
    PurchaseRequest:
      description: |-
        Request body for POST /v1/transactions/purchase.

        Atomic sale: authorise then immediately capture in a single API call.
        Idempotent via request_id scoped to the authenticated merchant.
      example:
        amount:
          currency: SGD
          value: '000000005000'
        auto_capture: true
        card:
          token: '4111111111111111'
        channel: ECOMMERCE
        request_id: order-20260528-001
      properties:
        amount:
          $ref: '#/components/schemas/MoneyAmount'
          description: Amount to charge for this sale, as a {currency, value} money object.
        auto_capture:
          const: true
          default: true
          description: Purchase always auto-captures. Must be true (v1).
          title: Auto Capture
          type: boolean
        card:
          $ref: '#/components/schemas/CardToken'
          description: Card group carrying the network token (no raw PAN).
        channel:
          description: >-
            Transaction channel -- the source/manner of the payment. Shapes
            routing candidate selection and the capability/operation constraints
            applied downstream (acquirer_channel + acquirer_channel_operation
            gating).
          enum:
            - CARD_PRESENT
            - ECOMMERCE
            - MOTO
            - PAYMENT_LINK
            - IN_APP
            - RECURRING
          title: Channel
          type: string
        emv:
          anyOf:
            - type: string
            - additionalProperties:
                type: string
              type: object
            - type: 'null'
          description: >-
            EMV chip data read from the card, for card-present sales. Send it
            either as the hex string your terminal produced or as a map of EMV
            tag to hex value. Omit it for online payments made with a token.
          title: Emv
        loyalty:
          anyOf:
            - discriminator:
                mapping:
                  coupon:
                    $ref: '#/components/schemas/LoyaltyRedemptionCouponRequest'
                  sale:
                    $ref: '#/components/schemas/LoyaltyRedemptionRequest'
                  voucher:
                    $ref: '#/components/schemas/LoyaltyRedemptionVoucherRequest'
                propertyName: kind
              oneOf:
                - $ref: '#/components/schemas/LoyaltyRedemptionRequest'
                - $ref: '#/components/schemas/LoyaltyRedemptionVoucherRequest'
                - $ref: '#/components/schemas/LoyaltyRedemptionCouponRequest'
            - type: 'null'
          description: >-
            Optional loyalty redemption component (discriminated on 'kind':
            sale|voucher|coupon; defaults to 'sale'). When present, this sale
            includes a loyalty redemption. The payment acquirer must have a
            loyalty leg configured. NOT supported on auth or capture requests.
          title: Loyalty
        metadata:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          description: Optional merchant-supplied metadata (max 10 KB, max 5 depth levels).
          title: Metadata
        order_reference:
          anyOf:
            - maxLength: 128
              type: string
            - type: 'null'
          description: >-
            Merchant's order/cart reference for this payment (common in
            ecommerce). Stored, searchable via the transaction list filter, and
            forwarded to the acquirer for reconciliation. Capture/void/refund
            inherit it from this transaction, so one acquirer-side lookup
            returns the whole order. Acquirers impose their own limits and
            character rules (commonly 20 characters, alphanumeric) and will
            shorten the value to fit, so prefer short references using letters,
            digits, '-', '.' and '_', and put the varying part LAST -- values
            are shortened from the front. 
          title: Order Reference
        request_id:
          description: >-
            Merchant-supplied idempotency key; replays return the original
            response.
          maxLength: 64
          minLength: 8
          title: Request Id
          type: string
        three_ds:
          anyOf:
            - $ref: '#/components/schemas/ThreeDsPurchaseInput'
            - type: 'null'
          description: >-
            3DS result (one of {ref} | {mode:non_payer_auth} | {cavv,...});
            absent == non-payer-auth.
      required:
        - request_id
        - amount
        - card
        - channel
      title: PurchaseRequest
      type: object
    SuccessResponse_TransactionResponse_:
      description: >-
        Standard success envelope. Every successful response has this shape,
        with the operation's own payload under `data`.
      properties:
        data:
          anyOf:
            - $ref: '#/components/schemas/TransactionResponse'
            - type: 'null'
          description: >-
            The operation's result. Its shape is documented per operation;
            omitted on responses that carry no payload.
        message:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Optional human-readable note. Omitted from the response when not
            set, which is the case for every payment operation today. Never
            parse it.
          title: Message
        request_id:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Correlation ID for this HTTP request, for logs and support. Send
            your own in the `X-Request-Id` header (letters, digits and hyphens,
            up to 36 characters -- other characters are stripped) or the gateway
            generates one. This is NOT the `request_id` idempotency key you send
            in a transaction body; the two are unrelated.
          title: Request Id
        status:
          default: ok
          description: >-
            Always `ok` on a successful (2xx) response. Errors use a different
            body shape entirely (RFC 9457 problem details), so branch on the
            HTTP status code, not on this field.
          title: Status
          type: string
      title: SuccessResponse[TransactionResponse]
      type: object
    Problem:
      type: object
      x-docs-interim: true
      description: >-
        RFC 9457 problem details. Returned with `Content-Type:
        application/problem+json`. Interim: not yet a named component in the
        gateway team's published contract — every response there inlines its own
        smaller ad-hoc object; this shape reflects what our error pages and
        error-catalog.json actually document.
      properties:
        type:
          type: string
          format: uri
          description: >-
            A URN identifying the error condition, e.g.
            `urn:radiumone:gateway:validation-error`.
        title:
          type: string
          description: Short
          human-readable summary of the error type.: null
        status:
          type: integer
          description: The HTTP status code
          repeated in the body for convenience.: null
        detail:
          type: string
          description: Human-readable explanation specific to this occurrence.
        instance:
          type: string
          description: The request path that produced this error.
        request_id:
          type: string
          description: >-
            Correlation ID for this request (see the envelope `request_id` note
            above). Include it when contacting support.
        code:
          type: string
          description: Optional short machine-readable code
          distinct from `type`.: null
        retry_allowed:
          type: boolean
          description: >-
            When present, whether it's safe to retry with the same idempotency
            key.
        errors:
          type: array
          description: >-
            Present on most `urn:radiumone:gateway:validation-error` (400)
            responses — one entry per invalid field. Some 400s of this type are
            raised by checks that run after validation and have no `errors`
            array; always handle it being absent. Rely on `pointer`/`parameter`
            and `code`, not the wording of `detail`, which can change.
          items:
            type: object
            properties:
              pointer:
                type: string
                description: >-
                  JSON Pointer to the invalid field in the request body, e.g.
                  `/amount/currency` or `/items/0/name`. An empty string means
                  the whole request (e.g. the body isn't valid JSON).
              parameter:
                type: string
                description: >-
                  Name of the invalid query or path parameter. Set instead of
                  `pointer` when the failing value came from the URL, not the
                  body.
              code:
                type: string
                description: >-
                  Machine-readable error type for this field, e.g. `missing` or
                  `string_too_long`.
              detail:
                type: string
                description: Human-readable explanation for this field.
      required:
        - type
        - title
        - status
        - detail
    MoneyAmount:
      description: >-
        A money value: an ISO 4217 currency plus the amount in that currency's
        minor units.


        `value` is a numeric string in minor units -- the decimal point is
        implied by

        the currency (2 places for most currencies, so `"1050"` means 10.50). Up
        to 12

        digits; leading zeros are optional.
      properties:
        currency:
          description: ISO 4217 currency code (e.g. SGD, USD).
          maxLength: 3
          minLength: 3
          title: Currency
          type: string
        value:
          description: >-
            Amount in minor units as a numeric string (e.g. '1050' or
            '000000001050' = 10.50). Zero-padding optional; up to 12 digits.
          maxLength: 12
          minLength: 1
          pattern: ^\d{1,12}$
          title: Value
          type: string
      required:
        - currency
        - value
      title: MoneyAmount
      type: object
    CardToken:
      description: >-
        The card to charge, identified by its payment token.


        Send only the payment token issued for the card -- never the card number
        or

        expiry. The token stands in for both.
      properties:
        token:
          description: >-
            The card's payment token (digits only). Treat it as opaque -- it is
            not a card number.
          maxLength: 128
          minLength: 8
          title: Token
          type: string
      required:
        - token
      title: CardToken
      type: object
    LoyaltyRedemptionCouponRequest:
      description: >-
        Redeem loyalty coupons on their own, rather than as part of a sale.


        Not yet available: a purchase carrying this component is always rejected
        with a

        422 error. Do not build against it.
      properties:
        coupons:
          description: Coupons to redeem (at least one).
          items:
            $ref: '#/components/schemas/RedeemCoupon'
          minItems: 1
          title: Coupons
          type: array
        kind:
          const: coupon
          description: >-
            Selects the redemption intent: `coupon` redeems coupons on their
            own.
          title: Kind
          type: string
      required:
        - kind
        - coupons
      title: LoyaltyRedemptionCouponRequest
      type: object
    LoyaltyRedemptionRequest:
      description: >-
        Redeem loyalty points as part of a purchase.


        Include this on a purchase to pay for part (or all) of the sale with
        loyalty

        points, optionally naming the pools to draw from and any vouchers to
        apply. It is

        accepted on purchase only -- not on authorization or capture -- and the
        sale must

        be routed to an acquirer that has a loyalty programme linked, otherwise
        the request

        is rejected before any payment is attempted.
      properties:
        allow_full_redemption:
          default: false
          description: >-
            Deprecated no-op, retained for backward compatibility. Full
            redemption (loyalty covers 100% of the sale) is always permitted and
            yields a loyalty-only transaction with no card charge / no payment
            leg.
          title: Allow Full Redemption
          type: boolean
        kind:
          const: sale
          default: sale
          description: >-
            Selects the redemption intent: `sale` redeems points (and any
            vouchers) as part of this purchase. Defaults to `sale` when omitted.
          title: Kind
          type: string
        point_redeem_amount:
          anyOf:
            - minimum: 0
              type: integer
            - type: 'null'
          description: >-
            Points to redeem in minor currency units. None or 0 means the
            loyalty host decides the redemption amount.
          title: Point Redeem Amount
        redeem_pools:
          description: Specific pools to redeem from (empty = host decides).
          items:
            $ref: '#/components/schemas/RedeemPool'
          title: Redeem Pools
          type: array
        vouchers:
          description: Vouchers to apply in this redemption (empty = no vouchers).
          items:
            $ref: '#/components/schemas/RedeemVoucher'
          title: Vouchers
          type: array
      title: LoyaltyRedemptionRequest
      type: object
    LoyaltyRedemptionVoucherRequest:
      description: >-
        Redeem loyalty vouchers on their own, rather than as part of a sale.


        Not yet available: a purchase carrying this component is always rejected
        with a

        422 error. Do not build against it. To use vouchers today, include them
        in the

        `vouchers` list of a normal loyalty redemption on a purchase.
      properties:
        kind:
          const: voucher
          description: >-
            Selects the redemption intent: `voucher` redeems vouchers on their
            own.
          title: Kind
          type: string
        vouchers:
          description: Vouchers to redeem (at least one).
          items:
            $ref: '#/components/schemas/RedeemVoucher'
          minItems: 1
          title: Vouchers
          type: array
      required:
        - kind
        - vouchers
      title: LoyaltyRedemptionVoucherRequest
      type: object
    ThreeDsPurchaseInput:
      additionalProperties: false
      description: >-
        The 3-D Secure result to attach to a purchase or authorization.


        Send exactly one of: `ref` (the reference returned when the gateway ran
        the

        authentication for you), `mode: non_payer_auth` (this payment was not

        authenticated), or a `cavv` block (you authenticated the cardholder
        through your

        own 3DS provider). Omitting `three_ds` altogether is the same as

        `mode: non_payer_auth`.
      properties:
        cavv:
          anyOf:
            - maxLength: 64
              type: string
            - type: 'null'
          description: >-
            Cardholder Authentication Verification Value produced by your own
            3DS provider. Send it only when you ran the authentication yourself,
            and send `eci`, `ds_transaction_id` and `version` alongside it.
          title: Cavv
        ds_transaction_id:
          anyOf:
            - maxLength: 64
              type: string
            - type: 'null'
          description: >-
            The card scheme directory server's identifier for the
            authentication, from your own 3DS provider. Send it with `cavv`.
          title: Ds Transaction Id
        eci:
          anyOf:
            - maxLength: 2
              type: string
            - type: 'null'
          description: >-
            Electronic Commerce Indicator from your own 3DS provider, recording
            how the card was authenticated and who carries liability. Send it
            with `cavv`.
          title: Eci
        mode:
          anyOf:
            - const: non_payer_auth
              type: string
            - type: 'null'
          description: Explicit non-payer-auth.
          title: Mode
        ref:
          anyOf:
            - maxLength: 80
              type: string
            - type: 'null'
          description: Single-use 3DS authentication ref.
          title: Ref
        version:
          anyOf:
            - maxLength: 10
              type: string
            - type: 'null'
          description: >-
            3DS protocol version your provider used, for example `2.2.0`. Send
            it with `cavv`.
          title: Version
      title: ThreeDsPurchaseInput
      type: object
    TransactionResponse:
      description: >-
        A single transaction and its current outcome.


        `group_id` links the card and loyalty parts of a sale that used both,
        and

        `loyalty` carries the loyalty outcome when the purchase redeemed points.
      properties:
        amount:
          description: Amount in smallest currency unit.
          title: Amount
          type: integer
        created_at:
          description: ISO 8601 creation timestamp (UTC).
          title: Created At
          type: string
        currency:
          description: ISO 4217 currency code.
          title: Currency
          type: string
        group_id:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Transaction group UUID shared by payment and loyalty legs in a split
            sale.
          title: Group Id
        id:
          description: Transaction UUID.
          title: Id
          type: string
        loyalty:
          anyOf:
            - $ref: '#/components/schemas/LoyaltyOutcomeComponent'
            - type: 'null'
          description: >-
            Loyalty leg outcome (present only when purchase included a
            redemption component).
        order_reference:
          anyOf:
            - type: string
            - type: 'null'
          description: Merchant order/cart reference supplied at auth/purchase (ecommerce).
          title: Order Reference
        parent_transaction_id:
          anyOf:
            - type: string
            - type: 'null'
          description: UUID of parent authorization (for capture/void/refund).
          title: Parent Transaction Id
        payment_method_type:
          description: Payment instrument category.
          title: Payment Method Type
          type: string
        request_id:
          description: Merchant-supplied idempotency key.
          title: Request Id
          type: string
        response_code:
          default: ''
          description: >-
            Verbatim acquirer response code, for display and reconciliation
            only. DO NOT derive success from it: an operator can classify a
            non-'00' code as APPROVED for an acquirer (or for one operation), in
            which case this field still shows the host's non-'00' value while
            the transaction is approved. Key on `status` instead. The gateway
            never rewrites this value, so it always matches what the acquirer's
            own statement will show.
          title: Response Code
          type: string
        status:
          description: >-
            Current transaction lifecycle status, and THE field to key the
            outcome on. Approved = the operation's success status (AUTHORIZE ->
            AUTHORIZED; PURCHASE/CAPTURE/REFUND -> CAPTURED; VOID -> VOIDED).
            DECLINED = the issuer declined (hard decline). FAILED = a
            system/network error, which is NOT a statement about the card.
            Derived from the resolved decline classification, not from
            response_code alone.
          title: Status
          type: string
        switch_request_id:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            The upstream processor's own reference for this transaction. Quote
            it when raising an issue with us about a specific transaction.
          title: Switch Request Id
        type:
          description: 'Transaction type: AUTHORIZE, CAPTURE, VOID, or REFUND.'
          title: Type
          type: string
        updated_at:
          description: ISO 8601 last-updated timestamp (UTC).
          title: Updated At
          type: string
      required:
        - id
        - request_id
        - type
        - status
        - amount
        - currency
        - payment_method_type
        - created_at
        - updated_at
      title: TransactionResponse
      type: object
    RedeemCoupon:
      description: A loyalty coupon to redeem (coupon-redemption intent).
      properties:
        coupon_code:
          description: Coupon identifier to redeem.
          maxLength: 64
          minLength: 1
          title: Coupon Code
          type: string
        quantity:
          default: 1
          description: Number of coupon units to redeem.
          maximum: 999999
          minimum: 1
          title: Quantity
          type: integer
      required:
        - coupon_code
      title: RedeemCoupon
      type: object
    RedeemPool:
      description: A loyalty pool to redeem points from.
      properties:
        club_code:
          description: Loyalty club code (e.g. 'UNI').
          maxLength: 32
          minLength: 1
          title: Club Code
          type: string
        pool_id:
          description: Pool identifier within the club.
          maxLength: 32
          minLength: 1
          title: Pool Id
          type: string
      required:
        - club_code
        - pool_id
      title: RedeemPool
      type: object
    RedeemVoucher:
      description: A loyalty voucher to apply to this redemption.
      properties:
        quantity:
          description: Number of voucher units to redeem.
          maximum: 999999
          minimum: 1
          title: Quantity
          type: integer
        voucher_code:
          description: Voucher identifier to redeem.
          maxLength: 64
          minLength: 1
          title: Voucher Code
          type: string
      required:
        - voucher_code
        - quantity
      title: RedeemVoucher
      type: object
    LoyaltyOutcomeComponent:
      description: >-
        What happened to the loyalty part of a sale.


        Present when the purchase included a loyalty redemption. It reports only
        the

        loyalty side, which can succeed or fail independently of the card
        payment, so read

        it alongside the transaction's own status rather than treating either as
        the whole

        result.
      properties:
        leg_status:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Loyalty leg transaction status (CAPTURED, FAILED, PENDING, etc.) --
            key the leg outcome on this, not on response_code. A sale can
            approve on the card leg and decline on the loyalty leg; the two are
            independent.
          title: Leg Status
        pools:
          anyOf:
            - items:
                $ref: '#/components/schemas/LoyaltyPool'
              type: array
            - type: 'null'
          description: Post-redemption pool balances (returned by some hosts).
          title: Pools
        redeemed_amount:
          anyOf:
            - type: integer
            - type: 'null'
          description: Points redeemed in minor currency units (host-confirmed).
          title: Redeemed Amount
        response_code:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Verbatim acquirer response code from the loyalty leg, for display
            and reconciliation only. Not an outcome signal -- see leg_status.
          title: Response Code
        switch_request_id:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            The upstream processor's own reference for the loyalty part of this
            sale. Quote it when raising an issue with us about the redemption.
          title: Switch Request Id
        vouchers:
          anyOf:
            - items:
                $ref: '#/components/schemas/LoyaltyVoucher'
              type: array
            - type: 'null'
          description: Vouchers consumed in this redemption.
          title: Vouchers
      title: LoyaltyOutcomeComponent
      type: object
    LoyaltyPool:
      description: A loyalty point pool balance returned by a loyalty balance inquiry.
      properties:
        account_indicator:
          anyOf:
            - type: string
            - type: 'null'
          description: Account type indicator from the host.
          title: Account Indicator
        balance_sign:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Sign of the balance: '0' = positive, '1' = negative. This is the
            authoritative sign -- prefer it over the sign of `point_balance`. It
            reports the sign only and does not say whether the pool can be
            redeemed.
          title: Balance Sign
        club_code:
          description: Loyalty club code (e.g. 'UNI').
          title: Club Code
          type: string
        expiry_date:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Point expiry date as DDMMYYYY, passed through as the loyalty
            programme returned it. '00000000' means the points do not expire.
            Expired pools are not filtered out, so hide them yourself if you
            don't want to show them. In sandbox, test cards return fixed
            profiles, so expiry dates there are not realistic.
          title: Expiry Date
        point_balance:
          description: >-
            Current pool balance as a signed decimal (2 decimal places).
            Fractional for monetary pools (e.g. a 'UNI$' cash-back pool returns
            4534.56); whole for point pools. The sign always agrees with
            `balance_sign`, except at exactly zero, where the number carries no
            sign -- read `balance_sign` instead.
          title: Point Balance
          type: number
        pool_id:
          description: Pool identifier within the club.
          title: Pool Id
          type: string
        pool_name:
          description: Human-readable pool label (e.g. 'UNI$').
          title: Pool Name
          type: string
      required:
        - club_code
        - pool_id
        - pool_name
        - point_balance
      title: LoyaltyPool
      type: object
    LoyaltyVoucher:
      description: A redeemable voucher offered against a loyalty pool.
      properties:
        description:
          description: Human-readable voucher label.
          title: Description
          type: string
        max_redeemable:
          description: Maximum vouchers redeemable in one order.
          title: Max Redeemable
          type: integer
        points_price:
          description: >-
            Points required to redeem one voucher. Fractional for monetary pools
            (e.g. a 'UNI$' cash-back voucher priced at 15.50); whole for point
            pools. Host-supplied, so accept both to avoid dropping decimals.
          title: Points Price
          type: number
        pool_name:
          description: Pool the voucher redeems against (e.g. 'UNI$').
          title: Pool Name
          type: string
        redeem_value:
          description: >-
            Monetary value of the voucher in currency major units (e.g. 10.0 =
            $10.00).
          title: Redeem Value
          type: number
        voucher_code:
          description: Voucher identifier used to redeem.
          title: Voucher Code
          type: string
      required:
        - voucher_code
        - description
        - pool_name
        - points_price
        - redeem_value
        - max_redeemable
      title: LoyaltyVoucher
      type: object
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Bearer access token from `POST /v1/auth/token`. Treat it as an opaque
        string — do not depend on its internal encoding, which has changed
        before and isn't part of the contract.
      x-docs-interim: true

````