openapi: 3.0.3
info:
  title: CROSS Pay API
  version: 0.9.9
  description: >-
    CROSS Pay v1 REST API. Merchant servers create a PaymentIntent and
    drive its lifecycle from a server-to-server context using the merchant
    API key.


    Try it out on this site is proxied through the sandbox (`/v1`)
    with the sandbox API key injected server-side. Production keys are never
    exposed to the browser.


    ## Download the spec


    Import the full spec into Postman, an SDK generator, or your IDE. It
    covers exactly the merchant server endpoints documented on this page:


    - [OpenAPI (JSON)](/openapi/v1.json)

    - [OpenAPI (YAML)](/openapi/v1.yaml)


    ## Authentication


    All `/payments/**` endpoints and `GET /catalog` require a merchant API
    key (`Authorization: Bearer <API_KEY>`). The buyer-facing hosted
    checkout, on-chain session lookups for wallet clients, the public
    countries / chains lookups, and provider-side webhooks are consumed by
    the CROSS Pay hosted checkout, wallet SDKs, and the platform itself —
    they are not part of this merchant integration reference.


    ## Environment


    CROSS Pay currently exposes the Production environment
    (`https://api.crosspay.io`): a 53-country catalog, chain slugs
    `ethereum` / `bsc` / `cross`, crypto currencies USDT / CROSSD, and an
    API key prefix of `sk_live_…`.


    A public Sandbox environment (with self-serve test keys, test cards,
    and test tokens) is **planned for an upcoming release**.


    See the [Supported Countries & Payment Method Catalog](/integration/countries)
    for the full per-country mapping.


    ## PaymentStatus overview


    | Status                | Meaning / entry path                                                       |

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

    | `PENDING`             | Right after `POST /payments`. No quote selected yet.                        |

    | `PROCESSING`          | After the buyer selects a payment method on the hosted checkout. The external PG-handoff window — waiting for the PG/chain terminating webhook (`PAID`/`FAILED`) or the expiry worker (`EXPIRED`). |

    | `PAID` / `FAILED`     | A PG/chain webhook terminates `PROCESSING`. Terminal.                       |

    | `EXPIRED`             | TTL expiry worker. Reachable from both `PENDING` and `PROCESSING`. Terminal. |

    | `CANCELLED`           | Merchant cancel call or a buyer-cancel webhook. Reachable only from `PENDING` — a `PROCESSING` cancel is rejected with `409 INVALID_PAYMENT_STATE` due to the PG race. Terminal. |

    | `PARTIALLY_REFUNDED` / `REFUNDED` | Refund results. Reached from `PAID`. Terminal.                  |


    `PENDING → PAID/FAILED` direct transitions are no longer allowed. Every
    webhook-driven terminal transition passes through `PROCESSING`. For the
    full state machine and webhook mapping, see the
    [Webhooks Overview](/webhooks/overview).


    ## Recent changes


    - **New PG: MyCard (v0.9.9, additive)**: the TW/HK/SEA fiat hosted-page
    PG `MyCard` was added to the PG registry. There is no buyer-facing
    wire-shape change — only the value `mycard` was added to the `provider`
    enum on `Payment`, and payments handled by MyCard report `provider` as
    `mycard`. The MyCard ops callbacks are outside this merchant reference
    (webhook group).

    - **`GET /catalog` now requires authentication (v0.9.3, BREAKING)**: the
    formerly public (no-auth) catalog now requires a **Merchant API Key**.
    Calling it without `Authorization: Bearer <API_KEY>` returns 401.
    `Cache-Control` also changed from `public` to `private` so shared caches
    (CDN/proxy) do not store it. `ETag` / `If-None-Match` 304 revalidation is
    unchanged.

    - **`GET /catalog` per-merchant scoping (v0.9.5)**: the catalog is
    filtered per authenticated merchant — it exposes only the currencies
    payable through the merchant's provisioned payment methods
    (`merchant_payment_method`) and omits countries with nothing payable. The
    response wire shape is unchanged; only the returned set narrows. The
    `ETag` now derives from per-merchant freshness.

    - **`include_pegged_assets` now defaults to `true` (v0.7.55, BREAKING)**:
    `POST /payments` flips the opt-in flag introduced in v0.7.51 to opt-out.
    A fiat-currency payment created **without** the flag now also exposes
    stablecoins pegged to that fiat (USDT / USDC / …) as payment options on
    the hosted checkout. Merchants that must keep stablecoins hidden have to
    send an explicit `include_pegged_assets: false`. No other wire-shape or
    DB change. See the field description on `CreatePaymentRequest` for
    before/after behaviour.

    - **Added `GET /catalog` (v0.7.55, additive)**: returns, per supported
    country, the currencies/assets payable through the calling merchant's
    provisioned payment methods, with the display decimals of each. Served
    from a cache with `ETag` / `If-None-Match` revalidation.

    - **Country-level crypto restrictions (v0.7.55)**: an admin-managed
    restriction list (types `CRYPTO` / `ONRAMP`) now governs where crypto
    payment is offered. Checkout quotes hide crypto payment methods when the
    buyer's IP-resolved country (CloudFront-Viewer-Country — authoritative,
    not client-supplied) carries a `CRYPTO` restriction, and `GET /catalog`
    excludes crypto currencies for those countries. Response field
    structures are unchanged — only the returned sets narrow. The former
    on-ramp `country_blacklist` config (default `KR`) migrated into this
    list as `ONRAMP` rows.

    - **Added `PROCESSING` status (v0.7.34, additive)**: `PROCESSING` was
    added to the `PaymentStatus` enum. The PG webhook terminates
    `PROCESSING → PAID/FAILED`. Existing clients keep working, but consumers
    that branch on `status` without a default / unknown path must explicitly
    handle `PROCESSING` (showing a "PG processing — awaiting result" state
    is recommended).

    - **Re-opened `POST /payments/{payment_id}/cancel` (v0.7.34)**: the
    cancel route removed in v0.7.27 is exposed again to merchants with the
    same wire format. Cancelling a `PROCESSING` payment returns
    `409 INVALID_PAYMENT_STATE` — done to avoid a race against the PG. Cancel
    only while `PENDING`, or wait for a terminal result and use the refund
    API.

    - `POST /payments`: the optional `country` field is only forwarded to
    the checkout URL as a `?country=XX` hint and is not persisted to the
    database.
  contact:
    name: Cross Pay Team
servers:
  - url: /v1
    description: Docs sandbox proxy (secrets injected server-side)
tags:
  - name: Payments
    description: Merchant PaymentIntent API (API Key auth)
  - name: Catalog
    description: Per-country payable currency catalog (API Key auth)
x-webhook-events:
  - event: payment.completed
    description: |
      Emitted when the PG or chain reports confirmation and the PaymentIntent
      transitions to `PAID`. Merchants finalize the order and trigger digital
      asset delivery or shipment on receipt.
    payload_example:
      type: payment.completed
      timestamp: "2026-04-21T12:00:00Z"
      data:
        payment:
          id: pay_abc123
          status: PAID
          currency: KRW
          amount: "50000"
          payer_uid: user_789
          receipt_id: rcpt_xyz
          provider: binancepay
          paid_at: "2026-04-21T12:00:00Z"
          metadata:
            order_id: shop-order-789
  - event: payment.failed
    description: |
      Emitted when the PG or chain reports failure and the PaymentIntent
      transitions to `FAILED`. `data.payment.failure_reason` is a controlled
      vocabulary (UNKNOWN/DECLINED/TX_REVERTED/AMOUNT_MISMATCH/
      TOKEN_MISMATCH/ONCHAIN_TIMEOUT).
    payload_example:
      type: payment.failed
      timestamp: "2026-04-21T12:05:00Z"
      data:
        payment:
          id: pay_abc123
          status: FAILED
          currency: KRW
          amount: "50000"
          payer_uid: user_789
          failure_reason: DECLINED
          failed_at: "2026-04-21T12:05:00Z"
          metadata:
            order_id: shop-order-789
  - event: payment.expired
    description: |
      Emitted when the TTL expiry worker sweeps an in-flight PaymentIntent
      (`PENDING` or `PROCESSING`) and transitions it to `EXPIRED`. Merchants
      drive cart recovery or re-payment prompts from this signal.
    payload_example:
      type: payment.expired
      timestamp: "2026-04-21T12:30:00Z"
      data:
        payment:
          id: pay_abc123
          status: EXPIRED
          currency: KRW
          amount: "50000"
          payer_uid: user_789
          expired_at: "2026-04-21T12:30:00Z"
          metadata:
            order_id: shop-order-789
  - event: payment.cancelled
    description: |
      Emitted when the merchant explicitly cancels a `PENDING` PaymentIntent.
      Cancel calls on a `PROCESSING` payment are rejected with
      `409 INVALID_PAYMENT_STATE`, so this event does not fire from that state.
    payload_example:
      type: payment.cancelled
      timestamp: "2026-04-21T12:10:00Z"
      data:
        payment:
          id: pay_abc123
          status: CANCELLED
          currency: KRW
          amount: "50000"
          payer_uid: user_789
          cancelled_at: "2026-04-21T12:10:00Z"
          metadata:
            order_id: shop-order-789
paths:
  /payments:
    post:
      tags:
        - Payments
      summary: Create payment (intent)
      description: >
        Creates a PaymentIntent. The merchant hands the returned `checkout_url` to the buyer (via redirect or link) to start payment. `checkout_id` is the identifier of the same page;
        you should not need to assemble a URL yourself.


        - The `Idempotency-Key` header prevents duplicate creation (24h TTL, Redis-cached).

        - `metadata` is stored and returned as opaque JSON; CROSS Pay does not interpret it.

        - Pass `amount` as a string (preserves shopspring/decimal precision).

        - `expires_in` is in seconds; defaults to 30 minutes when 0 or unset.

        - `checkout_url` has the form `{checkout.base_url}/{checkout_id}`, and is omitted from the response in dev environments when `checkout.base_url` is empty.

        - When `country` (ISO 3166-1 alpha-2, optional) is supplied, it is forwarded as `?country=XX` on the response `checkout_url` so the hosted checkout
        page can pre-select the billing country. Not persisted to the DB — the `checkout_url` on subsequent GET responses will not carry the query.

        - `include_pegged_assets` (optional, **default `true`** since v0.7.55 — previously `false`) controls whether a fiat-currency payment also offers
        stablecoins pegged to that fiat (e.g. USD → USDT / USDC / CROSSD) on the hosted checkout. Send an explicit `false` to keep the checkout fiat-only.
        Payments whose `currency` itself is a token symbol (`USDT`) are unaffected by this flag.
      operationId: create-payment
      security:
        - MerchantApiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePaymentRequest'
            examples:
              with_product:
                summary: With product metadata (all fields)
                value:
                  currency: KRW
                  amount: '50000'
                  payer_uid: user-456
                  expires_in: 1800
                  metadata:
                    product:
                      name: Game Pass Premium
                      detail: 3-month premium subscription
                      image: https://store.example.com/img/game-pass.png
                      category: DIGITAL_GOODS
                      quantity: 1
                      vendor: GameStore
                    order_id: shop-order-789
              physical_goods:
                summary: Physical goods example
                value:
                  currency: USD
                  amount: '149.99'
                  payer_uid: user-101
                  metadata:
                    product:
                      name: Wireless Headphones
                      detail: Noise-cancelling bluetooth headphones
                      category: PHYSICAL_GOODS
                      vendor: AudioTech
                    order_id: order-2026-0042
              minimal:
                summary: Minimum required fields only (no product)
                value:
                  currency: USD
                  amount: '29.99'
                  payer_uid: user-123
              fiat_only:
                summary: Fiat-only checkout (opt out of pegged stablecoins)
                value:
                  currency: USD
                  amount: '29.99'
                  payer_uid: user-123
                  include_pegged_assets: false
      responses:
        '200':
          $ref: '#/components/responses/FDSBlocked'
        '201':
          description: Payment created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payment'
              example:
                id: pay_abc123
                checkout_id: cs_a1b2c3d4
                checkout_url: https://checkout.example.com/cs_a1b2c3d4
                merchant_id: 1
                status: PENDING
                currency: KRW
                amount: '50000'
                payer_uid: user-456
                expires_at: '2026-03-30T12:30:00Z'
                metadata:
                  product:
                    name: Game Pass Premium
                    detail: 3-month premium subscription
                    image: https://store.example.com/img/game-pass.png
                    category: DIGITAL_GOODS
                    quantity: 1
                    vendor: GameStore
                  order_id: shop-order-789
                tax_amount: '0'
                refunded_amount: '0'
                include_pegged_assets: true
                tax_manager: NONE
                created_at: '2026-03-30T12:00:00Z'
                updated_at: '2026-03-30T12:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Idempotency key conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: IDEMPOTENCY_CONFLICT
                message: idempotency key already used with different request body
  /payments/{payment_id}:
    get:
      tags:
        - Payments
      summary: Get payment
      operationId: get-payment
      security:
        - MerchantApiKey: []
      parameters:
        - $ref: '#/components/parameters/PaymentId'
      responses:
        '200':
          description: Payment fetched successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payment'
              examples:
                pending:
                  summary: PENDING status (before quote selection)
                  value:
                    id: pay_abc123
                    checkout_id: cs_a1b2c3d4
                    checkout_url: https://checkout.example.com/cs_a1b2c3d4
                    merchant_id: 1
                    status: PENDING
                    currency: KRW
                    amount: '50000'
                    payer_uid: user-456
                    expires_at: '2026-03-30T12:30:00Z'
                    metadata:
                      product:
                        name: Game Pass Premium
                        category: DIGITAL_GOODS
                      order_id: shop-order-789
                    tax_amount: '0'
                    refunded_amount: '0'
                    include_pegged_assets: true
                    tax_manager: NONE
                    created_at: '2026-03-30T12:00:00Z'
                    updated_at: '2026-03-30T12:00:00Z'
                paid:
                  summary: PAID status (quote included)
                  value:
                    id: pay_def456
                    checkout_id: cs_e5f6g7h8
                    checkout_url: https://checkout.example.com/cs_e5f6g7h8
                    merchant_id: 1
                    status: PAID
                    currency: USDT
                    amount: '36.50'
                    payer_uid: user-789
                    payment_method_id: 1
                    provider: binancepay
                    tax_amount: '3.65'
                    paid_at: '2026-04-08T12:05:00Z'
                    refunded_amount: '0'
                    include_pegged_assets: true
                    tax_manager: SERVICE
                    fee_mode: INCLUSIVE
                    fee_rate: '0.029'
                    fee_amount: '1.06'
                    estimated_tax: '3.65'
                    quote_total_amount: '40.15'
                    metadata:
                      product:
                        name: Monthly Subscription
                        category: SUBSCRIPTION
                        vendor: StreamCo
                      order_id: shop-order-456
                    created_at: '2026-04-08T12:00:00Z'
                    updated_at: '2026-04-08T12:05:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /payments/{payment_id}/cancel:
    post:
      tags:
        - Payments
      summary: Cancel payment
      description: >-
        Only `PENDING` payments can be cancelled. `PROCESSING` payments are owned by the external PG, so a unilateral cancel
        would race with the PG's `PAID` confirmation and is rejected with `409 INVALID_PAYMENT_STATE`. `PAID` / `EXPIRED` /
        `CANCELLED` / `*_REFUNDED` likewise return `409`. If the buyer abandons during `PROCESSING`, the expiry worker
        finalizes it as `EXPIRED` on TTL expiry, or the PG webhook closes it as `FAILED`.


        The `Idempotency-Key` header is optional; retries with the same key replay the first response verbatim (24h TTL, Redis-cached).
      operationId: cancel-payment
      security:
        - MerchantApiKey: []
      parameters:
        - $ref: '#/components/parameters/PaymentId'
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            format: uuid
          description: >-
            Optional idempotency key. When provided, retries with the same key
            replay the first response verbatim (24h TTL, Redis-cached).
          example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '200':
          description: Payment cancelled successfully
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    type: string
                    enum:
                      - cancelled
              example:
                status: cancelled
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: >-
            Invalid status transition. `PROCESSING` / `PAID` / `EXPIRED` / `CANCELLED` / `PARTIALLY_REFUNDED` /
            `REFUNDED` payments cannot be cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                processing:
                  summary: PROCESSING — rejected to avoid PG race
                  value:
                    code: INVALID_PAYMENT_STATE
                    message: cannot cancel payment in PROCESSING status
                paid:
                  summary: PAID — use the refund API instead
                  value:
                    code: INVALID_PAYMENT_STATE
                    message: cannot cancel payment in PAID status
  /catalog:
    get:
      tags:
        - Catalog
      summary: Get payment catalog
      description: >-
        Returns the catalog of active supported countries and the currencies (assets) payable in each, with
        the display decimals of every currency. The catalog is scoped to the calling merchant: it lists only
        the currencies payable through payment methods the merchant is provisioned for, and omits countries
        in which the merchant has nothing payable.


        - `decimals` is the **payment/display precision** of the currency (ISO 4217 for fiat — e.g. `JPY` 0,
        `USD` 2; project convention 4 for crypto symbols). It is **not** the on-chain token decimals, which
        vary per chain (e.g. USDT: Ethereum 6, BSC 18) — resolve those from the chain/token metadata when
        converting to smallest units.

        - Crypto currencies are excluded for countries under an active `CRYPTO` restriction
        (see Recent changes), so the catalog matches what the hosted checkout actually offers there.

        - Responses are cache-backed and carry an `ETag` derived from `updated_at` plus
        `Cache-Control: private, max-age=300`. Revalidate with `If-None-Match` to get a cheap `304` instead
        of re-downloading.
      operationId: get-catalog
      security:
        - MerchantApiKey: []
      parameters:
        - name: If-None-Match
          in: header
          required: false
          schema:
            type: string
          description: >-
            ETag from a previous response. When it still matches the current catalog freshness, the server
            answers `304 Not Modified` with no body.
          example: '"1769904000000000000"'
      responses:
        '200':
          description: Catalog fetched successfully
          headers:
            ETag:
              schema:
                type: string
              description: Freshness tag derived from `updated_at`; changes only when the underlying country/currency data changes.
            Cache-Control:
              schema:
                type: string
              description: '`private, max-age=300` — merchant-key-gated, so shared caches must not store it.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Catalog'
              example:
                updated_at: '2026-06-01T09:00:00Z'
                countries:
                  - country_code: JP
                    name: Japan
                    currencies:
                      - currency: JPY
                        decimals: 0
                      - currency: USDT
                        decimals: 4
                      - currency: CROSSD
                        decimals: 4
                  - country_code: US
                    name: United States
                    currencies:
                      - currency: USD
                        decimals: 2
                      - currency: USDT
                        decimals: 4
                      - currency: CROSSD
                        decimals: 4
        '304':
          description: Not modified — the `If-None-Match` ETag still matches; reuse the cached body.
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  securitySchemes:
    MerchantApiKey:
      type: http
      scheme: bearer
      description: Merchant API Key
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        format: uuid
      description: Idempotency key (24h TTL)
      example: 550e8400-e29b-41d4-a716-446655440000
    PaymentId:
      name: payment_id
      in: path
      required: true
      schema:
        type: string
      description: Payment ID
      example: pay_abc123
  schemas:
    CreatePaymentRequest:
      type: object
      required:
        - currency
        - amount
        - payer_uid
      properties:
        currency:
          type: string
          description: Payment currency (ISO 4217)
          example: KRW
        amount:
          type: string
          description: Payment amount (string, preserves decimal precision)
          example: '50000'
        payer_uid:
          type: string
          description: Merchant-side user identifier (required)
          example: user-456
        payer_email:
          type: string
          format: email
          deprecated: true
          description: >-
            Payer email. Kept for backward compatibility; the email the buyer
            enters on the hosted checkout page takes precedence and overwrites
            this value. Validated as an email when provided.
          example: user@example.com
        expires_in:
          type: integer
          minimum: 0
          maximum: 86400
          description: Expiration time in seconds. Defaults to 1800s (30 minutes) when 0 or unset. Maximum 86400s (24 hours).
          example: 1800
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: >-
            ISO 3166-1 alpha-2 country code (optional). When provided, it is forwarded as `?country=XX` on the response
            `checkout_url` and used as a hint so the hosted checkout pre-selects the billing country. Not persisted to the DB —
            flows only on the POST response; the `checkout_url` on subsequent GET responses does not carry the query. No semantic
            validation beyond length.
          example: KR
        include_pegged_assets:
          type: boolean
          default: true
          description: >-
            Whether the hosted checkout also offers stablecoins pegged to the payment's fiat `currency`
            (e.g. USD → USDT / USDC / CROSSD) as payment options. **Defaults to `true` when omitted** —
            flipped from the v0.7.51 opt-in policy in v0.7.55 (BREAKING); merchants that must keep
            stablecoins hidden now send an explicit `false`. Only meaningful for fiat-currency payments:
            when `currency` itself names a token symbol (`USDT`), that symbol is matched directly and this
            flag has no effect. The resolved value is persisted at creation and echoed on every Payment
            response.
          example: true
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Merchant-domain information. Well-known key: `product` (name, detail, image, quantity, category, vendor). category enum:
            DIGITAL_GOODS, PHYSICAL_GOODS, IN_GAME_ITEM, SUBSCRIPTION. Other keys are stored and returned opaquely.
          properties:
            product:
              type: object
              properties:
                name:
                  type: string
                  description: Product name (shown on the PG payment window)
                detail:
                  type: string
                  description: Product description
                image:
                  type: string
                  format: uri
                  description: Product image URL
                quantity:
                  type: integer
                  minimum: 1
                  description: Quantity (defaults to 1)
                category:
                  type: string
                  enum:
                    - DIGITAL_GOODS
                    - PHYSICAL_GOODS
                    - IN_GAME_ITEM
                    - SUBSCRIPTION
                  description: Product category
                vendor:
                  type: string
                  description: Seller / publisher
    Payment:
      type: object
      description: Payment entity. Fee/quote fields are omitted from the response before a quote is selected (omitempty).
      properties:
        id:
          type: string
          example: pay_abc123
        checkout_id:
          type: string
          example: cs_a1b2c3d4
        checkout_url:
          type: string
          format: uri
          description: >-
            Hosted checkout page URL the buyer uses to complete payment. Format `{checkout.base_url}/{checkout_id}`. Omitted from the
            response (omitempty) when `base_url` is unset (dev environment).
          example: https://checkout.example.com/cs_a1b2c3d4
        merchant_id:
          type: integer
          example: 1
        status:
          $ref: '#/components/schemas/PaymentStatus'
        currency:
          type: string
          example: KRW
        amount:
          type: string
          description: Decimal string
          example: '50000'
        payer_uid:
          type: string
          example: user-456
        payer_email:
          type: string
          description: omitempty — omitted when unset
          example: user@example.com
        expires_at:
          type: string
          format: date-time
          description: omitempty
        metadata:
          type: object
          additionalProperties: true
        payment_method_id:
          type: integer
          description: omitempty — set after quote selection
        provider:
          type: string
          enum:
            - binancepay
            - payletter
            - crosspay
            - worldpay
            - mycard
          description: omitempty — set after quote selection. Same as the PG registry key (pg.PGName).
        tax_amount:
          type: string
          description: Decimal string
          example: '0'
        paid_at:
          type: string
          format: date-time
          description: omitempty — set after payment completion
        refunded_amount:
          type: string
          description: Decimal string
          example: '0'
        include_pegged_assets:
          type: boolean
          description: >-
            The creation-time pegged-stablecoin exposure choice (see `CreatePaymentRequest.include_pegged_assets`).
            Always present; an omitted field at creation resolves to `true`.
          example: true
        tax_manager:
          $ref: '#/components/schemas/TaxManager'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        fee_mode:
          $ref: '#/components/schemas/FeeMode'
        fee_rate:
          type: string
          description: omitempty — decimal string
        fee_amount:
          type: string
          description: omitempty — decimal string
        estimated_tax:
          type: string
          description: omitempty — decimal string
        quote_total_amount:
          type: string
          description: omitempty — decimal string
        suggested_country:
          type: string
          minLength: 2
          maxLength: 2
          pattern: '^[A-Z]{2}$'
          description: >-
            ISO 3166-1 alpha-2 country hint. Present only in
            `GET /v1/checkout/{id}` responses (derived from the
            CloudFront-Viewer-Country header); never returned by the
            `/payments/**` merchant endpoints. UI hint only — do not use for
            tax, routing, or access control.
          example: KR
    PaymentStatus:
      type: string
      enum:
        - PENDING
        - PROCESSING
        - PAID
        - FAILED
        - EXPIRED
        - CANCELLED
        - PARTIALLY_REFUNDED
        - REFUNDED
      description: >-
        Payment status. `PENDING` is the pre-quote-selection state; `PROCESSING` is the external handoff window after PG session
        creation, waiting for a finalizing webhook (`PAID`/`FAILED`) or the expiry worker (`EXPIRED`). Direct `PENDING → PAID/FAILED`
        is not allowed — every webhook-driven terminal transition passes through `PROCESSING`. See
        [Webhooks Overview](/webhooks/overview) for full transitions.
    TaxManager:
      type: string
      enum:
        - NONE
        - SERVICE
        - PG
      description: Party responsible for tax management
    FeeMode:
      type: string
      enum:
        - INCLUSIVE
        - EXCLUSIVE
      description: |
        Fee mode:
        - INCLUSIVE — fee included (deducted at merchant settlement)
        - EXCLUSIVE — fee added on top (paid by the buyer)
    Catalog:
      type: object
      description: >-
        Full catalog snapshot for the calling merchant. `updated_at` is the freshness timestamp the `ETag`
        derives from.
      required:
        - updated_at
        - countries
      properties:
        updated_at:
          type: string
          format: date-time
          description: Last time the underlying country/currency/token/chain data changed.
          example: '2026-06-01T09:00:00Z'
        countries:
          type: array
          items:
            $ref: '#/components/schemas/CatalogCountry'
    CatalogCountry:
      type: object
      description: One active supported country and the currencies payable in it for this merchant.
      required:
        - country_code
        - name
        - currencies
      properties:
        country_code:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2. The `GL` global bucket is merged into every country and never surfaced standalone.
          example: JP
        name:
          type: string
          example: Japan
        currencies:
          type: array
          description: >-
            Payable currencies — fiat codes first (country-specific then global), crypto symbols last,
            deduped. Crypto entries are dropped for `CRYPTO`-restricted countries.
          items:
            $ref: '#/components/schemas/CatalogCurrency'
    CatalogCurrency:
      type: object
      description: One payable asset and its display decimals.
      required:
        - currency
        - decimals
      properties:
        currency:
          type: string
          description: ISO 4217 fiat code or crypto token symbol.
          example: JPY
        decimals:
          type: integer
          format: int32
          description: >-
            Payment/display precision — ISO 4217 exponent for fiat, project convention 4 for crypto symbols.
            Not the on-chain token decimals.
          example: 0
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
        details:
          type: string
          description: omitempty
    FDSDecision:
      type: object
      description: >-
        Fraud Detection System outcome for a single request. **Allow** responses include the minimal envelope `{outcome:
        "ALLOW"}` so SDKs can uniformly branch on `fds.outcome` regardless of decision. **Deny** responses additionally
        include `code`, `message`, `risk_level`, `risk_score`, `rule` so merchant CS / chargeback workflows have the
        full context. `code` is a stable enum that identifies which rule fired and is forward-compatible — treat unknown
        codes as generic blocks.
      required:
        - outcome
      properties:
        outcome:
          type: string
          enum:
            - ALLOW
            - DENY
          description: Decision outcome. Future values may include REVIEW / CHALLENGE.
          example: DENY
        code:
          type: string
          description: >-
            Stable rule identifier surfaced on Deny (e.g. PAYER_BLOCKED, COUNTRY_BLOCKED). New rules add new code
            values; treat unknown codes as a generic block. Omitted on Allow.
          example: PAYER_BLOCKED
        message:
          type: string
          description: Human-readable summary on Deny. Ops/debug context, not user-facing copy. Omitted on Allow.
          example: payer is blocked
        risk_level:
          type: string
          enum:
            - normal
            - elevated
            - highest
          description: >-
            Coarse-grained risk classification on Deny. Today blocklist rules emit `highest` (hard signal); future
            velocity/ML rules will populate the full range. Omitted on Allow.
          example: highest
        risk_score:
          type: integer
          minimum: 0
          maximum: 100
          description: >-
            Numeric risk score (0..100) on Deny. Currently 100 for blocklist hits; future rules emit fractional values.
            Omitted on Allow.
          example: 100
        rule:
          type: string
          description: >-
            Stable rule identifier on Deny (e.g. payer_blocklist, country_blocklist). Distinct from `code` so multiple
            sub-rules can share a Reason (e.g. several velocity policies under VELOCITY_EXCEEDED). Omitted on Allow.
          example: payer_blocklist
    FDSBlockedResponse:
      type: object
      description: >-
        Envelope returned with HTTP 200 OK on `/v1/payments` endpoints when FDS denies the request. The normal success
        response of the affected endpoint is NOT returned alongside — `fds` is the only top-level field. SDKs distinguish
        via `fds.outcome === "DENY"`.
      required:
        - fds
      properties:
        fds:
          $ref: '#/components/schemas/FDSDecision'
  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: BAD_REQUEST
            message: bad request
    Unauthorized:
      description: Authentication failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: UNAUTHORIZED
            message: unauthorized
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: NOT_FOUND
            message: resource not found
    FDSBlocked:
      description: >-
        FDS denied the request. The HTTP status stays 200 (this is a business outcome, not a transport-level error) and
        the body is the `{ fds: ... }` envelope. Any `/v1/payments` endpoint can return this response.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/FDSBlockedResponse'
          examples:
            payer_blocked:
              summary: globally blocked buyer
              value:
                fds:
                  outcome: DENY
                  code: PAYER_BLOCKED
                  message: payer is blocked
                  risk_level: highest
                  risk_score: 100
                  rule: payer_blocklist
            country_blocked:
              summary: country on the blocklist
              value:
                fds:
                  outcome: DENY
                  code: COUNTRY_BLOCKED
                  message: country is blocked
                  risk_level: highest
                  risk_score: 100
                  rule: country_blocklist
