{
  "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.\n\nTry 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.\n\n## Download the spec\n\nImport the full spec into Postman, an SDK generator, or your IDE. It covers exactly the merchant server endpoints documented on this page:\n\n- [OpenAPI (JSON)](/openapi/v1.json)\n- [OpenAPI (YAML)](/openapi/v1.yaml)\n\n## Authentication\n\nAll `/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.\n\n## Environment\n\nCROSS 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_…`.\n\nA public Sandbox environment (with self-serve test keys, test cards, and test tokens) is **planned for an upcoming release**.\n\nSee the [Supported Countries & Payment Method Catalog](/integration/countries) for the full per-country mapping.\n\n## PaymentStatus overview\n\n| Status                | Meaning / entry path                                                       |\n| --------------------- | -------------------------------------------------------------------------- |\n| `PENDING`             | Right after `POST /payments`. No quote selected yet.                        |\n| `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`). |\n| `PAID` / `FAILED`     | A PG/chain webhook terminates `PROCESSING`. Terminal.                       |\n| `EXPIRED`             | TTL expiry worker. Reachable from both `PENDING` and `PROCESSING`. Terminal. |\n| `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. |\n| `PARTIALLY_REFUNDED` / `REFUNDED` | Refund results. Reached from `PAID`. Terminal.                  |\n\n`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).\n\n## Recent changes\n\n- **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).\n- **`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.\n- **`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.\n- **`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.\n- **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.\n- **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.\n- **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).\n- **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.\n- `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\ntransitions to `PAID`. Merchants finalize the order and trigger digital\nasset delivery or shipment on receipt.\n",
      "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\ntransitions to `FAILED`. `data.payment.failure_reason` is a controlled\nvocabulary (UNKNOWN/DECLINED/TX_REVERTED/AMOUNT_MISMATCH/\nTOKEN_MISMATCH/ONCHAIN_TIMEOUT).\n",
      "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\n(`PENDING` or `PROCESSING`) and transitions it to `EXPIRED`. Merchants\ndrive cart recovery or re-payment prompts from this signal.\n",
      "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.\nCancel calls on a `PROCESSING` payment are rejected with\n`409 INVALID_PAYMENT_STATE`, so this event does not fire from that state.\n",
      "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.\n\n- The `Idempotency-Key` header prevents duplicate creation (24h TTL, Redis-cached).\n- `metadata` is stored and returned as opaque JSON; CROSS Pay does not interpret it.\n- Pass `amount` as a string (preserves shopspring/decimal precision).\n- `expires_in` is in seconds; defaults to 30 minutes when 0 or unset.\n- `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.\n- 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.\n- `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.\n",
        "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`.\n\nThe `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.\n\n- `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.\n- 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.\n- 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:\n- INCLUSIVE — fee included (deducted at merchant settlement)\n- EXCLUSIVE — fee added on top (paid by the buyer)\n"
      },
      "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"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
