{
  "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- Money is in major units (e.g. `50.00` = $50.00 USD). The refund endpoint takes amounts in the original transaction's local currency, also major units.\n- Paginated lists use offset envelope: `?limit=&offset=` → `{ data, pagination: { total, limit, offset, pages } }`.\n- All authenticated endpoints take `Authorization: Bearer <accessToken>` (minted via POST /auth/token).\n\n**Sandbox:** swap base URL to `https://sandbox.key2pays.com/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.key2pays.com/docs for the conceptual docs (lifecycle, settlement, webhooks signing, multi-tenant model).",
    "contact": {
      "name": "Key2Pay support",
      "url": "https://docs.key2pays.com/docs"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.key2pays.com/api/v1",
      "description": "Production"
    },
    {
      "url": "https://sandbox.key2pays.com/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": "Create, retrieve, list, refund, hosted checkout."
    },
    {
      "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.",
                      "example": 900
                    }
                  }
                }
              }
            }
          },
          "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",
                      "example": 900
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/ping": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Credential + connectivity check",
        "description": "200 confirms your key works, what environment you're on, and which API version you're pinning to. No side effects, cheap to call.",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "sandbox",
                        "production"
                      ],
                      "example": "sandbox"
                    },
                    "keyKind": {
                      "type": "string",
                      "enum": [
                        "secret",
                        "publishable"
                      ],
                      "example": "secret"
                    },
                    "keyId": {
                      "type": "string",
                      "example": "sk_test_sh…7u10"
                    },
                    "apiVersion": {
                      "type": "string",
                      "example": "2026-05-01"
                    },
                    "merchant": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "MCH-ON-009"
                        },
                        "name": {
                          "type": "string",
                          "example": "Golden Dragon"
                        }
                      }
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/me": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Resolve the merchant identity behind the credential",
        "description": "Returns the merchant id, name, industry, tier, trustScore, capabilities flags, and active environment. Useful as a sanity check after auth.",
        "responses": {
          "200": {
            "description": "Identity payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "example": "MCH-ON-009"
                    },
                    "name": {
                      "type": "string",
                      "example": "Golden Dragon"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "industry": {
                      "type": "string",
                      "example": "money_services_business"
                    },
                    "tier": {
                      "type": "string",
                      "enum": [
                        "starter",
                        "standard",
                        "premium"
                      ],
                      "example": "starter"
                    },
                    "trustScore": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 1000,
                      "example": 400
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "sandbox",
                        "production"
                      ]
                    },
                    "capabilities": {
                      "type": "object",
                      "properties": {
                        "checkout": {
                          "type": "boolean"
                        },
                        "directCharge": {
                          "type": "boolean"
                        },
                        "refunds": {
                          "type": "boolean"
                        },
                        "webhooks": {
                          "type": "boolean"
                        },
                        "crypto": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "methods": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/me/balance": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Get the merchant's current balance + fee summary + reserve schedule",
        "description": "Returns four balance buckets (available, pending, frozen, reserve) all in USD major units, plus a 90-day fee summary and the next 3 upcoming reserve releases. See /docs/settlement-flow for the lifecycle.",
        "responses": {
          "200": {
            "description": "Balance + summary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "merchantId": {
                      "type": "string"
                    },
                    "currency": {
                      "type": "string",
                      "enum": [
                        "USD"
                      ]
                    },
                    "balance": {
                      "type": "object",
                      "properties": {
                        "available": {
                          "type": "number",
                          "example": 12480.55
                        },
                        "pending": {
                          "type": "number",
                          "example": 230.1
                        },
                        "frozen": {
                          "type": "number",
                          "example": 0
                        },
                        "reserve": {
                          "type": "number",
                          "example": 1500
                        }
                      }
                    },
                    "tier": {
                      "type": "string",
                      "enum": [
                        "starter",
                        "standard",
                        "premium"
                      ]
                    },
                    "trustScore": {
                      "type": "integer"
                    },
                    "rollingReservePct": {
                      "type": "number",
                      "example": 5
                    },
                    "feeSummary": {
                      "type": "object",
                      "properties": {
                        "grossVolume": {
                          "type": "number"
                        },
                        "platformFees": {
                          "type": "number"
                        },
                        "processingFees": {
                          "type": "number"
                        },
                        "networkFees": {
                          "type": "number"
                        },
                        "chargebackFees": {
                          "type": "number"
                        },
                        "totalFees": {
                          "type": "number"
                        },
                        "netBalance": {
                          "type": "number"
                        }
                      }
                    },
                    "movements": {
                      "type": "array",
                      "description": "Last 100 balance movements (capture, fee, freeze, reserve, etc.).",
                      "items": {
                        "type": "object"
                      }
                    },
                    "reserveSchedule": {
                      "type": "array",
                      "description": "Next 3 upcoming reserve releases.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "amount": {
                            "type": "number"
                          },
                          "releaseDate": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "fromDate": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/payment-methods": {
      "get": {
        "tags": [
          "Payment methods"
        ],
        "summary": "List the methods the authenticated shop can accept",
        "description": "One row per end-user-visible payment rail (Walmart, BBVA, SPEI, OXXO, …). Each entry has a stable 4-digit `paymentMethodId` that you store and send back on POST /payments. NOT paginated — this is a small per-country catalog. Cache it.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ISO-2 or ISO-3 — restrict to one country.",
            "example": "MEX"
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "ONLINE",
                "CASH",
                "CREDIT_CARD"
              ]
            }
          },
          {
            "name": "method",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Internal slug filter (e.g. spei, oxxo, voucher)."
          }
        ],
        "responses": {
          "200": {
            "description": "Catalog response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "shop": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "sandbox",
                        "production"
                      ]
                    },
                    "filters": {
                      "type": "object"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "totalAvailable": {
                      "type": "integer"
                    },
                    "routableCount": {
                      "type": "integer"
                    },
                    "methods": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PaymentMethod"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/payments": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create a payment (direct-charge flow)",
        "description": "Creates a transaction. Response includes `paymentFormUrl` — the upstream provider's hosted checkout URL — for direct-charge integrations that build their own UI. For a hosted-checkout flow where we render the per-method UI, use POST /checkout/sessions instead. The `paymentMethodId` is the 4-digit id from GET /payment-methods.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Replay-safe retry. Same key + same body returns the original response. Different body returns 409 idempotency_conflict. TTL 24h."
          },
          {
            "name": "Sandbox-Simulate",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "paid",
                "failed",
                "expired",
                "chargeback",
                "slow_payment"
              ]
            },
            "description": "Sandbox-only. Schedules a deterministic state transition. See /docs/sandbox-simulate."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount"
                ],
                "properties": {
                  "amount": {
                    "type": "number",
                    "description": "USD major units. >0, ≤100,000.",
                    "example": 50
                  },
                  "paymentMethodId": {
                    "type": "string",
                    "description": "4-digit id from GET /payment-methods. Recommended.",
                    "example": "1008"
                  },
                  "paymentMethod": {
                    "type": "string",
                    "description": "LEGACY. Slug taxonomy (spei, oxxo, voucher, …). Use paymentMethodId in new code."
                  },
                  "country": {
                    "type": "string",
                    "description": "ISO-2 or ISO-3 — normalized to ISO-3 internally.",
                    "example": "MEX"
                  },
                  "userEmail": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "userName": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "merchantOrderId": {
                    "type": "string",
                    "description": "Your own reference id (≤120 chars). Queryable via ?merchantOrderId=… on GET /payments.",
                    "example": "ORD-12345"
                  },
                  "hostedCheckout": {
                    "type": "boolean",
                    "description": "When true, include `checkoutUrl` in the response and omit `paymentFormUrl`. Equivalent to calling POST /checkout/sessions."
                  },
                  "returnUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where the hosted checkout sends the customer after completion. Only used with hostedCheckout=true."
                  },
                  "merchantId": {
                    "type": "string",
                    "description": "Only required for multi-merchant tokens; defaults to the auth-derived merchant."
                  },
                  "shopId": {
                    "type": "string",
                    "description": "Only required for multi-shop tokens; defaults to the auth-derived shop."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transaction created. Status is `pending` for async methods (SPEI, OXXO, voucher); `completed` for sync methods (card approved immediately).",
            "headers": {
              "Idempotent-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Set when this response is a replay of a previous request with the same Idempotency-Key."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transactionId": {
                      "type": "string",
                      "example": "TXN-MP2WEMT1-KAPL"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "completed"
                      ]
                    },
                    "amount": {
                      "type": "number"
                    },
                    "amountLocal": {
                      "type": "number"
                    },
                    "currencyLocal": {
                      "type": "string"
                    },
                    "fxRate": {
                      "type": "number"
                    },
                    "paymentMethodId": {
                      "type": "string",
                      "description": "Echoed back from the input."
                    },
                    "fees": {
                      "type": "object"
                    },
                    "settlement": {
                      "type": "object"
                    },
                    "paymentFormUrl": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri",
                      "description": "Upstream provider URL (direct-charge flow)."
                    },
                    "checkoutUrl": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri",
                      "description": "Key2Pay-hosted URL (hosted-checkout flow). Mutually exclusive with paymentFormUrl in the response."
                    },
                    "paymentData": {
                      "type": "object",
                      "description": "Method-specific instructions. See /docs/payment-data-shapes."
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "409": {
            "description": "Idempotency conflict OR cascade exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "cascade_exhausted — no provider could take the charge.",
            "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": [
          "Payments"
        ],
        "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"
          }
        }
      }
    },
    "/checkout/sessions": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create a hosted-checkout session",
        "description": "Hosted-checkout flow. Same body as POST /payments — but the response is shaped as a checkout session with `checkoutUrl` only (no `paymentFormUrl`). You redirect the customer to `checkoutUrl` and we render the per-method UI (QR for PIX, CLABE for SPEI, voucher reference for cash, redirect for card). The integrator never sees the upstream provider URL.",
        "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": 50
                  },
                  "paymentMethodId": {
                    "type": "string",
                    "example": "1008"
                  },
                  "country": {
                    "type": "string",
                    "example": "MEX"
                  },
                  "userEmail": {
                    "type": "string",
                    "format": "email"
                  },
                  "userName": {
                    "type": "string"
                  },
                  "merchantOrderId": {
                    "type": "string"
                  },
                  "returnUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where the hosted page sends the customer once they finish or cancel."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sessionId": {
                      "type": "string",
                      "example": "TXN-MP25SDIS-ZQ68"
                    },
                    "checkoutUrl": {
                      "type": "string",
                      "format": "uri",
                      "example": "https://sandbox.key2pays.com/c/TXN-MP25SDIS-ZQ68?returnUrl=…"
                    },
                    "paymentMethodId": {
                      "type": "string",
                      "example": "1008"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    },
                    "amount": {
                      "type": "number"
                    },
                    "amountLocal": {
                      "type": "number"
                    },
                    "currencyLocal": {
                      "type": "string"
                    },
                    "fxRate": {
                      "type": "number"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          }
        }
      }
    },
    "/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 15 min — 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",
          "fxRate",
          "paymentMethodId",
          "paymentMethod",
          "status",
          "country",
          "fees",
          "settlement",
          "timestamps"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Transaction id (TXN-…).",
            "example": "TXN-MP2WEMT1-KAPL"
          },
          "merchantId": {
            "type": "string",
            "example": "MCH-ON-009"
          },
          "shopId": {
            "type": [
              "string",
              "null"
            ],
            "example": "SHP-MP1STV8W-832A"
          },
          "amount": {
            "type": "number",
            "description": "Amount in USD (major units).",
            "example": 50
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD"
            ],
            "example": "USD"
          },
          "amountLocal": {
            "type": "number",
            "description": "Amount in the customer's local currency (major units).",
            "example": 882.17
          },
          "currencyLocal": {
            "type": "string",
            "description": "ISO-4217 code of the local currency.",
            "example": "MXN"
          },
          "fxRate": {
            "type": "number",
            "description": "FX rate at capture time (USD → local).",
            "example": 17.6434
          },
          "paymentMethodId": {
            "type": [
              "string",
              "null"
            ],
            "description": "OUR 4-digit method id. Same value sent on POST /payments.",
            "example": "1008"
          },
          "paymentMethod": {
            "type": "string",
            "description": "Slug taxonomy (legacy field; prefer paymentMethodId).",
            "example": "spei"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "expired",
              "refunded",
              "chargeback"
            ],
            "description": "See /docs/payment-lifecycle for the full state machine."
          },
          "providerStatus": {
            "type": "string",
            "description": "Raw upstream provider status (debug field).",
            "example": "SUCCESS"
          },
          "country": {
            "type": "string",
            "description": "ISO-3 country code of the payer.",
            "example": "MEX"
          },
          "userEmail": {
            "type": "string",
            "format": "email",
            "example": "test@test.com"
          },
          "userName": {
            "type": "string",
            "example": "Test User"
          },
          "fees": {
            "type": "object",
            "properties": {
              "platform": {
                "type": "number",
                "example": 1.75
              },
              "provider": {
                "type": "number",
                "example": 2.85
              },
              "network": {
                "type": "number",
                "example": 1.5
              },
              "markup": {
                "type": "number",
                "example": 0
              },
              "total": {
                "type": "number",
                "example": 6.1
              }
            }
          },
          "settlement": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "instant",
                  "delayed"
                ],
                "example": "delayed"
              },
              "delay": {
                "type": "string",
                "example": "48h"
              },
              "reserve": {
                "type": "number",
                "example": 5
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "settled",
                  "frozen"
                ],
                "example": "pending"
              }
            }
          },
          "timestamps": {
            "type": "object",
            "properties": {
              "created": {
                "type": "string",
                "format": "date-time",
                "example": "2026-05-12T17:11:51.498Z"
              },
              "completed": {
                "type": "string",
                "format": "date-time"
              },
              "failed": {
                "type": "string",
                "format": "date-time"
              },
              "expired": {
                "type": "string",
                "format": "date-time"
              },
              "refunded": {
                "type": "string",
                "format": "date-time"
              },
              "paymentReceived": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "paymentFormUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Upstream provider's hosted checkout URL (direct-charge flow only).",
            "example": "https://secure-int.key2pay.io/checkout?token=…"
          },
          "checkoutUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Key2Pay-hosted checkout URL (hosted-checkout flow only).",
            "example": "https://sandbox.key2pays.com/c/TXN-…?returnUrl=…"
          },
          "txHash": {
            "type": [
              "string",
              "null"
            ],
            "description": "On-chain settlement hash (set after settlement worker runs)."
          },
          "merchantOrderId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your reference id from POST /payments.",
            "example": "ORD-12345"
          }
        }
      },
      "PaymentMethod": {
        "type": "object",
        "description": "ONE end-user-visible payment rail (Walmart, BBVA, SPEI, OXXO, …). Identified by `paymentMethodId` — same value goes back on POST /payments.",
        "required": [
          "paymentMethodId",
          "method",
          "methodLabel",
          "name",
          "country",
          "countryIso3",
          "channel",
          "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"
          },
          "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."
          }
        }
      },
      "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."
          }
        }
      }
    }
  }
}