{
  "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 USD by default — and you SETTLE in USD.** The `amount` you send to `POST /payments` and `POST /checkout/sessions` is in USD major units unless you pass a `currency`. Optionally you may initiate in a **presentment currency** (`currency: \"EUR\"`, `\"MXN\"`, …): we convert `amount` (denominated in that currency) to a USD headline at our rate MINUS the FX markup, and you settle that USD. The response echoes `currency` + `inputAmount` alongside the resulting USD `amount`. Either way, Key2Pay then converts USD → the buyer's local currency automatically (per country + method) and returns `amountLocal` + `currencyLocal`.\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": "Deposit accounts",
      "description": "Fixed CLABE per end user: get-or-create one, list them, and read every deposit received on one. Every transfer in is credited to your balance automatically."
    },
    {
      "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": "Returns ONLY the methods the shop can actually charge right now — every entry is chargeable via POST /payments (a method that would fail `not_enabled_for_shop` / `cascade_exhausted` is never listed). 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, and an `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). `totalAvailable` reports how many rails are configured for the shop; `routableCount` how many are chargeable (== `count` by default). Pass `include_unroutable=1` to also receive the non-chargeable rails with their `routable` flag + `unroutableReason` for diagnostics. 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)."
          },
          {
            "name": "include_unroutable",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "Diagnostics: also return non-chargeable rails (with routable=false + unroutableReason). Default omits them."
          }
        ],
        "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. **`amount` is in USD by default, and you SETTLE in USD.** Optionally initiate in a **presentment currency** by passing `currency` (e.g. `\"EUR\"`, `\"MXN\"`): we convert `amount` (denominated in that currency) to a USD headline at our rate MINUS the FX markup. **The response returns your presentment values as `inputCurrency` + `inputAmount`, and `currency` is ALWAYS `\"USD\"` (the settlement currency) — the converted USD figure is `amount`.** If we have no FX rate for the currency, the charge is rejected 422 `fx_unavailable` (we never guess a rate). Either way we then convert USD → the buyer's local currency automatically and return `amountLocal` + `currencyLocal`. Response includes `paymentFormUrl` — the upstream provider's hosted checkout URL. 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": "Charge amount, in `currency` (major units). USD by default (e.g. 50 = $50.00 USD). With a non-USD `currency` (presentment) this is the amount in that currency; we convert it to a USD headline (≤100,000 USD) at our rate minus the FX markup. >0.",
                    "example": 50
                  },
                  "currency": {
                    "type": "string",
                    "description": "OPTIONAL presentment currency (ISO-4217, 3 letters, default \"USD\"). Pass e.g. \"EUR\"/\"GBP\"/\"MXN\" to price in that currency — we convert `amount` to USD (our rate minus the FX markup) and you settle in USD. In the response your value comes back as `inputCurrency` (NOT `currency`, which is always \"USD\") and `amount` becomes 100 in that currency. No FX rate for the currency → 422 `fx_unavailable`.",
                    "example": "EUR"
                  },
                  "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"
                  },
                  "tag1": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "OPTIONAL free-form tag (≤200 chars). Stash any info of your interest — it's stored on the transaction and echoed back verbatim on this response and on GET /payments. Pass-through only: never affects routing, pricing, or the provider. Same in sandbox + production.",
                    "example": "campaign:black-friday"
                  },
                  "tag2": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "OPTIONAL free-form tag (≤200 chars). See tag1.",
                    "example": "channel:mobile-app"
                  },
                  "tag3": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "OPTIONAL free-form tag (≤200 chars). See tag1.",
                    "example": "note:vip-customer"
                  },
                  "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",
                      "description": "The USD headline you settle in. With a presentment `currency`, this is the CONVERTED USD (your `inputAmount` at our rate minus markup).",
                      "example": 114.45
                    },
                    "currency": {
                      "type": "string",
                      "description": "ALWAYS `\"USD\"` — the settlement currency. (Your presentment currency comes back as `inputCurrency`.)",
                      "example": "USD"
                    },
                    "inputCurrency": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The presentment currency you sent in `currency` (e.g. `\"EUR\"`). `null` for a USD-initiated charge.",
                      "example": "EUR"
                    },
                    "inputAmount": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The amount you sent, in `inputCurrency` (e.g. 100 EUR). `null` for a USD-initiated charge.",
                      "example": 100
                    },
                    "amountLocal": {
                      "type": "number",
                      "description": "The buyer's local equivalent (converted from `amount` USD).",
                      "example": 1994.16
                    },
                    "currencyLocal": {
                      "type": "string",
                      "example": "MXN"
                    },
                    "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."
                    },
                    "tag1": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Echoed back from the input (null when not sent)."
                    },
                    "tag2": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Echoed back from the input (null when not sent)."
                    },
                    "tag3": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Echoed back from the input (null when not sent)."
                    },
                    "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": "One of: `cascade_exhausted` (no provider could take the charge for this method/country); `missing_required_fields` — the rail needs a buyer field that's missing or malformed (in this case `error.details.missingFields` / `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); or `fx_unavailable` — you sent a presentment `currency` we have no FX rate for (`error.details.currency` names it). We never guess a rate.",
            "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": "Presentment currency (ISO-4217, default \"USD\"). USD → you settle in USD, no conversion. A non-USD value (e.g. \"EUR\") is converted to a USD headline at our rate minus the FX markup; you still settle in USD. The buyer-side local conversion is then 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`, and `recipientFields` — the EXACT beneficiary fields to send in `recipient` for THIS method (self-documenting, so you never guess). In sandbox the synthetic `sbx_po_*` test rails are prepended.",
        "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"
                          },
                          "sandbox": {
                            "type": "boolean",
                            "description": "true for the synthetic sbx_po_* test rails (sandbox only)."
                          },
                          "recipientFields": {
                            "type": "array",
                            "description": "The beneficiary fields to send in `recipient` for this method — read them to build your payout request correctly (labels, required, type, and `options` for bank selectors like Colombia/Chile/Peru where the value is the exact Pagsmile bank_code).",
                            "items": {
                              "type": "object",
                              "properties": {
                                "key": {
                                  "type": "string",
                                  "example": "account",
                                  "description": "The recipient.<key> to send (name, documentId, account, bankCode, accountType, phone, email)."
                                },
                                "label": {
                                  "type": "string",
                                  "example": "CCI (Código de Cuenta Interbancario)"
                                },
                                "type": {
                                  "type": "string",
                                  "enum": [
                                    "text",
                                    "tel",
                                    "select"
                                  ]
                                },
                                "required": {
                                  "type": "boolean"
                                },
                                "placeholder": {
                                  "type": "string"
                                },
                                "help": {
                                  "type": "string"
                                },
                                "options": {
                                  "type": "array",
                                  "description": "Present for `select` fields (e.g. the bank list). value = the exact code to send.",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "value": {
                                        "type": "string",
                                        "example": "0001"
                                      },
                                      "label": {
                                        "type": "string",
                                        "example": "Banco Continental (BBVA)"
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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. Read `recipientFields` from GET /me/payout/methods to know EXACTLY which `recipient.*` fields this method needs (CLABE 18 for SPEI, CCI 20 for Peru bank, PIX key for PIX, bank_code from the selector for Colombia/Chile/Peru, email for PayPal, …). A malformed field is caught before the provider is called and the debit is reversed — you're never left short. Same behaviour in sandbox and production; the only difference is that sandbox uses the sbx_po_* / fund-sandbox / simulate flow.",
        "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": "Beneficiary details — send the fields listed in that method's `recipientFields`. Common keys: name, documentId, account (CLABE/CCI/PIX key/account no.), bankCode (from the selector or auto-derived for SPEI/Argentina), accountType, phone, email.",
                    "example": {
                      "name": "Juan Perez",
                      "documentId": "PEPJ800101HDF",
                      "account": "012180012345678901",
                      "bankCode": "012"
                    }
                  }
                }
              }
            }
          }
        },
        "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": "The payout was NOT created. `payout_dispatch_failed` — the provider rejected the beneficiary data; `details.failureReason` carries the reason and the debit was already reversed (correct the recipient and retry with the same Idempotency-Key). Also: `currency_not_funded` · `payout_method_unavailable` · `balance_insufficient` (details.availableUsd/shortfallUsd) · `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)."
          }
        }
      }
    },
    "/me/deposit-accounts": {
      "post": {
        "tags": [
          "Deposit accounts"
        ],
        "summary": "Get or create a deposit account (CLABE) for one end user",
        "description": "GET-OR-CREATE, keyed on `endUserRef`. If that end user ALREADY has a deposit account on this shop, the existing one is returned with `created: false` and **HTTP 200 — no account is opened**. Otherwise a new CLABE is minted and returned with `created: true` and **HTTP 201**.\n\nSafe to call every time you render your customer's deposit screen: you never have to track whether you asked before, and a repeated call can never leave one end user with two CLABEs (concurrent calls collapse into a single account).\n\nThe shop is taken from your credential — there is no `shopId` field. A CLABE created by one shop is invisible to your other shops.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "endUserRef",
                  "name",
                  "email"
                ],
                "properties": {
                  "endUserRef": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "YOUR identifier for the end user (their id in your system). Required — it is the idempotency key of this endpoint.",
                    "example": "user_12345"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "The end user's full name. Required — you show it next to the CLABE, and it identifies the deposit when the transfer carries no payer name.",
                    "example": "Juan Pérez"
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "REQUIRED TO OPEN AN ACCOUNT. It identifies the end customer everywhere the CLABE appears and becomes the `userEmail` of every transaction its deposits credit. Omitting it returns 400 `invalid_request` with `details.required: [\"email\"]`. NOTE: the requirement applies to CREATION only — a call for an `endUserRef` that already has an account returns it (200) whether or not you send an email, so existing integrations keep reading accounts opened before this rule.",
                    "example": "juan@example.com"
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "CONDITIONAL — some shops' deposit accounts cannot be opened without a phone as well; when yours is one of them, omitting it returns 400 with `details.required` naming it.",
                    "example": "+525512345678"
                  },
                  "documentType": {
                    "type": "string",
                    "maxLength": 20,
                    "description": "Optional — e.g. RFC, CURP, DNI.",
                    "example": "CURP"
                  },
                  "documentId": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "Optional — the document value."
                  },
                  "country": {
                    "type": "string",
                    "maxLength": 3,
                    "description": "Optional — the end user's country (ISO-2/3).",
                    "example": "MEX"
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 500
                    },
                    "description": "Optional — up to 20 string key/values echoed back on every read."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The end user already had an account — returned unchanged, and the provider was NOT contacted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "created": {
                      "type": "boolean",
                      "example": false
                    },
                    "account": {
                      "$ref": "#/components/schemas/DepositAccount"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "A new deposit account was opened.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "created": {
                      "type": "boolean",
                      "example": true
                    },
                    "account": {
                      "$ref": "#/components/schemas/DepositAccount"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — the body failed validation (`details.issues`), the end user's `email` was missing on a CREATE (`details.required: [\"email\"]`), a further contact field your shop's deposit accounts require is missing (`details.required`, e.g. `[\"email\",\"phone\"]`), or the credential is not shop-scoped."
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "403": {
            "description": "`permission_denied` — the dashboard user's role lacks `deposits.create_account` (a raw API key is never gated by roles) · `environment_mismatch` — the credential's environment does not match the shop's (this one DOES apply to API keys: it is what a `sk_test_` gets once the shop is promoted to production)."
          },
          "409": {
            "description": "`deposit_account_conflict` — more than one account already exists for that `endUserRef` on this shop, so we will not pick one. No account was created."
          },
          "422": {
            "description": "`deposit_accounts_unavailable` — deposit accounts are not enabled for this shop. Not transient: retrying does not fix it."
          },
          "429": {
            "description": "`rate_limited`."
          },
          "500": {
            "description": "`internal_error`. Safe to retry: a repeated create for the same `endUserRef` can only ever return the existing account."
          },
          "503": {
            "description": "`service_unavailable` — the account could not be opened upstream right now. Safe to retry: a retry never opens a second CLABE for the same `endUserRef`."
          }
        }
      },
      "get": {
        "tags": [
          "Deposit accounts"
        ],
        "summary": "List your deposit accounts",
        "description": "Every deposit account of the shop the credential belongs to, newest first. Filter by `endUserRef` to look up one customer's CLABE.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "endUserRef",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Return only the account of that end user."
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "stats"
              ]
            },
            "description": "Pass `stats` to get each account's rolled-up activity (how much it received, how many deposits, the last one). Opt-in: it costs one extra aggregate query, so a lookup that only needs the CLABE does not pay for it."
          }
        ],
        "responses": {
          "200": {
            "description": "Accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accounts": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/DepositAccount"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "stats": {
                                "$ref": "#/components/schemas/DepositAccountStats"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — the credential is not shop-scoped."
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "403": {
            "description": "`permission_denied` — the dashboard user's role lacks `mod.deposit_accounts` (a raw API key is never gated by roles) · `environment_mismatch` — the credential's environment does not match the shop's (this one DOES apply to API keys)."
          },
          "500": {
            "description": "`internal_error`."
          }
        }
      }
    },
    "/me/deposit-accounts/{id}/deposits": {
      "get": {
        "tags": [
          "Deposit accounts"
        ],
        "summary": "List the deposits received on one CLABE",
        "description": "The transactional report of one deposit account: every transfer received on it, newest first, with the amount the end user sent, its USD equivalent, the fee charged and the net credited — plus the rolled-up totals.\n\nThe USD amount and the fee are the ones SNAPSHOTTED when each deposit was credited. They are never re-derived from today's FX rate or today's pricing, so this report does not change under you.\n\nScoped to your shop: an id that belongs to another shop returns the same 404 as an id that does not exist.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "eda_9f2c1b7ad0e34f5a8c6b2d10"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The account, its totals, and its deposits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/DepositAccount"
                    },
                    "stats": {
                      "$ref": "#/components/schemas/DepositAccountStats"
                    },
                    "deposits": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Deposit"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — the credential is not shop-scoped."
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "403": {
            "description": "`permission_denied` — the dashboard user's role lacks `mod.deposit_accounts` (a raw API key is never gated by roles) · `environment_mismatch` — the credential's environment does not match the shop's (this one DOES apply to API keys)."
          },
          "404": {
            "description": "`deposit_account_not_found`."
          },
          "500": {
            "description": "`internal_error`."
          }
        }
      }
    },
    "/me/deposit-accounts/{id}": {
      "get": {
        "tags": [
          "Deposit accounts"
        ],
        "summary": "Retrieve one deposit account",
        "description": "Scoped to your shop. An id that belongs to another shop returns the same 404 as an id that does not exist.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "eda_9f2c1b7ad0e34f5a8c6b2d10"
          }
        ],
        "responses": {
          "200": {
            "description": "Account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/DepositAccount"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — the credential is not shop-scoped."
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "403": {
            "description": "`permission_denied` — the dashboard user's role lacks `mod.deposit_accounts` (a raw API key is never gated by roles) · `environment_mismatch` — the credential's environment does not match the shop's (this one DOES apply to API keys)."
          },
          "404": {
            "description": "`deposit_account_not_found`."
          },
          "500": {
            "description": "`internal_error`."
          }
        }
      }
    },
    "/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) — the settlement figure. With a presentment charge, this is the converted USD.",
            "example": 50
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD"
            ],
            "description": "Always \"USD\" — the settlement currency. The presentment currency is `inputCurrency`.",
            "example": "USD"
          },
          "inputCurrency": {
            "type": [
              "string",
              "null"
            ],
            "description": "Presentment currency the charge was initiated in (ISO-4217), or null for a USD charge.",
            "example": "EUR"
          },
          "inputAmount": {
            "type": [
              "number",
              "null"
            ],
            "description": "Amount in `inputCurrency` the charge was initiated with, or null for a USD charge.",
            "example": 100
          },
          "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. On a deposit-account credit there is no order of yours to echo, so we generate a stable `DEP-…` reference instead — use it to deduplicate, not to identify the customer (that is `endUserRef`).",
            "example": "ORD-12345"
          },
          "endUserRef": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR identifier for the end user, when the payment credited a deposit account you opened with it. `null` on every other payment.",
            "example": "user_12345"
          },
          "tag1": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-form tag you sent on POST /payments (null when not set)."
          },
          "tag2": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-form tag you sent on POST /payments (null when not set)."
          },
          "tag3": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-form tag you sent on POST /payments (null when not set)."
          }
        }
      },
      "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
          }
        }
      },
      "DepositAccount": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "eda_9f2c1b7ad0e34f5a8c6b2d10"
          },
          "endUserRef": {
            "type": "string",
            "nullable": true,
            "description": "YOUR id for this end user — the one you sent on create. It is the get-or-create key.",
            "example": "user_12345"
          },
          "clabe": {
            "type": "string",
            "description": "The 18-digit CLABE the end user transfers to. Fixed: it does not change between deposits.",
            "example": "646180111812345678"
          },
          "bank": {
            "type": "string",
            "nullable": true,
            "description": "Receiving institution — present ONLY when the rail reports it. `null` means we were not told; do NOT infer it from the CLABE prefix."
          },
          "currency": {
            "type": "string",
            "example": "MXN"
          },
          "country": {
            "type": "string",
            "example": "MEX"
          },
          "methodName": {
            "type": "string",
            "description": "Merchant-facing name of the rail — the same label the credited transaction will carry.",
            "example": "SPEI — Depósito"
          },
          "customer": {
            "type": "object",
            "description": "The end user this account belongs to, exactly as you declared them.",
            "properties": {
              "name": {
                "type": "string",
                "example": "Juan Pérez"
              },
              "email": {
                "type": "string",
                "nullable": true,
                "example": "juan@example.com"
              },
              "phone": {
                "type": "string",
                "nullable": true,
                "example": "+525512345678"
              },
              "documentType": {
                "type": "string",
                "nullable": true,
                "example": "CURP"
              },
              "documentId": {
                "type": "string",
                "nullable": true,
                "example": "PEJJ850101HDFRRN08"
              },
              "country": {
                "type": "string",
                "nullable": true,
                "example": "MEX"
              }
            }
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Your own key/value passthrough (max 20 keys, values ≤ 500 chars)."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ],
            "description": "`disabled` is set by support, never through the API. It means: stop showing this CLABE to your end user and open a ticket. Get-or-create keeps returning the same disabled account (no replacement is minted behind your back) and an incoming transfer is not bounced."
          },
          "environment": {
            "type": "string",
            "enum": [
              "sandbox",
              "production"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DepositAccountStats": {
        "type": "object",
        "properties": {
          "deposits": {
            "type": "integer",
            "description": "Every inbound transfer recorded on this CLABE, credited or not.",
            "example": 2
          },
          "credited": {
            "type": "integer",
            "description": "Those that were credited to your balance (i.e. produced a transaction).",
            "example": 2
          },
          "totalUsd": {
            "type": "number",
            "description": "Sum of the credited deposits in USD, at the rate of each deposit's own day.",
            "example": 205.5
          },
          "localTotals": {
            "type": "array",
            "description": "Credited totals in the currency they arrived in. A list (not a single number) because totals are never summed across currencies.",
            "items": {
              "type": "object",
              "properties": {
                "currency": {
                  "type": "string",
                  "example": "MXN"
                },
                "amount": {
                  "type": "number",
                  "example": 3500
                }
              }
            }
          },
          "lastDepositAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "Deposit": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "edp_4b81f0c2a9d7e6135f80ab24"
          },
          "transactionId": {
            "type": "string",
            "nullable": true,
            "description": "The pay-in transaction this deposit created — look it up with `GET /payments/{id}`. `null` while the deposit has not been credited.",
            "example": "TXN-DEP-9F2C1B7AD0E3"
          },
          "amount": {
            "type": "number",
            "description": "What the end user transferred, in `currency`.",
            "example": 2000
          },
          "currency": {
            "type": "string",
            "example": "MXN"
          },
          "amountUsd": {
            "type": "number",
            "description": "Converted when the deposit was credited, at that day's rate. It is NOT re-converted later.",
            "example": 117.65
          },
          "feeUsd": {
            "type": "number",
            "nullable": true,
            "description": "The fee charged on this deposit, snapshotted when it was credited. `null` when the deposit was never credited — which is not the same as a zero fee."
          },
          "netUsd": {
            "type": "number",
            "nullable": true,
            "description": "`amountUsd` minus the fee: what reached your balance."
          },
          "status": {
            "type": "string",
            "enum": [
              "credited",
              "pending",
              "rejected"
            ],
            "description": "`credited` = it is in your balance. `pending` = it arrived and is still being confirmed; it credits automatically. `rejected` = it will not be credited."
          },
          "payerName": {
            "type": "string",
            "nullable": true,
            "description": "Who sent the transfer, as their bank reported it."
          },
          "payerBank": {
            "type": "string",
            "nullable": true,
            "description": "The SENDER's institution."
          },
          "payerDocument": {
            "type": "string",
            "nullable": true
          },
          "trackingKey": {
            "type": "string",
            "nullable": true,
            "description": "The rail's tracking key (SPEI: clave de rastreo) — what appears on the payer's bank statement."
          },
          "reference": {
            "type": "string",
            "nullable": true
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "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."
          }
        }
      }
    }
  }
}