{
  "openapi": "3.1.0",
  "info": {
    "title": "BOTONPAY Public API",
    "version": "1.3.0",
    "description": "REST API for accepting payments. Authenticate every request with `Authorization: Bearer <API_KEY>`. Live keys start with `bp_live_`, sandbox keys with `bp_test_`.\n\n**Integration order:** create the local order and persist `merchant_order_id` BEFORE calling `POST /api/public/v1/deals`. Create Deal returns HTTP 201 and the `deal.created` webhook is sent immediately afterwards.\n\n**Identifiers:** `deal_uuid` is issued by BotonPay; `merchant_order_id` is yours. They never match — look up local records by `merchant_order_id`."
  },
  "servers": [
    {
      "url": "https://botonpay.org",
      "description": "Production (v1)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Deals",
      "description": "Create, read, list, cancel deals"
    },
    {
      "name": "Health",
      "description": "Health & status checks"
    },
    {
      "name": "Account",
      "description": "Merchant account info"
    },
    {
      "name": "Reference",
      "description": "Currencies and exchange rates"
    },
    {
      "name": "Webhooks",
      "description": "Webhook management & testing"
    },
    {
      "name": "Payouts",
      "description": "Create, read, list, cancel payouts"
    },
    {
      "name": "API Keys",
      "description": "Manage API keys (root API key required)"
    }
  ],
  "paths": {
    "/api/public/v1/deals": {
      "post": {
        "tags": [
          "Deals"
        ],
        "summary": "Create Deal",
        "description": "Creates a new deal. Mode A: `amount_fiat` + `fiat` — deal is created immediately and a requisite is allocated. Mode B: no `amount_fiat` — a payment link is created; customer picks amount on the payment page.\n\nSuccessful creation returns **HTTP 201 Created**. Create the local order and persist `merchant_order_id` BEFORE calling this endpoint: the `deal.created` webhook is sent immediately after the deal is committed.\n\nIdempotent by `Idempotency-Key` and by `merchant_order_id`.",
        "externalDocs": {
          "description": "For TRY and EGP `client_name` (payer full name) is REQUIRED in Mode A. In Mode B the customer types it on the payment page.",
          "url": "https://botonpay.org/api-docs"
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "UUID v4. Required for production integrations. Retries with the same key and the same body return the original response (HTTP status + body) with the `Idempotent-Replay: true` header. The same key with a different body returns HTTP 409 `idempotency_conflict`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDealRequest"
              },
              "example": {
                "merchant_order_id": "ORDER-124",
                "fiat": "RUB",
                "amount_fiat": 5000,
                "callback_url": "https://merchant.com/webhook",
                "success_url": "https://merchant.com/success",
                "cancel_url": "https://merchant.com/cancel",
                "customer_id": "USER-123",
                "client_name": "Ahmet Yilmaz",
                "metadata": {
                  "product_id": "sku_42"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idempotent response (existing deal/link)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealResponse"
                }
              }
            }
          },
          "201": {
            "description": "Deal or payment link created",
            "headers": {
              "Idempotent-Replay": {
                "description": "`true` when this is a replay of a previously stored response.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "description": "Нет доступных реквизитов — сделка не создана",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorNoAvailableRequisites"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "Реквизиты назначены, но snapshot неполный — сделка отменена, HTTP 201 не отправлен",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRequisitesSnapshotIncomplete"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "get": {
        "tags": [
          "Deals"
        ],
        "summary": "List Deals",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "date_to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "merchant_order_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of deals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Deal"
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "pages": {
                      "type": "integer"
                    },
                    "has_next": {
                      "type": "boolean"
                    },
                    "has_prev": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/health": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Health Check",
        "description": "Liveness endpoint. Does not require an API key.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    },
                    "version": {
                      "type": "string",
                      "example": "1.3.0"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/deals/{id}": {
      "get": {
        "tags": [
          "Deals"
        ],
        "summary": "Get Deal",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`deal_uuid`, `external_id` or `merchant_order_id`."
          }
        ],
        "responses": {
          "200": {
            "description": "Deal details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/deals/by-merchant-order/{mid}": {
      "get": {
        "tags": [
          "Deals"
        ],
        "summary": "Get Deal by merchant_order_id",
        "description": "Reconciliation lookup by the merchant's own order id. Use it after a timeout on Create Deal, or when a webhook arrives for an order you cannot find locally.",
        "parameters": [
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deal details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/deals/by-idempotency-key/{key}": {
      "get": {
        "tags": [
          "Deals"
        ],
        "summary": "Get Create Deal result by Idempotency-Key",
        "description": "Recovers the authoritative Create Deal result when the original response was lost (timeout, connection reset). Returns HTTP 202 with code `DEAL_CREATION_PROCESSING` while the original request is still running.",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stored Create Deal response",
            "headers": {
              "Idempotent-Replayed": {
                "description": "Always `true`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealResponse"
                }
              }
            }
          },
          "202": {
            "description": "Deal creation is still processing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/deals/{id}/cancel": {
      "post": {
        "tags": [
          "Deals"
        ],
        "summary": "Cancel Deal",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deal cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "cancelled"
                      ]
                    },
                    "idempotent": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/deals/{id}/history": {
      "get": {
        "tags": [
          "Deals"
        ],
        "summary": "Get Deal History",
        "description": "Returns the status timeline of the deal (created, paid, completed) and any audit events.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deal history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Merchant Info",
        "description": "Returns merchant profile and API key metadata (scopes, allowed_fiats, is_root).",
        "responses": {
          "200": {
            "description": "Merchant info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/currencies": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "Supported Currencies",
        "description": "Returns allowed fiats for this key with min/max amounts.",
        "responses": {
          "200": {
            "description": "List of currencies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/rates": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "Exchange Rates",
        "description": "USDT→fiat rates (cached 30s). Optional `?fiat=RUB` to fetch a single rate.",
        "parameters": [
          {
            "name": "fiat",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rates map",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/webhooks/test": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Send Test Webhook",
        "description": "Sends a signed sample payload to the key's `webhook_url` (or `url` from the request body). Returns delivery status.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "502": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/status": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Service Status",
        "description": "Detailed status of database, webhook queue, and exchange-rate feed. Does not require auth.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/payouts": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Create Payout",
        "description": "Creates a payout (withdrawal to a recipient). Requires scope `payouts:write` and a unique `Idempotency-Key` header. Reserves `amount + fee` on the merchant fiat balance.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "merchant_order_id",
                  "currency",
                  "amount",
                  "payment_method",
                  "recipient"
                ],
                "properties": {
                  "merchant_order_id": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "currency": {
                    "type": "string",
                    "example": "TRY"
                  },
                  "amount": {
                    "type": "number",
                    "exclusiveMinimum": 0
                  },
                  "payment_method": {
                    "type": "string",
                    "enum": [
                      "card",
                      "bank_account",
                      "phone",
                      "sbp",
                      "upi",
                      "iban",
                      "mobile_wallet"
                    ]
                  },
                  "bank_code": {
                    "type": "string",
                    "maxLength": 50
                  },
                  "recipient": {
                    "type": "object",
                    "required": [
                      "full_name"
                    ],
                    "properties": {
                      "full_name": {
                        "type": "string"
                      },
                      "card_number": {
                        "type": "string"
                      },
                      "account_number": {
                        "type": "string"
                      },
                      "iban": {
                        "type": "string"
                      },
                      "swift": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string"
                      },
                      "upi_id": {
                        "type": "string"
                      },
                      "wallet_number": {
                        "type": "string"
                      },
                      "bank_name": {
                        "type": "string"
                      },
                      "bank_code": {
                        "type": "string"
                      },
                      "country": {
                        "type": "string"
                      },
                      "tax_id": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string",
                        "format": "email"
                      }
                    }
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "callback_url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "success_url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "fail_url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "customer_id": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "metadata": {
                    "type": "object"
                  },
                  "is_test": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payout created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "List Payouts",
        "description": "Requires scope `payouts:read`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "currency",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customer_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "merchant_order_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "date_to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated payouts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/payouts/{id}": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Get Payout",
        "description": "Requires scope `payouts:read`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payout",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/payouts/by-merchant-order/{mid}": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Get Payout by merchant_order_id",
        "description": "Requires scope `payouts:read`.",
        "parameters": [
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payout",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/payouts/{id}/cancel": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Cancel Payout",
        "description": "Cancels a payout that has not been executed yet. Requires scope `payouts:write`. Returns `409 conflict` with `error.details.code = payout_cannot_be_cancelled` when the current status does not allow cancellation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 300
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/sandbox/payouts/{id}/status": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Simulate Payout status (sandbox)",
        "description": "Test keys (`bp_test_…`) only. Moves a sandbox payout to the requested status.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated payout",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/api-keys": {
      "get": {
        "tags": [
          "API Keys"
        ],
        "summary": "List API Keys",
        "description": "Requires a root API key.",
        "responses": {
          "200": {
            "description": "List of keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Create API Key",
        "description": "Requires a root API key. The raw key is returned only once in `data.key`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "is_test": {
                    "type": "boolean"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "deals:read",
                        "deals:write",
                        "deals:cancel",
                        "payouts:read",
                        "payouts:write",
                        "webhooks:read",
                        "webhooks:write",
                        "webhooks:test",
                        "rates:read"
                      ]
                    }
                  },
                  "allowed_fiats": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "allowed_origins": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "webhook_url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/public/v1/api-keys/{id}": {
      "get": {
        "tags": [
          "API Keys"
        ],
        "summary": "Get API Key",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "API Keys"
        ],
        "summary": "Update API Key",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "API Keys"
        ],
        "summary": "Delete API Key",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "bp_live_… or bp_test_…"
      }
    },
    "schemas": {
      "CreateDealRequest": {
        "type": "object",
        "properties": {
          "currency": {
            "type": "string",
            "example": "RUB",
            "description": "Валюта операции (канонич. поле)."
          },
          "fiat": {
            "type": "string",
            "example": "RUB",
            "description": "Deprecated: алиас currency. При конфликте значений — 422."
          },
          "amount_fiat": {
            "type": "number",
            "minimum": 0.01,
            "description": "Сумма в фиате. Алиас: amount."
          },
          "amount": {
            "type": "number",
            "minimum": 0.01,
            "description": "Deprecated: алиас amount_fiat."
          },
          "payment_method": {
            "type": "string",
            "nullable": true
          },
          "bank_code": {
            "type": "string",
            "nullable": true
          },
          "merchant_order_id": {
            "type": "string",
            "maxLength": 120
          },
          "callback_url": {
            "type": "string",
            "format": "uri"
          },
          "success_url": {
            "type": "string",
            "format": "uri"
          },
          "cancel_url": {
            "type": "string",
            "format": "uri"
          },
          "customer_id": {
            "type": "string",
            "maxLength": 120
          },
          "client_name": {
            "type": "string",
            "maxLength": 120,
            "description": "ФИО плательщика. Обязательно для TRY и EGP (Mode A). Передаётся трейдеру для сверки платежа."
          },
          "label": {
            "type": "string",
            "maxLength": 120
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "Deal": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "deal_uuid": {
            "type": "string",
            "format": "uuid",
            "description": "Explicit alias of `id`. The BotonPay deal identifier — never equal to `merchant_order_id`."
          },
          "external_id": {
            "type": "string",
            "example": "D-1042"
          },
          "merchant_order_id": {
            "type": "string",
            "nullable": true,
            "description": "The merchant's own order id, returned verbatim. Use this to look up your local order."
          },
          "client_name": {
            "type": "string",
            "nullable": true,
            "description": "ФИО плательщика (обязательно для TRY и EGP)."
          },
          "status": {
            "type": "string",
            "enum": [
              "waiting_payment",
              "processing",
              "paid",
              "completed",
              "cancelled",
              "expired",
              "failed",
              "disputed"
            ]
          },
          "status_version": {
            "type": "integer",
            "description": "Monotonic status version. Use it to order and deduplicate webhook events."
          },
          "currency": {
            "type": "string"
          },
          "fiat": {
            "type": "string",
            "description": "Deprecated: алиас currency."
          },
          "amount_fiat": {
            "type": "number"
          },
          "amount_fiat_decimal": {
            "type": "string",
            "description": "Сумма в фиате, строка с 2 знаками."
          },
          "amount_usdt": {
            "type": "number"
          },
          "rate": {
            "type": "number"
          },
          "exchange_rate": {
            "type": "string",
            "nullable": true,
            "description": "Зафиксированный курс (строка)."
          },
          "exchange_rate_direction": {
            "type": "string",
            "nullable": true,
            "example": "FIAT_PER_USDT"
          },
          "rate_source": {
            "type": "string",
            "nullable": true
          },
          "rate_locked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "gross_amount_usdt": {
            "type": "string",
            "nullable": true,
            "description": "Сумма в USDT до комиссии (8 знаков)."
          },
          "merchant_fee_percent": {
            "type": "string",
            "nullable": true
          },
          "merchant_fee_usdt": {
            "type": "string",
            "nullable": true
          },
          "merchant_net_amount_usdt": {
            "type": "string",
            "nullable": true,
            "description": "Итог к зачислению мерчанту в USDT."
          },
          "merchant_amount_usdt": {
            "type": "string",
            "nullable": true,
            "description": "Итоговая сумма USDT, зачисляемая на баланс мерчанта после удержания комиссии (алиас merchant_net_amount_usdt; для PayOut — сумма списания)."
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "idempotency_key": {
            "type": "string",
            "nullable": true
          },
          "requisites": {
            "type": "object",
            "nullable": true,
            "description": "Snapshot назначенных реквизитов, зафиксированный в момент создания сделки. Возвращается уже в первом ответе POST /deals и никогда не меняется. Неприменимые поля равны null, но минимум одно платёжное поле всегда заполнено.",
            "properties": {
              "bank_name": {
                "type": "string",
                "nullable": true
              },
              "recipient_name": {
                "type": "string",
                "nullable": true
              },
              "card_number": {
                "type": "string",
                "nullable": true
              },
              "account_number": {
                "type": "string",
                "nullable": true
              },
              "iban": {
                "type": "string",
                "nullable": true
              },
              "phone": {
                "type": "string",
                "nullable": true
              },
              "qr_code": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "paid_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "payment_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "deal_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "requisites_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "is_test": {
            "type": "boolean"
          },
          "payment_details": {
            "type": "object",
            "nullable": true,
            "description": "Deprecated-алиас `requisites` (тот же snapshot).",
            "properties": {
              "bank": {
                "type": "string"
              },
              "holder": {
                "type": "string"
              },
              "card": {
                "type": "string"
              },
              "account": {
                "type": "string",
                "nullable": true
              },
              "iban": {
                "type": "string",
                "nullable": true
              },
              "phone": {
                "type": "string",
                "nullable": true
              },
              "qr_code": {
                "type": "string",
                "nullable": true
              }
            }
          }
        },
        "required": [
          "id",
          "deal_uuid",
          "merchant_order_id",
          "status",
          "currency",
          "fiat",
          "amount_fiat",
          "amount_usdt",
          "rate",
          "requisites_id",
          "requisites",
          "payment_details",
          "payment_url",
          "deal_url",
          "created_at",
          "expires_at",
          "is_test",
          "environment"
        ]
      },
      "DealResponse": {
        "type": "object",
        "required": [
          "success",
          "deal"
        ],
        "description": "Успешный POST /deals возвращает HTTP 201 с полной сделкой и назначенными реквизитами. Ждать webhook deal.created для получения реквизитов не нужно.",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "idempotent": {
            "type": "boolean"
          },
          "deal": {
            "$ref": "#/components/schemas/Deal"
          }
        }
      },
      "ErrorNoAvailableRequisites": {
        "type": "object",
        "description": "HTTP 422: подходящих реквизитов нет. Сделка не создаётся, webhook не отправляется.",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "no_available_requisites"
              },
              "message": {
                "type": "string",
                "example": "No available requisites for this amount and currency"
              }
            }
          }
        }
      },
      "ErrorRequisitesSnapshotIncomplete": {
        "type": "object",
        "description": "HTTP 500: реквизит назначен, но snapshot неполный. HTTP 201 в этом случае не отправляется.",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "requisites_snapshot_incomplete"
              },
              "message": {
                "type": "string",
                "example": "Assigned requisites are incomplete"
              }
            }
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "event",
          "deal_uuid",
          "status",
          "status_version"
        ],
        "properties": {
          "event": {
            "type": "string",
            "description": "Event name only — no status transition, brackets or arrows.",
            "enum": [
              "deal.created",
              "deal.waiting_payment",
              "deal.payment_detected",
              "deal.processing",
              "deal.completed",
              "deal.cancelled",
              "deal.expired",
              "deal.failed",
              "deal.dispute_created",
              "deal.dispute_resolved"
            ]
          },
          "deal_uuid": {
            "type": "string",
            "format": "uuid",
            "description": "BotonPay deal UUID."
          },
          "deal_id": {
            "type": "string",
            "format": "uuid",
            "description": "Alias of `deal_uuid`."
          },
          "merchant_order_id": {
            "type": "string",
            "nullable": true,
            "description": "The merchant's order id, verbatim."
          },
          "previous_status": {
            "type": "string",
            "nullable": true,
            "description": "Status before the change; null for `deal.created`."
          },
          "status": {
            "type": "string",
            "enum": [
              "waiting_payment",
              "processing",
              "paid",
              "completed",
              "cancelled",
              "expired",
              "failed",
              "disputed"
            ]
          },
          "status_version": {
            "type": "integer",
            "description": "Monotonic version of the deal status."
          },
          "fiat": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "amount_fiat": {
            "type": "number"
          },
          "amount_usdt": {
            "type": "number"
          },
          "rate": {
            "type": "number"
          },
          "merchant_amount_usdt": {
            "type": "string",
            "nullable": true
          },
          "client_name": {
            "type": "string",
            "nullable": true,
            "description": "Always present for TRY and EGP."
          },
          "cancelled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "cancellation_reason": {
            "type": "string",
            "nullable": true,
            "example": "timeout"
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "is_test": {
            "type": "boolean"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "event_created_at": {
            "type": "string",
            "format": "date-time"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ApiError": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "invalid_api_key",
                  "forbidden",
                  "origin_not_allowed",
                  "not_found",
                  "rate_limited",
                  "conflict",
                  "idempotency_conflict",
                  "unprocessable",
                  "internal_error",
                  "NO_AVAILABLE_REQUISITES",
                  "CURRENCY_DISABLED",
                  "REQUISITE_SEARCH_TIMEOUT",
                  "DEAL_CREATION_PROCESSING"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {}
            }
          }
        },
        "example": {
          "success": false,
          "error": {
            "code": "invalid_api_key",
            "message": "Invalid API key"
          }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Error response",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "deal.event": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "PayIn deal event callback",
        "description": "Sent to `callback_url` (request body) or, when absent, to the webhook URL configured on the API key. The URL is used verbatim — redirects (301/302/307/308) are NOT followed.\n\nAcknowledge with any HTTP 2xx; the response body is ignored. Response timeout is 10 seconds.\n\nRetries — fast schedule for `deal.created` and for HTTP 404 responses: 2s, 2s, 5s, 10s, 30s; then the standard schedule: 1m, 5m, 15m, 1h, 6h, 24h.\n\nDeduplicate by `X-Webhook-Id`, or by `deal_uuid` + `event` + `status_version`.",
        "security": [],
        "parameters": [
          {
            "name": "X-Signature",
            "in": "header",
            "required": true,
            "description": "HMAC-SHA256 (hex) of the RAW request body, keyed with the PayIn webhook secret. Verify before JSON parsing.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Webhook-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "deal.created"
            }
          },
          {
            "name": "X-Webhook-Id",
            "in": "header",
            "required": true,
            "description": "Delivery UUID — use as the idempotency key.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "X-Webhook-Attempt",
            "in": "header",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "X-Request-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "event": "deal.created",
                "deal_uuid": "b3a1c9e0-1234-4abc-9def-1234567890ab",
                "deal_id": "b3a1c9e0-1234-4abc-9def-1234567890ab",
                "merchant_order_id": "ORDER-124",
                "previous_status": null,
                "status": "waiting_payment",
                "status_version": 1,
                "fiat": "RUB",
                "currency": "RUB",
                "amount_fiat": 5000,
                "amount_usdt": 62.81,
                "rate": 79.6,
                "client_name": null,
                "environment": "live",
                "is_test": false,
                "metadata": {
                  "product_id": "sku_42"
                },
                "event_created_at": "2026-01-01T00:00:00.000Z",
                "timestamp": "2026-01-01T00:00:00.000Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged. Any 2xx is treated as success."
          },
          "404": {
            "description": "Treated as a race condition (merchant record not ready) and retried on the fast schedule."
          }
        }
      }
    }
  }
}