{
  "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://api.ionea.io/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.ionea.io/docs for the conceptual docs (lifecycle, settlement, webhooks signing, multi-tenant model).",
    "contact": {
      "name": "Key2Pay support",
      "url": "https://docs.ionea.io/docs"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.ionea.io/api/v1",
      "description": "Production"
    },
    {
      "url": "https://api.ionea.io/api/v1",
      "description": "Sandbox — test keys only, no real money movement"
    }
  ],
  "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`. The response gives you ONE of two ways to collect: a DIRECT rail (SPEI, OXXO, cash, PIX) returns the buyer's instructions inline on `paymentData` (CLABE + reference, barcode, QR) with `paymentFormUrl: null`; a HOSTED rail returns `paymentFormUrl` to redirect the buyer to. A charge that could produce neither is rejected 502 `payment_instructions_unavailable` rather than creating an unpayable transaction. 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": "Provider's hosted page (hosted rails only). NULL for a direct rail such as SPEI / OXXO / cash — for those the buyer's data is inline on `paymentData`."
                    },
                    "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 payment instructions. Always carries `method` (the resolved slug). For a DIRECT rail it also carries what the buyer needs to pay — e.g. `clabe` + `reference` + `bankName` + `beneficiaryName` + `dueDate` for SPEI, `barcode` for OXXO/vouchers, `qrCode` for PIX. Render these in your own UI: direct rails have no `paymentFormUrl`. Also returned by GET /payments/{id}. 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-methods": {
      "get": {
        "tags": [
          "Deposit accounts"
        ],
        "summary": "List the deposit methods enabled for your shop",
        "description": "The deposit methods your shop can open CLABEs on. Everything listed here is usable: a method that appears in this list can always be passed as `depositMethodId` on `POST /me/deposit-accounts`.\n\n**You only need this when your shop has more than one.** With a single enabled method — the usual case — omit `depositMethodId` and we use it. With two or more we do NOT choose for you (opening the CLABE on the wrong method sends your customer's money to a destination we do not reconcile, and a CLABE you already handed out cannot be taken back): the create call answers `422 deposit_method_required` and lists these same options in `details.depositMethods`.\n\nAn empty list means deposit accounts are not enabled for your shop yet.\n\nThe shop is taken from your credential — there is no `shopId` parameter.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The enabled deposit methods.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "methods": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DepositMethod"
                      }
                    }
                  }
                }
              }
            }
          },
          "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 the deposit-accounts module (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)."
          },
          "500": {
            "description": "`internal_error`."
          }
        }
      }
    },
    "/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 BENEFICIARY'S FULL NAME — the end user this CLABE is being opened for. Required. You show it next to the CLABE on their deposit screen, it identifies the deposit when the transfer carries no payer name, and it comes back on every read as `beneficiary.name` (on the account) and `beneficiaryName` (on each deposit).",
                    "example": "Juan Pérez"
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "THE BENEFICIARY'S EMAIL. REQUIRED TO OPEN AN ACCOUNT. It comes back on every read as `beneficiary.email` (account) and `beneficiaryEmail` (deposit). 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."
                  },
                  "depositMethodId": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "WHICH deposit method opens this account — a `depositMethodId` from `GET /me/deposit-methods`. Optional while your shop has ONE enabled method (the usual case): omit it and we use it. Required once it has two or more — we never choose for you, because a CLABE opened on the wrong method cannot be taken back. Sending one that is not enabled for your shop returns 422 `deposit_method_unavailable`; we never fall back to another.",
                    "example": "pm_7f31a0c4"
                  }
                }
              }
            }
          }
        },
        "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": "None of these is transient — retrying the same request does not fix any of them.\n\n`deposit_accounts_unavailable` — deposit accounts are not enabled for this shop.\n\n`deposit_method_required` — your shop has MORE THAN ONE deposit method enabled and you did not say which to use. `details.depositMethods` lists them (same shape as `GET /me/deposit-methods`): resend with `depositMethodId`.\n\n`deposit_method_unavailable` — the `depositMethodId` you sent is not one of your shop's enabled methods. We do not fall back to another one; `details.depositMethods` lists the valid ones."
          },
          "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."
          },
          "502": {
            "description": "`deposit_account_provisioning_failed` — the rail answered but did not return a usable account, so nothing was created. Retry ONCE; if it persists it is a configuration problem on your shop's deposit method and retrying will not fix it."
          },
          "503": {
            "description": "`service_unavailable` — the account could not be opened upstream right now (a genuine transient: network/timeout/5xx). 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": "Provider's hosted page (hosted rails only). NULL for a direct rail — see `paymentData`.",
            "example": "https://secure-int.key2pay.io/checkout?token=…"
          },
          "paymentData": {
            "type": "object",
            "description": "Method-specific payment instructions. Always carries `method`; for a DIRECT rail it also carries what the buyer needs to pay (SPEI: `clabe` + `reference` + `bankName` + `beneficiaryName` + `dueDate`; OXXO/vouchers: `barcode`; PIX: `qrCode`).",
            "example": {
              "method": "spei",
              "clabe": "703428043000024977",
              "reference": "1303628",
              "bankName": "tesored",
              "beneficiaryName": "CLB Payment",
              "dueDate": "2026-08-04T09:13:20.381Z"
            }
          },
          "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"
          },
          "beneficiary": {
            "type": "object",
            "description": "**Who this CLABE belongs to** — the end user you opened it for, as you declared them on create. This is the account holder you show next to the CLABE on your customer's deposit screen.\n\nDo not confuse it with the PAYER of an individual deposit (`payerName`): the beneficiary owns the account, the payer is whoever sent one particular transfer. They are usually the same person, and when they are not, that difference is what you want to see.\n\nSame person as `customer` below — `beneficiary` is the short form (who it is), `customer` carries their full record (phone, document, country).",
            "properties": {
              "name": {
                "type": "string",
                "description": "The beneficiary's full name — what you sent as `name`. Always present: it is required to open an account.",
                "example": "Juan Pérez"
              },
              "email": {
                "type": "string",
                "nullable": true,
                "description": "The beneficiary's email — what you sent as `email`. Required to open an account since 2026-07-31, so it is `null` only on accounts opened before that.",
                "example": "juan@example.com"
              }
            }
          },
          "customer": {
            "type": "object",
            "description": "The full record of the same person as `beneficiary` — 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."
          },
          "beneficiaryName": {
            "type": "string",
            "description": "**Who this CLABE belongs to** — the end user you opened the account for, from YOUR own declaration (not from the rail). Always present.",
            "example": "Juan Pérez"
          },
          "beneficiaryEmail": {
            "type": "string",
            "nullable": true,
            "description": "The beneficiary's email, from your declaration. `null` only on accounts opened before it became required.",
            "example": "juan@example.com"
          },
          "payerName": {
            "type": "string",
            "nullable": true,
            "description": "Who SENT this transfer — the sending account holder's name, exactly as their bank reported it. Compare it against `beneficiaryName` to confirm the deposit came from the person you expected: a mismatch means a third party funded your customer's account.",
            "example": "JUAN PEREZ GARCIA"
          },
          "payerBank": {
            "type": "string",
            "nullable": true,
            "description": "The SENDER's institution, as the rail reported it.",
            "example": "BBVA MEXICO"
          },
          "payerDocument": {
            "type": "string",
            "nullable": true,
            "description": "The sender's tax id, when the rail reports one (in Mexico, their RFC). `null` when the rail did not provide it — not every institution does.",
            "example": "PEGJ850315H2A"
          },
          "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"
          }
        }
      },
      "DepositMethod": {
        "type": "object",
        "properties": {
          "depositMethodId": {
            "type": "string",
            "description": "Send this back as `depositMethodId` on `POST /me/deposit-accounts`.",
            "example": "pm_7f31a0c4"
          },
          "method": {
            "type": "string",
            "description": "The rail slug.",
            "example": "spei"
          },
          "name": {
            "type": "string",
            "description": "Merchant-facing name — the same label the credited transaction will carry. When two methods share a rail, this is what tells them apart.",
            "example": "SPEI — Depósito"
          },
          "country": {
            "type": "string",
            "example": "MEX"
          },
          "currency": {
            "type": "string",
            "example": "MXN"
          }
        }
      },
      "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."
          }
        }
      }
    }
  }
}