{
  "openapi": "3.1.0",
  "info": {
    "title": "Key2Pay API",
    "version": "2026-05-01",
    "summary": "Multi-tenant payment orchestration platform — LATAM-first.",
    "description": "Public REST API for accepting payments through Key2Pay's provider cascade.\n\n**Conventions:**\n- JSON request/response bodies, UTF-8.\n- Field names are `camelCase` everywhere. Timestamps are ISO-8601 strings.\n- **Operation currency is ALWAYS USD.** Every payment is initiated in USD — the `amount` you send to `POST /payments` and `POST /checkout/sessions` is in USD major units, and there is no input currency to choose. Key2Pay converts USD → the buyer's local currency automatically (per country + method) and returns the buyer-side figures as `amountLocal` + `currencyLocal` on the transaction. You always price + settle in USD.\n- Money is in major units (e.g. `50.00` = $50.00 USD). The refund endpoint is the one exception: it takes amounts in the original transaction's LOCAL currency, also major units.\n- Paginated lists use offset envelope: `?limit=&offset=` → `{ data, pagination: { total, limit, offset, pages } }`.\n- All authenticated endpoints take `Authorization: Bearer <accessToken>` (minted via POST /auth/token).\n\n**Sandbox:** swap base URL to `https://sandbox.key2pay.ai/api/v1` and use `sk_test_…` / `pk_test_…` keys. Add `Sandbox-Simulate: paid` / `failed` / `expired` / `chargeback` / `slow_payment` to drive deterministic outcomes from CI.\n\n**Full guides:** see https://docs.key2pay.ai/docs for the conceptual docs (lifecycle, settlement, webhooks signing, multi-tenant model).",
    "contact": {
      "name": "Key2Pay support",
      "url": "https://docs.key2pay.ai/docs"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.key2pay.ai/api/v1",
      "description": "Production"
    },
    {
      "url": "https://sandbox.key2pay.ai/api/v1",
      "description": "Sandbox — test keys only, no real money movement"
    },
    {
      "url": "https://api.key2pays.com/api/v1",
      "description": "Production (legacy — deprecated, sunset 2026-06-30). Migrate to https://api.key2pay.ai/api/v1."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Auth",
      "description": "Token exchange + rotation."
    },
    {
      "name": "Health",
      "description": "Ping, identity, balance."
    },
    {
      "name": "Payment methods",
      "description": "Catalog of methods enabled per shop."
    },
    {
      "name": "Payments",
      "description": "Pay-in: create, retrieve, list, hosted checkout."
    },
    {
      "name": "Refunds",
      "description": "Refund a captured payment (full or partial)."
    },
    {
      "name": "Payouts",
      "description": "Pay-out environment: balance, transfer from pay-in, FX swap, send payouts."
    },
    {
      "name": "Webhooks",
      "description": "Register endpoints, rotate signing secrets, inspect delivery log, replay."
    }
  ],
  "paths": {
    "/auth/token": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Exchange apiKey + secretKey for a Bearer access token",
        "description": "Two-credential auth flow. Exchange your shop's apiKey + secretKey pair for a short-lived Bearer token (15 min) plus a refresh token (30 days). The Bearer is what you send on every other endpoint.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey",
                  "secretKey"
                ],
                "properties": {
                  "apiKey": {
                    "type": "string",
                    "description": "Publishable id of the key pair.",
                    "example": "pk_test_shp…832a_kzcb1jy4gy"
                  },
                  "secretKey": {
                    "type": "string",
                    "description": "Secret half of the key pair.",
                    "example": "sk_test_shp…832a_c323glhdenhuva7u10"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accessToken": {
                      "type": "string",
                      "description": "Short-lived JWT. Pass on every other endpoint as `Authorization: Bearer …`."
                    },
                    "refreshToken": {
                      "type": "string",
                      "description": "Long-lived token used with POST /auth/refresh. Rotate together with accessToken."
                    },
                    "tokenType": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ]
                    },
                    "expiresIn": {
                      "type": "integer",
                      "description": "Seconds until the accessToken expires (1 hour).",
                      "example": 3600
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/auth/refresh": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Rotate the access + refresh token pair",
        "description": "Use the refresh token from /auth/token to mint a fresh access + refresh pair. Both rotate — the old refresh token becomes invalid as soon as the new one is issued.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "refreshToken"
                ],
                "properties": {
                  "refreshToken": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token rotated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accessToken": {
                      "type": "string"
                    },
                    "refreshToken": {
                      "type": "string"
                    },
                    "tokenType": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ]
                    },
                    "expiresIn": {
                      "type": "integer",
                      "description": "Seconds until the accessToken expires (1 hour).",
                      "example": 3600
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/ping": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Credential + connectivity check",
        "description": "200 confirms your key works, what environment you're on, and which API version you're pinning to. No side effects, cheap to call.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "sandbox",
                        "production"
                      ],
                      "example": "sandbox"
                    },
                    "keyKind": {
                      "type": "string",
                      "enum": [
                        "secret",
                        "publishable"
                      ],
                      "example": "secret"
                    },
                    "keyId": {
                      "type": "string",
                      "example": "sk_test_sh…7u10"
                    },
                    "apiVersion": {
                      "type": "string",
                      "example": "2026-05-01"
                    },
                    "merchant": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "MCH-ON-009"
                        },
                        "name": {
                          "type": "string",
                          "example": "Golden Dragon"
                        }
                      }
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/me": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Resolve the merchant identity behind the credential",
        "description": "Returns the merchant id, name, industry, tier, trustScore, capabilities flags, and active environment. Useful as a sanity check after auth.",
        "responses": {
          "200": {
            "description": "Identity payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "example": "MCH-ON-009"
                    },
                    "name": {
                      "type": "string",
                      "example": "Golden Dragon"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "industry": {
                      "type": "string",
                      "example": "money_services_business"
                    },
                    "tier": {
                      "type": "string",
                      "enum": [
                        "starter",
                        "standard",
                        "premium"
                      ],
                      "example": "starter"
                    },
                    "trustScore": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 1000,
                      "example": 400
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "sandbox",
                        "production"
                      ]
                    },
                    "capabilities": {
                      "type": "object",
                      "properties": {
                        "checkout": {
                          "type": "boolean"
                        },
                        "directCharge": {
                          "type": "boolean"
                        },
                        "refunds": {
                          "type": "boolean"
                        },
                        "webhooks": {
                          "type": "boolean"
                        },
                        "crypto": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "methods": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/me/balance": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Get the merchant's current balance + fee summary + reserve schedule",
        "description": "Returns four balance buckets (available, pending, frozen, reserve) all in USD major units, plus a 90-day fee summary and the next 3 upcoming reserve releases. See /docs/settlement-flow for the lifecycle.",
        "responses": {
          "200": {
            "description": "Balance + summary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "merchantId": {
                      "type": "string"
                    },
                    "currency": {
                      "type": "string",
                      "enum": [
                        "USD"
                      ]
                    },
                    "balance": {
                      "type": "object",
                      "properties": {
                        "available": {
                          "type": "number",
                          "example": 12480.55
                        },
                        "pending": {
                          "type": "number",
                          "example": 230.1
                        },
                        "frozen": {
                          "type": "number",
                          "example": 0
                        },
                        "reserve": {
                          "type": "number",
                          "example": 1500
                        }
                      }
                    },
                    "tier": {
                      "type": "string",
                      "enum": [
                        "starter",
                        "standard",
                        "premium"
                      ]
                    },
                    "trustScore": {
                      "type": "integer"
                    },
                    "rollingReservePct": {
                      "type": "number",
                      "example": 5
                    },
                    "feeSummary": {
                      "type": "object",
                      "properties": {
                        "grossVolume": {
                          "type": "number"
                        },
                        "platformFees": {
                          "type": "number"
                        },
                        "processingFees": {
                          "type": "number"
                        },
                        "networkFees": {
                          "type": "number"
                        },
                        "chargebackFees": {
                          "type": "number"
                        },
                        "totalFees": {
                          "type": "number"
                        },
                        "netBalance": {
                          "type": "number"
                        }
                      }
                    },
                    "movements": {
                      "type": "array",
                      "description": "Last 100 balance movements (capture, fee, freeze, reserve, etc.).",
                      "items": {
                        "type": "object"
                      }
                    },
                    "reserveSchedule": {
                      "type": "array",
                      "description": "Next 3 upcoming reserve releases.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "amount": {
                            "type": "number"
                          },
                          "releaseDate": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "fromDate": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/payment-methods": {
      "get": {
        "tags": [
          "Payment methods"
        ],
        "summary": "List the methods the authenticated shop can accept",
        "description": "One row per end-user-visible payment rail (Walmart, BBVA, SPEI, OXXO, …). Each entry has a stable 4-digit `paymentMethodId` that you store and send back on POST /payments. Each entry also carries `iconUrl` — an absolute, public, cacheable URL to the method's icon hosted by us that you can render directly (`<img src={iconUrl}>`); it always resolves (a custom uploaded logo when set, otherwise a generic category icon). NOT paginated — this is a small per-country catalog. Cache it.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ISO-2 or ISO-3 — restrict to one country.",
            "example": "MEX"
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "ONLINE",
                "CASH",
                "CREDIT_CARD"
              ]
            }
          },
          {
            "name": "method",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Internal slug filter (e.g. spei, oxxo, voucher)."
          }
        ],
        "responses": {
          "200": {
            "description": "Catalog response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "shop": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "sandbox",
                        "production"
                      ]
                    },
                    "filters": {
                      "type": "object"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "totalAvailable": {
                      "type": "integer"
                    },
                    "routableCount": {
                      "type": "integer"
                    },
                    "methods": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PaymentMethod"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/payments": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create a payment (direct-charge flow)",
        "description": "Creates a transaction. **The `amount` is ALWAYS in USD** — Key2Pay's operation currency is USD and the payment is always initiated in USD (there is no input currency field). We convert USD → the buyer's local currency automatically and return `amountLocal` + `currencyLocal`. Response includes `paymentFormUrl` — the upstream provider's hosted checkout URL — for direct-charge integrations that build their own UI. For a hosted-checkout flow where we render the per-method UI, use POST /checkout/sessions instead. The `paymentMethodId` is the 4-digit id from GET /payment-methods.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Replay-safe retry. Same key + same body returns the original response. Different body returns 409 idempotency_conflict. TTL 24h."
          },
          {
            "name": "Sandbox-Simulate",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "paid",
                "failed",
                "expired",
                "chargeback",
                "slow_payment"
              ]
            },
            "description": "Sandbox-only. Schedules a deterministic state transition. See /docs/sandbox-simulate."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount"
                ],
                "properties": {
                  "amount": {
                    "type": "number",
                    "description": "ALWAYS in USD (major units, e.g. 50 = $50.00 USD). Key2Pay initiates every payment in USD — there is NO currency field. The buyer-side local amount + currency are computed automatically and returned as amountLocal/currencyLocal. >0, ≤100,000.",
                    "example": 50
                  },
                  "paymentMethodId": {
                    "type": "string",
                    "description": "4-digit id from GET /payment-methods. Recommended.",
                    "example": "1008"
                  },
                  "paymentMethod": {
                    "type": "string",
                    "description": "LEGACY. Slug taxonomy (spei, oxxo, voucher, …). Use paymentMethodId in new code."
                  },
                  "country": {
                    "type": "string",
                    "description": "ISO-2 or ISO-3 — normalized to ISO-3 internally.",
                    "example": "MEX"
                  },
                  "userEmail": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254,
                    "description": "Required when the payment is initiated via the hosted checkout / payment-link flow (the checkout collects it); optional for direct API calls. The buyer's email — must be a valid address. In the checkout flow, if absent/malformed the response is 422 `missing_required_fields` naming `email`."
                  },
                  "userName": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Required when the payment is initiated via the hosted checkout / payment-link flow (the checkout collects it); optional for direct API calls. The buyer's full name (at least 2 words). In the checkout flow, if absent/malformed the response is 422 `missing_required_fields` naming `userName`."
                  },
                  "userPhone": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "The customer phone. Some providers (OXXO, PIX, voucher) require it for the upstream charge."
                  },
                  "documentId": {
                    "type": "string",
                    "description": "Required when the payment is initiated via the hosted checkout / payment-link flow (the checkout collects it); optional for direct API calls. Buyer identity document: RFC or CURP for Mexico, Brazil PIX (CPF — exactly 11 digits, no dots or dash, valid check digits), CC/DNI/etc. elsewhere. Note: individual rails may still require it on a direct charge — if a rail needs it and it's missing/malformed the response is 422 `missing_required_fields` with `details.missingFields` naming it (see Smart missing-data recovery below). Forwarded to the provider, not persisted on the tx.",
                    "example": "BADD110313HCMLNS09"
                  },
                  "documentType": {
                    "type": "string",
                    "description": "Type of documentId (e.g. RFC, CURP). Inferred when omitted."
                  },
                  "vpa": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "UPI VPA (India / Bitolo UPI rails), e.g. user@bank. Required by UPI; surfaced via 422 missing_required_fields when absent."
                  },
                  "userIp": {
                    "type": "string",
                    "description": "Buyer IP for fraud scoring. Auto-captured by the hosted checkout; pass it on direct server-to-server charges."
                  },
                  "merchantOrderId": {
                    "type": "string",
                    "description": "Your own reference id (≤120 chars). Queryable via ?merchantOrderId=… on GET /payments.",
                    "example": "ORD-12345"
                  },
                  "hostedCheckout": {
                    "type": "boolean",
                    "description": "When true, include `checkoutUrl` in the response and omit `paymentFormUrl`. Equivalent to calling POST /checkout/sessions."
                  },
                  "returnUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where the hosted checkout sends the customer after completion. Only used with hostedCheckout=true."
                  },
                  "merchantId": {
                    "type": "string",
                    "description": "Only required for multi-merchant tokens; defaults to the auth-derived merchant."
                  },
                  "shopId": {
                    "type": "string",
                    "description": "Only required for multi-shop tokens; defaults to the auth-derived shop."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transaction created. Status is `pending` for async methods (SPEI, OXXO, voucher); `completed` for sync methods (card approved immediately).",
            "headers": {
              "Idempotent-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Set when this response is a replay of a previous request with the same Idempotency-Key."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transactionId": {
                      "type": "string",
                      "example": "TXN-MP2WEMT1-KAPL"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "completed"
                      ]
                    },
                    "amount": {
                      "type": "number"
                    },
                    "amountLocal": {
                      "type": "number"
                    },
                    "currencyLocal": {
                      "type": "string"
                    },
                    "paymentMethodId": {
                      "type": "string",
                      "description": "Echoed back from the input."
                    },
                    "paymentMethod": {
                      "type": "string",
                      "description": "Canonical slug of the rail we routed to (e.g. `spei`, `pix`, `oxxo`).",
                      "example": "spei"
                    },
                    "paymentMethodName": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Human display name of the routed rail (the operator's label, e.g. `SPEI`).",
                      "example": "SPEI"
                    },
                    "logoUrl": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri",
                      "description": "Absolute public URL of the routed payment method's brand logo — render it directly in your own checkout UI. Reflects the operator's latest uploaded icon in real time; never expires.",
                      "example": "https://api.key2pay.ai/api/payment-method-logo/spei__mex?v=2026-07-04T00:00:00.000Z"
                    },
                    "fees": {
                      "type": "object"
                    },
                    "settlement": {
                      "type": "object"
                    },
                    "paymentFormUrl": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri",
                      "description": "Upstream provider URL (direct-charge flow)."
                    },
                    "checkoutUrl": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri",
                      "description": "Key2Pay-hosted URL (hosted-checkout flow). Mutually exclusive with paymentFormUrl in the response."
                    },
                    "paymentData": {
                      "type": "object",
                      "description": "Method-specific instructions. See /docs/payment-data-shapes."
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "409": {
            "description": "Idempotency conflict — same Idempotency-Key replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "Either `cascade_exhausted` (no provider could take the charge for this method/country) OR `missing_required_fields` — the rail needs a buyer field that's missing or malformed. In the latter case `error.details.missingFields` (or `malformedFields`) lists each field with `key`, `type`, `label` and its validation rule. Collect them and retry the SAME charge; you may reuse the same Idempotency-Key (errors are never cached, so the corrected body goes through).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "List payments (paginated)",
        "description": "Same `{ data, pagination }` envelope as every other listing.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "completed",
                "failed",
                "expired",
                "refunded",
                "chargeback"
              ]
            }
          },
          {
            "name": "paymentMethodId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "4-digit id to filter to a specific retailer/bank (e.g. 1003 = Walmart MEX)."
          },
          {
            "name": "country",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "merchantOrderId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Your reference id. Useful for disaster recovery if you lost the txId."
          },
          {
            "name": "method",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "LEGACY slug bucket filter. Prefer paymentMethodId."
          }
        ],
        "responses": {
          "200": {
            "description": "Page of transactions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Transaction"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/payments/{id}": {
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "Retrieve a single transaction",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transaction.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transaction"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/payments/{id}/refund": {
      "post": {
        "tags": [
          "Refunds"
        ],
        "summary": "Refund a captured payment (full or partial)",
        "description": "Opens an internal claim that releases funds back through the original provider. `amount` is in LOCAL-currency MAJOR units (e.g. 100 = 100 MXN, NOT cents). Omit to refund the full transaction.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "number",
                    "description": "Local-currency major units. Defaults to the full captured amount.",
                    "example": 100
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 500,
                    "example": "requested_by_customer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Refund claim opened.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "refundId": {
                      "type": "string",
                      "example": "CLM-MP331G7R-9F49"
                    },
                    "transactionId": {
                      "type": "string",
                      "example": "TXN-MP331BTF-R308"
                    },
                    "amount": {
                      "type": "number",
                      "example": 100
                    },
                    "amountUsd": {
                      "type": "number",
                      "example": 5.806
                    },
                    "currency": {
                      "type": "string",
                      "example": "MXN"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    },
                    "reason": {
                      "type": "string"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/payments/{id}/simulate": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Simulate a status transition (sandbox only)",
        "description": "SANDBOX ONLY. Drives a test payment to a terminal status on demand and fires the matching signed webhook(s). `paid`/`failed`/`expired` require a `pending` tx; `refunded`/`chargeback` require a `completed` tx (so to test a chargeback, call `paid` first, then `chargeback`). In production this is refused with 400. See /docs/sandbox-testing.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "paid",
                      "failed",
                      "expired",
                      "refunded",
                      "chargeback"
                    ],
                    "example": "paid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transition applied; webhook(s) fired.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "example": "TXN-MP331BTF-R308"
                    },
                    "action": {
                      "type": "string",
                      "example": "mark_paid"
                    },
                    "previousStatus": {
                      "type": "string",
                      "example": "pending"
                    },
                    "status": {
                      "type": "string",
                      "example": "completed"
                    },
                    "eventsFired": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "payment.completed",
                        "payment.captured"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/checkout/sessions": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create a hosted-checkout session",
        "description": "Hosted-checkout flow. **Two modes** based on whether `paymentMethodId` is present:\n\n- **SELECTOR MODE** (omit `paymentMethodId`): we mint a `cs_xxx` session token. The returned `checkoutUrl` points at `/checkout/<token>` — a premium grid where the customer picks the method (logo + country flag + USD limits + fee per card). After they pick, we create the tx + redirect to `/c/<txId>` which redirects to the provider's hosted form. Recommended for most integrations — one line of code.\n\n- **ONE-SHOT MODE** (include `paymentMethodId`): back-compat behavior. We create the tx immediately and return `/c/<txId>` directly. Use when you already picked the method in your own UI.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount"
                ],
                "properties": {
                  "amount": {
                    "type": "number",
                    "example": 100,
                    "description": "Charge amount in USD (major units). Key2Pay initiates every payment in USD; the buyer's local equivalent is computed automatically."
                  },
                  "currency": {
                    "type": "string",
                    "example": "USD",
                    "default": "USD",
                    "description": "Operation currency — ALWAYS \"USD\" for Key2Pay. Defaults to USD; you don't need to send it. (We don't accept a non-USD operation currency: the buyer-side local conversion is automatic.)"
                  },
                  "paymentMethodId": {
                    "type": "string",
                    "example": "1008",
                    "description": "Omit to activate SELECTOR MODE. Include to go straight to /c/<txId> ONE-SHOT MODE."
                  },
                  "country": {
                    "type": "string",
                    "example": "MX",
                    "description": "ISO-2 or ISO-3. Selector mode: filters the grid to that country. Without it, all methods of the shop are shown."
                  },
                  "customer": {
                    "type": "object",
                    "description": "**REQUIRED in selector mode** (PR #105). The platform forwards these to the provider on every charge — missing fields produce per-provider 400s downstream that drop the tx AFTER the customer clicked Pay. Validated at session create.",
                    "required": [
                      "firstName",
                      "lastName",
                      "email",
                      "phone"
                    ],
                    "properties": {
                      "firstName": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 60,
                        "example": "Carlos"
                      },
                      "lastName": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 60,
                        "example": "Pérez"
                      },
                      "email": {
                        "type": "string",
                        "format": "email",
                        "maxLength": 254,
                        "example": "buyer@acme.com"
                      },
                      "phone": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 40,
                        "example": "+52 55 1234 5678",
                        "description": "Include country code (e.g. +52)."
                      },
                      "documentId": {
                        "type": "string",
                        "maxLength": 40,
                        "example": "RFC / CPF / CURP / DNI",
                        "description": "Optional — required by some methods (OXXO, voucher). The provider's hosted form prompts for it when needed."
                      }
                    }
                  },
                  "userEmail": {
                    "type": "string",
                    "format": "email",
                    "description": "ONE-SHOT MODE only — alias for `customer.email`."
                  },
                  "userName": {
                    "type": "string",
                    "description": "ONE-SHOT MODE only — alias for `customer.fullName`."
                  },
                  "merchantOrderId": {
                    "type": "string",
                    "description": "Your own order id. Echoed back on the response + on webhooks."
                  },
                  "returnUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where the hosted page sends the customer once they finish or cancel."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session created (one-shot mode) OR session record (selector mode — status=awaiting_method).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sessionId": {
                      "type": "string",
                      "example": "cs_91c5474e524b4108b9a29cec7443",
                      "description": "SELECTOR MODE: cs_xxx token. ONE-SHOT MODE: TXN-xxx (the tx id)."
                    },
                    "checkoutUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "SELECTOR MODE: https://api.key2pays.com/checkout/<token>. ONE-SHOT MODE: https://api.key2pays.com/c/<txId>."
                    },
                    "paymentMethodId": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "1008",
                      "description": "Null in selector mode (customer hasn't picked yet). Set in one-shot mode."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "awaiting_method"
                      ],
                      "description": "`awaiting_method` = selector mode, customer hasn't picked yet. `pending` = one-shot mode, tx created waiting for provider."
                    },
                    "amount": {
                      "type": "number"
                    },
                    "currency": {
                      "type": "string",
                      "description": "Selector mode only."
                    },
                    "country": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Echoed selector country filter."
                    },
                    "amountLocal": {
                      "type": "number",
                      "description": "One-shot mode only."
                    },
                    "currencyLocal": {
                      "type": "string",
                      "description": "One-shot mode only."
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/checkout/sessions/{token}": {
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "Fetch a checkout session",
        "description": "Public read-only endpoint to inspect a checkout session. **The token IS the auth** (192 bits of entropy, 24h TTL) — no Bearer required. Used internally by the selector page; integrators can call it from their backend to verify a session is still valid before sending the link to the customer.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "cs_91c5474e524b4108b9a29cec7443"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session info.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "number"
                    },
                    "currency": {
                      "type": "string"
                    },
                    "country": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "customer": {
                      "type": "object",
                      "properties": {
                        "firstName": {
                          "type": "string"
                        },
                        "lastName": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        },
                        "phone": {
                          "type": "string"
                        },
                        "documentId": {
                          "type": "string"
                        }
                      }
                    },
                    "customerIp": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "PR #105 — customer-side IP captured on first hit to /checkout/<token> (or /select). Null until the customer opens the link."
                    },
                    "returnUrl": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "completedTxId": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null when the customer hasn't picked a method yet. Set to the TXN-xxx id once they have — re-visits to the selector URL redirect straight to /c/<txId>."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Session expired (24h TTL) or never existed."
          }
        }
      }
    },
    "/checkout/sessions/{token}/select": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Commit a method choice + create the tx",
        "description": "Called by the selector page (`/checkout/<token>`) when the customer picks a method. We merge any new customer fields into the session, resolve the shop's bearer server-side (the customer never sees the secret key), delegate to POST /payments with `hostedCheckout:true`, and return the `/c/<txId>` URL for the page to redirect to.\n\n**The token IS the auth** — no Bearer required. Integrators rarely call this directly; it's called by the selector page on click.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "cs_91c5474e524b4108b9a29cec7443"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "paymentMethodId"
                ],
                "properties": {
                  "paymentMethodId": {
                    "type": "string",
                    "example": "1008",
                    "description": "4-digit code from GET /payment-methods. The cascade routes to the underlying provider."
                  },
                  "customer": {
                    "type": "object",
                    "description": "Per-method-required fields captured by the selector (e.g. email + name if the integrator didn't pre-fill them).",
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "fullName": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string"
                      },
                      "documentId": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tx created. Redirect the customer to `checkoutUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sessionId": {
                      "type": "string",
                      "example": "cs_91c5474e524b4108b9a29cec7443"
                    },
                    "transactionId": {
                      "type": "string",
                      "example": "TXN-MPMUKN9G-8CA2"
                    },
                    "checkoutUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "https://api.key2pays.com/c/<txId> — page server-side redirects to the provider's hosted form."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "description": "checkout_session_not_found — token expired or never existed."
          },
          "409": {
            "description": "checkout_session_already_completed — the session already created a tx. `details.existingTxId` contains the previous txId so the UI can redirect there instead of treating as a hard error."
          }
        }
      }
    },
    "/me/payout/balance": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Pay-out balance per currency",
        "description": "The merchant's pay-out balances grouped by currency (USD is always present, starts at 0), plus `transferableFromPayinUsd` (how much pay-in USD can be moved in).",
        "responses": {
          "200": {
            "description": "Balances.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "merchantId": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "sandbox",
                        "production"
                      ]
                    },
                    "balances": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "currency": {
                            "type": "string",
                            "example": "USD"
                          },
                          "available": {
                            "type": "number"
                          },
                          "pending": {
                            "type": "number"
                          },
                          "reserved": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "transferableFromPayinUsd": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/me/payout/methods": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Available payout methods",
        "description": "The payout rails you can send through. Each carries `funded` (you have a balance in its currency) + `currencyAvailable`.",
        "responses": {
          "200": {
            "description": "Methods.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "po_mxn_spei"
                          },
                          "name": {
                            "type": "string"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "country": {
                            "type": "string"
                          },
                          "rail": {
                            "type": "string"
                          },
                          "minUsd": {
                            "type": "number"
                          },
                          "maxUsd": {
                            "type": "number"
                          },
                          "logoUrl": {
                            "type": "string",
                            "format": "uri",
                            "description": "Absolute public URL of the rail's brand logo — render it in your own UI. Reflects the operator's latest uploaded icon in real time.",
                            "example": "https://api.key2pay.ai/api/payment-method-logo/payout_spei?v=2026-07-04T00:00:00.000Z"
                          },
                          "funded": {
                            "type": "boolean"
                          },
                          "currencyAvailable": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/me/payout/currencies": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Available payout currencies",
        "description": "The DISTINCT currencies you can pay out in, derived from the configured payout rails (so you don't have to dedupe GET /me/payout/methods). Each carries `funded` (you have a balance in it) + `available` + the `rails` available in that currency. In sandbox, the synthetic `sbx_po_*` test rails' currencies are included too. Sorted by currency (USD first).",
        "responses": {
          "200": {
            "description": "Currencies.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "currencies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "currency": {
                            "type": "string",
                            "example": "MXN"
                          },
                          "country": {
                            "type": "string",
                            "example": "MX"
                          },
                          "funded": {
                            "type": "boolean"
                          },
                          "available": {
                            "type": "number"
                          },
                          "rails": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "example": "po_mxn_spei"
                                },
                                "rail": {
                                  "type": "string",
                                  "example": "spei"
                                },
                                "country": {
                                  "type": "string",
                                  "example": "MX"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/me/payout/transfer": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Transfer USD from pay-in to pay-out balance",
        "description": "Moves USD from your pay-in `available` to your pay-out balance, atomically. The funds can no longer be withdrawn to bank; they live in your pay-out balance for swaps + payouts.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Replay-safe. TTL 24h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amountUsd"
                ],
                "properties": {
                  "amountUsd": {
                    "type": "number",
                    "example": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transferred.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transfer": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "amountUsd": {
                          "type": "number"
                        },
                        "payinAvailableAfter": {
                          "type": "number"
                        },
                        "payoutAvailableUsdAfter": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "422": {
            "description": "balance_insufficient — your pay-in available is below the amount."
          },
          "429": {
            "description": "rate_limited."
          }
        }
      }
    },
    "/me/payout/swap": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Quote a USD→local swap",
        "parameters": [
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "MXN"
          },
          {
            "name": "amountUsd",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            },
            "example": 100
          }
        ],
        "responses": {
          "200": {
            "description": "Quote.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "quote": {
                      "type": "object",
                      "properties": {
                        "fromCurrency": {
                          "type": "string"
                        },
                        "toCurrency": {
                          "type": "string"
                        },
                        "amountUsd": {
                          "type": "number"
                        },
                        "rate": {
                          "type": "number"
                        },
                        "amountReceived": {
                          "type": "number"
                        },
                        "source": {
                          "type": "string"
                        },
                        "fetchedAt": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "422": {
            "description": "unsupported_currency — no live rate for that currency."
          }
        }
      },
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Execute a USD→local swap",
        "description": "Converts USD pay-out balance into a local-currency pay-out balance at the live rate, atomically.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "toCurrency",
                  "amountUsd"
                ],
                "properties": {
                  "toCurrency": {
                    "type": "string",
                    "example": "MXN"
                  },
                  "amountUsd": {
                    "type": "number",
                    "example": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Swapped.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "swap": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "422": {
            "description": "balance_insufficient / unsupported_currency."
          },
          "429": {
            "description": "rate_limited."
          }
        }
      }
    },
    "/me/payout/send": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "List payouts",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "completed",
                "failed"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of payouts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Payout"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      },
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Create a payout",
        "description": "Sends money out of your pay-out balance (in the method's currency) through a payout rail. Validations: the method must exist (`payout_method_unavailable`), you must hold a balance in its currency (`currency_not_funded`), and the amount must not exceed it (`balance_insufficient`).",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Replay-safe — a retried POST never sends twice. TTL 24h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "methodId",
                  "amount"
                ],
                "properties": {
                  "methodId": {
                    "type": "string",
                    "description": "From GET /me/payout/methods.",
                    "example": "po_mxn_spei"
                  },
                  "amount": {
                    "type": "number",
                    "description": "Amount in the method's currency.",
                    "example": 1850
                  },
                  "recipient": {
                    "type": "object",
                    "description": "Destination details (account, name, …).",
                    "example": {
                      "account": "012180012345678901",
                      "name": "Juan Perez"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payout created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payout": {
                      "$ref": "#/components/schemas/Payout"
                    },
                    "methodName": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Display name of the payout rail."
                    },
                    "logoUrl": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri",
                      "description": "Absolute public URL of the payout method brand logo."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "422": {
            "description": "currency_not_funded · payout_method_unavailable · balance_insufficient · amount_invalid."
          },
          "429": {
            "description": "rate_limited."
          }
        }
      }
    },
    "/me/payout/send/{id}": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Retrieve a payout",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payout.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payout": {
                      "$ref": "#/components/schemas/Payout"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "description": "payout_not_found."
          }
        }
      }
    },
    "/me/payout/send/{id}/simulate": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Simulate a payout outcome (sandbox)",
        "description": "Sandbox only — returns an error in production (`simulate_sandbox_only`, 400). Drives a sandbox payout to a terminal status on demand (the disbursement analogue of `POST /payments/{id}/simulate`). For a synthetic test payout (created via an `sbx_po_*` method) it flips the status with no ledger movement; for a real-debited sandbox payout it runs the same money logic as a provider webhook (completed → finalize; failed/rejected/refunded/returned → reverse the debit). Scoped to the merchant — a payout id from another merchant returns `payout_not_found`. This is the completion signal for sandbox payouts: they do NOT auto-settle, and the `Sandbox-Simulate` header is pay-in only (it does not affect payouts).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The payout id (e.g. po-202606-ab12cd34)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "processing",
                      "completed",
                      "paid",
                      "failed",
                      "rejected",
                      "refunded",
                      "returned"
                    ],
                    "description": "Target outcome. `completed`/`paid` → completed; `failed`/`rejected`/`refunded`/`returned` → failed (reverses the debit on a real-debited payout); `processing` → no-op.",
                    "example": "completed"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The payout in its new status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payout": {
                      "$ref": "#/components/schemas/Payout"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "simulate_sandbox_only (called in production) · invalid_request (bad/missing action) · not_sandbox (the payout is not a sandbox payout)."
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "description": "payout_not_found — no such payout under this merchant."
          },
          "422": {
            "description": "invalid_simulate_action — the action is not one a payout can take."
          }
        }
      }
    },
    "/me/payout/fund-sandbox": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Fund the pay-out balance with test money (sandbox)",
        "description": "Sandbox only — returns an error in production (`fund_sandbox_only`, 400). Credits the merchant's pay-out `available` balance in the given currency with test money, so an integrator can create + complete payouts in testing without first running pay-ins, a transfer, and a swap. It only ever writes the payout domain in sandbox — it never touches the production ledger or the pay-in domain. Scoped to the session's merchant (and shop when the session is shop-bound).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "currency",
                  "amount"
                ],
                "properties": {
                  "currency": {
                    "type": "string",
                    "description": "ISO 4217 currency to credit (exactly 3 letters).",
                    "example": "MXN"
                  },
                  "amount": {
                    "type": "number",
                    "description": "Amount of test money to credit, in `currency`. Must be > 0 and ≤ 1,000,000.",
                    "example": 5000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Funded. Returns the credited amount + the updated pay-out balances per currency.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "currency": {
                      "type": "string",
                      "example": "MXN"
                    },
                    "amount": {
                      "type": "number",
                      "example": 5000
                    },
                    "balances": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "currency": {
                            "type": "string",
                            "example": "USD"
                          },
                          "available": {
                            "type": "number"
                          },
                          "pending": {
                            "type": "number"
                          },
                          "reserved": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "fund_sandbox_only (called in production) · invalid_request (bad/missing currency or amount)."
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "422": {
            "description": "invalid_currency · amount_invalid · amount_too_large (over the 1,000,000 cap)."
          }
        }
      }
    },
    "/webhooks": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Register a webhook subscription",
        "description": "Register a URL to receive event notifications. If `url` is omitted we auto-generate a managed-inbox URL on our domain (`https://merchant.key2pays.com/api/webhooks/inbox/<shopSlug>`) — events flow to the dashboard inbox viewer. The `secret` is returned ONCE — store it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTPS endpoint. Omit to use managed inbox.",
                    "example": "https://acme.com/webhooks/key2pay"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "example": [
                      "payment.completed",
                      "payment.failed"
                    ]
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 240
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subscription created.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/WebhookSubscription"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "secret": {
                          "type": "string",
                          "description": "HMAC signing secret. Returned ONLY here, never on subsequent reads.",
                          "example": "whsec_2zP97…"
                        },
                        "managed": {
                          "type": "boolean",
                          "description": "True when the URL was auto-generated for the managed inbox flow."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      },
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhook subscriptions (paginated)",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Subscriptions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookSubscription"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Retrieve a subscription",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscription"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Update URL, events, active, description — secret stays unchanged",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1
                  },
                  "active": {
                    "type": "boolean"
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 240
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscription"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Permanently delete a subscription (cascade deletes deliveries)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/webhooks/{id}/rotate-secret": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Rotate the signing secret with a 24h grace window",
        "description": "Generates a fresh secret and keeps the OLD one valid for 24 hours. During the grace window every delivery carries TWO signatures (`X-Key2Pay-Signature: t=…,v1=<new>,v0=<old>`) so your handler accepts either while you migrate. After expiry only the new secret signs. Both secrets are returned ONCE in this response.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rotation complete.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "secret": {
                      "type": "string",
                      "description": "New signing secret. Returned ONCE."
                    },
                    "previousSecret": {
                      "type": "string",
                      "description": "Old secret, valid for the grace window."
                    },
                    "previousSecretExpiresAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "rotatedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "graceWindowHours": {
                      "type": "integer",
                      "example": 24
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/webhooks/{id}/deliveries": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delivery log for a subscription (paginated)",
        "description": "Every attempt we made for this subscription with status, HTTP code, attempt count, retry schedule. The canonical debug surface for missing events.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "succeeded",
                "failed",
                "dead_letter"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/webhooks/{id}/deliveries/{deliveryId}/replay": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Force-retry a delivery (useful for dead_letter rows after a handler fix)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Re-enqueued. The dispatcher cron picks it up on the next tick (≤5s).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "deliveryId": {
                      "type": "string"
                    },
                    "previousStatus": {
                      "type": "string"
                    },
                    "newStatus": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    },
                    "nextAttemptAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Short-lived JWT minted via `POST /auth/token`. Pass as `Authorization: Bearer <accessToken>`. Expires after 1 hour — refresh with `POST /auth/refresh`."
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Standard error envelope used by every 4xx/5xx response.",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "type",
              "message",
              "requestId"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable identifier (e.g. `invalid_request`, `transaction_not_found`).",
                "example": "invalid_request"
              },
              "type": {
                "type": "string",
                "description": "High-level category. One of `authentication_error`, `invalid_request_error`, `api_error`.",
                "example": "invalid_request_error"
              },
              "message": {
                "type": "string",
                "description": "Human-readable description. Safe to surface in logs; don't surface verbatim to end users.",
                "example": "Request failed schema validation."
              },
              "requestId": {
                "type": "string",
                "description": "Unique id for this request — quote it when contacting support.",
                "example": "req_mp2zb0l3_wrtlzq66"
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Optional structured details (e.g. validation issues array)."
              }
            }
          }
        }
      },
      "Pagination": {
        "type": "object",
        "required": [
          "total",
          "limit",
          "offset",
          "pages"
        ],
        "description": "Offset-based pagination block. Identical shape across every paginated list endpoint.",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Total rows matching the filter, across all pages.",
            "example": 127
          },
          "limit": {
            "type": "integer",
            "description": "Page size echoed back.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "Offset echoed back.",
            "example": 0
          },
          "pages": {
            "type": "integer",
            "description": "ceil(total / limit) — total page count.",
            "example": 3
          }
        }
      },
      "Transaction": {
        "type": "object",
        "description": "Public projection of a payment. Internal fields (crypto destination, upstream provider id, full cascade trail) are intentionally omitted.",
        "required": [
          "id",
          "merchantId",
          "amount",
          "currency",
          "amountLocal",
          "currencyLocal",
          "paymentMethodId",
          "paymentMethod",
          "status",
          "country",
          "fees",
          "settlement",
          "timestamps"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Transaction id (TXN-…).",
            "example": "TXN-MP2WEMT1-KAPL"
          },
          "merchantId": {
            "type": "string",
            "example": "MCH-ON-009"
          },
          "shopId": {
            "type": [
              "string",
              "null"
            ],
            "example": "SHP-MP1STV8W-832A"
          },
          "amount": {
            "type": "number",
            "description": "Amount in USD (major units).",
            "example": 50
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD"
            ],
            "example": "USD"
          },
          "amountLocal": {
            "type": "number",
            "description": "Amount in the customer's local currency (major units).",
            "example": 882.17
          },
          "currencyLocal": {
            "type": "string",
            "description": "ISO-4217 code of the local currency.",
            "example": "MXN"
          },
          "paymentMethodId": {
            "type": [
              "string",
              "null"
            ],
            "description": "OUR 4-digit method id. Same value sent on POST /payments.",
            "example": "1008"
          },
          "paymentMethod": {
            "type": "string",
            "description": "Slug taxonomy (legacy field; prefer paymentMethodId).",
            "example": "spei"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "expired",
              "refunded",
              "chargeback"
            ],
            "description": "See /docs/payment-lifecycle for the full state machine."
          },
          "providerStatus": {
            "type": "string",
            "description": "Raw upstream provider status (debug field).",
            "example": "SUCCESS"
          },
          "country": {
            "type": "string",
            "description": "ISO-3 country code of the payer.",
            "example": "MEX"
          },
          "userEmail": {
            "type": "string",
            "format": "email",
            "example": "test@test.com"
          },
          "userName": {
            "type": "string",
            "example": "Test User"
          },
          "fees": {
            "type": "object",
            "properties": {
              "platform": {
                "type": "number",
                "example": 1.75
              },
              "provider": {
                "type": "number",
                "example": 2.85
              },
              "network": {
                "type": "number",
                "example": 1.5
              },
              "markup": {
                "type": "number",
                "example": 0
              },
              "total": {
                "type": "number",
                "example": 6.1
              }
            }
          },
          "settlement": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "instant",
                  "delayed"
                ],
                "example": "delayed"
              },
              "delay": {
                "type": "string",
                "example": "48h"
              },
              "reserve": {
                "type": "number",
                "example": 5
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "settled",
                  "frozen"
                ],
                "example": "pending"
              }
            }
          },
          "timestamps": {
            "type": "object",
            "properties": {
              "created": {
                "type": "string",
                "format": "date-time",
                "example": "2026-05-12T17:11:51.498Z"
              },
              "completed": {
                "type": "string",
                "format": "date-time"
              },
              "failed": {
                "type": "string",
                "format": "date-time"
              },
              "expired": {
                "type": "string",
                "format": "date-time"
              },
              "refunded": {
                "type": "string",
                "format": "date-time"
              },
              "paymentReceived": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "paymentFormUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Upstream provider's hosted checkout URL (direct-charge flow only).",
            "example": "https://secure-int.key2pay.io/checkout?token=…"
          },
          "checkoutUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Key2Pay-hosted checkout URL (hosted-checkout flow only).",
            "example": "https://sandbox.key2pays.com/c/TXN-…?returnUrl=…"
          },
          "txHash": {
            "type": [
              "string",
              "null"
            ],
            "description": "On-chain settlement hash (set after settlement worker runs)."
          },
          "merchantOrderId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your reference id from POST /payments.",
            "example": "ORD-12345"
          }
        }
      },
      "PaymentMethod": {
        "type": "object",
        "description": "ONE end-user-visible payment rail (Walmart, BBVA, SPEI, OXXO, …). Identified by `paymentMethodId` — same value goes back on POST /payments.",
        "required": [
          "paymentMethodId",
          "method",
          "methodLabel",
          "name",
          "country",
          "countryIso3",
          "channel",
          "iconUrl",
          "currencies",
          "currencyLimits",
          "fee",
          "enabled",
          "online",
          "routable"
        ],
        "properties": {
          "paymentMethodId": {
            "type": "string",
            "description": "4-digit stable id assigned by us.",
            "example": "1008"
          },
          "method": {
            "type": "string",
            "description": "Internal slug taxonomy.",
            "example": "spei"
          },
          "methodLabel": {
            "type": "string",
            "example": "SPEI"
          },
          "name": {
            "type": "string",
            "description": "Catalog name (Walmart, BBVA, SPEI, …).",
            "example": "SPEI"
          },
          "country": {
            "type": "string",
            "description": "ISO-2 country code.",
            "example": "MX"
          },
          "countryIso3": {
            "type": "string",
            "description": "ISO-3 country code.",
            "example": "MEX"
          },
          "channel": {
            "type": "string",
            "enum": [
              "ONLINE",
              "CASH",
              "CREDIT_CARD"
            ],
            "example": "ONLINE"
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Relative (page-origin) icon URL. Back-compat. Prefer `iconUrl` — it's absolute and always resolves."
          },
          "iconUrl": {
            "type": "string",
            "format": "uri",
            "description": "Absolute, public, cacheable URL to the method's icon, hosted by us. Render it directly (`<img src={iconUrl}>`). ALWAYS resolves: a custom uploaded logo when set, otherwise a generic category icon (bank / cash / card) for the channel — never null, never a broken image.",
            "example": "https://api.key2pays.com/api/payment-method-logo/spei__mex?v=2026-06-30T00:00:00.000Z"
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "MXN",
              "USD"
            ]
          },
          "currencyLimits": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "currency": {
                  "type": "string",
                  "example": "MXN"
                },
                "min": {
                  "type": "number",
                  "example": 20
                },
                "max": {
                  "type": "number",
                  "example": 1018099.36
                }
              }
            }
          },
          "minTxUsd": {
            "type": [
              "number",
              "null"
            ]
          },
          "maxTxUsd": {
            "type": [
              "number",
              "null"
            ]
          },
          "fee": {
            "type": "object",
            "properties": {
              "percent": {
                "type": "number",
                "example": 1
              },
              "flat": {
                "type": "number",
                "example": 0
              },
              "currency": {
                "type": "string",
                "example": "MXN"
              }
            }
          },
          "enabled": {
            "type": "boolean",
            "description": "Admin-side enable flag on the cascade row."
          },
          "online": {
            "type": "boolean",
            "description": "Provider instance is currently active."
          },
          "routable": {
            "type": "boolean",
            "description": "True ONLY if a real processor is configured for this exact (method, region, externalId) right now. Use THIS to gate UI, not enabled/online."
          },
          "unroutableReason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when routable=false. Reasons: no_provider_for_method_region | no_active_provider_instance | vertical_not_allowed."
          }
        }
      },
      "Payout": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "po-202606-ab12cd34"
          },
          "methodId": {
            "type": "string",
            "example": "po_mxn_spei"
          },
          "methodName": {
            "type": "string",
            "example": "SPEI"
          },
          "currency": {
            "type": "string",
            "example": "MXN"
          },
          "amount": {
            "type": "number",
            "example": 1850
          },
          "recipient": {
            "type": "object"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ]
          },
          "provider": {
            "type": "string",
            "example": "simulated"
          },
          "providerRef": {
            "type": "string",
            "nullable": true
          },
          "environment": {
            "type": "string",
            "enum": [
              "sandbox",
              "production"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "WebhookSubscription": {
        "type": "object",
        "required": [
          "id",
          "url",
          "events",
          "active",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "wh_3f6c7b143fc87c3e5f6865d3"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://acme.com/webhooks/key2pay"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "payment.completed",
              "payment.refunded"
            ]
          },
          "active": {
            "type": "boolean",
            "example": true
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "example": "Production handler"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "required": [
          "id",
          "event",
          "status",
          "attempts",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "wd_a749dc7a45144c84b39404c8"
          },
          "event": {
            "type": "string",
            "example": "payment.completed"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "succeeded",
              "failed",
              "dead_letter"
            ],
            "example": "succeeded"
          },
          "attempts": {
            "type": "integer",
            "example": 1
          },
          "lastStatusCode": {
            "type": [
              "integer",
              "null"
            ],
            "example": 200
          },
          "lastError": {
            "type": [
              "string",
              "null"
            ]
          },
          "nextAttemptAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "succeededAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "relatedTxId": {
            "type": [
              "string",
              "null"
            ],
            "example": "TXN-MP2WEMT1-KAPL"
          }
        }
      }
    },
    "responses": {
      "AuthError": {
        "description": "401 — token invalid, expired, or missing.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "InvalidRequest": {
        "description": "400 — body validation failed. `error.details.issues` lists the offending fields.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "NotFound": {
        "description": "404 — resource not found, or belongs to a different tenant (we don't leak existence across tenants).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "RateLimited": {
        "description": "429 — rate limit exceeded. `Retry-After` header tells you when to retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds until the bucket refills."
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "integer"
            },
            "description": "Unix epoch when the bucket resets."
          }
        }
      }
    }
  }
}