{
  "openapi": "3.1.0",
  "info": {
    "title": "WolvPay API",
    "version": "1.0.0",
    "summary": "Non-custodial cryptocurrency invoicing API.",
    "description": "Create crypto invoices, read their status and receive HMAC-signed webhooks.\n\nThere is NO sandbox or test mode: every API key talks to the live service. Test webhook handling with the dashboard Webhook Tester (https://www.wolvpay.com/webhook_test) or by signing payloads locally. See https://www.wolvpay.com/docs#testing.\n\nAll amounts in responses and webhooks are in USD regardless of the `currency` used at creation. Branch on `success`, `error` and `message`; some business-rule failures currently carry `code: 500`.",
    "contact": { "name": "WolvPay", "url": "https://www.wolvpay.com" }
  },
  "externalDocs": { "description": "Human-readable docs (HTML)", "url": "https://www.wolvpay.com/docs" },
  "servers": [{ "url": "https://wolvpay.com/api/v1", "description": "Production (the only environment)" }],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Coins" },
    { "name": "Invoices" }
  ],
  "paths": {
    "/coins": {
      "get": {
        "tags": ["Coins"],
        "operationId": "getCoins",
        "summary": "List supported coins",
        "description": "Returns every supported coin with its minimum payable amount and current price in each supported fiat currency.",
        "responses": {
          "200": {
            "description": "Coins retrieved",
            "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" }, "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" }, "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" } },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessEnvelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": { "type": "array", "items": { "$ref": "#/components/schemas/Coin" } }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/invoices": {
      "post": {
        "tags": ["Invoices"],
        "operationId": "createInvoice",
        "summary": "Create an invoice",
        "description": "Creates an invoice that expires 24 hours after creation.\n\n- `white_label: false` returns a hosted checkout `url`; `coin` is ignored.\n- `white_label: true` (default) returns payment details. With `coin` you get `coin_address` and `coin_amount`; without it the status is `AWAITING_SELECTION` and `available_coins` is returned (then call `updateInvoice`).\n\nNot idempotent: there is no idempotency key. After a timeout, call `listInvoices` before retrying. Limits: 40 invoices/hour and 800/day per account. The invoice amount must be at least $1 USD equivalent.\n\nSome business-rule failures (unsupported currency, amount under $1, no enabled wallet, invoice rate limit) return `code: 500` with a descriptive `message`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateInvoiceRequest" },
              "examples": {
                "hosted": {
                  "summary": "Hosted checkout",
                  "value": { "amount": 25.0, "currency": "USD", "description": "Order #1234", "white_label": false, "redirect_url": "https://example.com/thanks" }
                },
                "whiteLabelWithCoin": {
                  "summary": "White-label, coin chosen",
                  "value": { "amount": 100.0, "currency": "USD", "coin": "ltc", "white_label": true }
                },
                "whiteLabelNoCoin": {
                  "summary": "White-label, customer picks the coin later",
                  "value": { "amount": 100.0, "currency": "EUR", "white_label": true }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invoice created. The shape depends on `white_label` and whether `coin` was supplied.",
            "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" }, "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" }, "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" } },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessEnvelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "oneOf": [
                            { "$ref": "#/components/schemas/HostedInvoiceCreated" },
                            { "$ref": "#/components/schemas/WhiteLabelInvoice" }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "500": { "$ref": "#/components/responses/Error" }
        }
      },
      "get": {
        "tags": ["Invoices"],
        "operationId": "listInvoices",
        "summary": "List invoices",
        "description": "Returns your invoices, newest first.",
        "parameters": [
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 25, "default": 25 }, "description": "Invoices per page. Default and maximum 25." }
        ],
        "responses": {
          "200": {
            "description": "Invoices retrieved",
            "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" }, "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" }, "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" } },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessEnvelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "invoices": { "type": "array", "items": { "$ref": "#/components/schemas/InvoiceListItem" } },
                            "pagination": { "$ref": "#/components/schemas/Pagination" }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "500": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/invoices/{invoice_id}": {
      "parameters": [
        { "name": "invoice_id", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" }, "description": "The `invoice_id` returned when the invoice was created." }
      ],
      "get": {
        "tags": ["Invoices"],
        "operationId": "getInvoice",
        "summary": "Retrieve an invoice",
        "description": "Authoritative source of truth for an invoice's status. Use it to confirm a webhook, and to reconcile if a webhook never arrived. Hosted invoices return only `invoice_id`, `url` and `status`. Returns 404 for invoices that do not exist or belong to another account (for example, Webhook Tester deliveries).",
        "responses": {
          "200": {
            "description": "Invoice retrieved",
            "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" }, "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" }, "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" } },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessEnvelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "oneOf": [
                            { "$ref": "#/components/schemas/HostedInvoice" },
                            { "$ref": "#/components/schemas/WhiteLabelInvoice" }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      },
      "post": {
        "tags": ["Invoices"],
        "operationId": "updateInvoice",
        "summary": "Select the coin for an invoice",
        "description": "Only for white-label invoices still in `AWAITING_SELECTION`. Once a coin is selected it cannot be changed. Failures (already selected, hosted invoice, coin unavailable) currently return `code: 500` with a descriptive `message`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["coin"],
                "properties": {
                  "coin": { "$ref": "#/components/schemas/CoinCode" }
                }
              },
              "example": { "coin": "btc" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Coin selected",
            "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" }, "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" }, "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" } },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SuccessEnvelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": { "$ref": "#/components/schemas/WhiteLabelInvoice" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "500": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "webhooks": {
    "invoiceEvent": {
      "post": {
        "operationId": "invoiceWebhook",
        "summary": "Invoice event delivered to your webhook endpoint",
        "description": "Sent as an HTTPS POST to a webhook URL you register in the dashboard (max 3 per account; public HTTPS domain only - no IPs, no localhost).\n\nVerify `X-WolvPay-Signature`: the hex-encoded HMAC-SHA256 of the RAW request body keyed with that endpoint's webhook secret; compare in constant time. Then confirm with `getInvoice` and fulfill only when `status` is `PAID`, idempotently on `invoice_id`. A 2xx response counts as delivered. Deliveries can repeat and arrive out of order. Requests originate from 128.140.76.192. Discord webhook URLs receive a Discord embed instead (unsigned).\n\nTest deliveries from the dashboard Webhook Tester use the same format with `invoice_id` starting `test_`, uppercase `coin` (`BTC`), and an invoice that does not exist in the API.",
        "parameters": [
          { "name": "X-WolvPay-Signature", "in": "header", "required": true, "schema": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "description": "Hex HMAC-SHA256 of the raw body using the webhook secret." }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookPayload" },
              "example": {
                "invoice_id": "a7d82ddf206ad975f5b3d2d3",
                "amount": "100.00",
                "description": "Invoice for Order #1234",
                "status": "PAID",
                "coin": "ltc",
                "coin_amount": "1.13455866",
                "coin_received": "1.13455866",
                "coin_address": "MQvWK1vphyfV1F6...",
                "redirect_url": "https://example.com/thank-you",
                "created_at": "2026-06-01 15:24:48"
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Acknowledged. Any 2xx counts as delivered." },
          "401": { "description": "Signature invalid (your handler should return this)." },
          "500": { "description": "Your processing failed; the delivery is recorded as failed and can be resent from the dashboard." }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from Dashboard > API Keys, sent as `Authorization: Bearer <key>`. Keep it server-side; never ship it to a browser."
      }
    },
    "headers": {
      "XRateLimitLimit": { "description": "Max requests per minute for this API key (100).", "schema": { "type": "integer" } },
      "XRateLimitRemaining": { "description": "Requests remaining in the current window.", "schema": { "type": "integer" } },
      "XRateLimitReset": { "description": "When the window resets (UTC epoch seconds).", "schema": { "type": "integer" } }
    },
    "responses": {
      "Error": {
        "description": "Error. Branch on `success`, `error` and `message`.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" },
            "example": { "success": false, "error": "Not Found", "code": 404, "message": "Invoice not found." }
          }
        }
      }
    },
    "schemas": {
      "SuccessEnvelope": {
        "type": "object",
        "required": ["success", "result", "code", "data"],
        "properties": {
          "success": { "type": "boolean", "const": true },
          "result": { "type": "string", "description": "Short human-readable message." },
          "code": { "type": "integer", "description": "HTTP status code." },
          "data": {}
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["success", "error", "code", "message"],
        "properties": {
          "success": { "type": "boolean", "const": false },
          "error": { "type": "string", "description": "Category, e.g. Bad Request, Unauthorized, Not Found, Too Many Requests, Internal Server Error, Rate Limit Exceeded." },
          "code": { "type": "integer", "description": "HTTP status code. Note: some client-side business-rule errors carry 500." },
          "message": { "type": "string" }
        }
      },
      "CoinCode": {
        "type": "string",
        "description": "Coin code. Must be a coin you have enabled a wallet for. Live list: GET /coins.",
        "examples": ["btc", "ltc", "erc20_usdc"],
        "enum": [
          "btc", "bch", "ltc", "doge", "eth", "trx",
          "erc20_usdc", "erc20_usdt", "bep20_usdc", "bep20_usdt",
          "polygon_pol", "polygon_usdc", "polygon_usdt",
          "sol_sol", "sol_usdc", "sol_usdt", "trc20_usdt"
        ]
      },
      "FiatCurrency": {
        "type": "string",
        "enum": [
          "AED", "AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "COP", "CZK", "DKK", "EUR", "GBP",
          "HKD", "HUF", "IDR", "INR", "JPY", "LKR", "MXN", "MYR", "NGN", "NOK", "PHP", "PLN",
          "RON", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "UAH", "UGX", "USD", "VND", "ZAR"
        ]
      },
      "InvoiceStatus": {
        "type": "string",
        "enum": ["AWAITING_SELECTION", "AWAITING_PAYMENT", "CONFIRMING_PAYMENT", "PAID", "UNDERPAID", "EXPIRED"],
        "description": "Fulfill an order only on PAID."
      },
      "Coin": {
        "type": "object",
        "properties": {
          "coin": { "$ref": "#/components/schemas/CoinCode" },
          "name": { "type": "string" },
          "logo": { "type": "string" },
          "minimum_transaction_coin": { "type": "string", "description": "Minimum payable amount, in that coin." },
          "prices": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Price of one coin per fiat currency code." }
        }
      },
      "CreateInvoiceRequest": {
        "type": "object",
        "required": ["amount"],
        "properties": {
          "amount": { "type": "number", "exclusiveMinimum": 0, "description": "Amount in `currency`. Converted to USD (rounded to 2 decimals); the USD value must be at least 1." },
          "currency": { "$ref": "#/components/schemas/FiatCurrency", "default": "USD" },
          "coin": { "$ref": "#/components/schemas/CoinCode", "description": "White-label only; ignored when white_label is false. Omit to let the customer choose later." },
          "description": { "type": "string" },
          "white_label": { "type": "boolean", "default": true, "description": "false = hosted checkout page (returns `url`)." },
          "redirect_url": { "type": "string", "format": "uri", "description": "Where to send the customer after payment." }
        }
      },
      "HostedInvoiceCreated": {
        "type": "object",
        "required": ["invoice_id", "url"],
        "properties": {
          "invoice_id": { "type": "string" },
          "url": { "type": "string", "format": "uri", "description": "Hosted checkout URL." }
        }
      },
      "HostedInvoice": {
        "type": "object",
        "required": ["invoice_id", "url", "status"],
        "properties": {
          "invoice_id": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "status": { "$ref": "#/components/schemas/InvoiceStatus" }
        }
      },
      "WhiteLabelInvoice": {
        "type": "object",
        "required": ["invoice_id", "amount", "status"],
        "properties": {
          "invoice_id": { "type": "string" },
          "amount": { "type": "number", "description": "Invoice amount in USD." },
          "description": { "type": ["string", "null"] },
          "status": { "$ref": "#/components/schemas/InvoiceStatus" },
          "coin": { "type": ["string", "null"], "description": "Null until a coin is selected." },
          "coin_amount": { "type": ["number", "string", "null"], "description": "Amount the customer must send, in `coin`." },
          "coin_received": { "type": ["number", "string"], "description": "Total confirmed so far, in `coin` (GET only)." },
          "coin_address": { "type": ["string", "null"], "description": "Deposit address." },
          "minimum_transaction_coin": { "type": ["string", "null"] },
          "redirect_url": { "type": ["string", "null"] },
          "created_at": { "type": "string", "description": "YYYY-MM-DD HH:MM:SS (server time)." },
          "expires_at": { "type": "string", "description": "YYYY-MM-DD HH:MM:SS (server time)." },
          "updated_at": { "type": "string", "description": "Present on updateInvoice responses." },
          "available_coins": { "type": "array", "items": { "$ref": "#/components/schemas/CoinCode" }, "description": "Present while status is AWAITING_SELECTION." }
        }
      },
      "InvoiceListItem": {
        "type": "object",
        "properties": {
          "invoice_id": { "type": "string" },
          "amount": { "type": "string" },
          "description": { "type": ["string", "null"] },
          "status": { "$ref": "#/components/schemas/InvoiceStatus" },
          "coin": { "type": ["string", "null"] },
          "coin_amount": { "type": ["string", "null"] },
          "coin_address": { "type": ["string", "null"] },
          "fee_amount_coin": { "type": ["string", "null"] },
          "fee_amount_usd": { "type": ["string", "null"] },
          "apply_fee": { "type": "integer" },
          "white_label": { "type": "integer", "description": "1 = white-label, 0 = hosted." },
          "redirect_url": { "type": ["string", "null"] },
          "created_at": { "type": "string" },
          "updated_at": { "type": ["string", "null"] }
        }
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "current_page": { "type": "integer" },
          "per_page": { "type": "integer" },
          "total_items": { "type": "integer" },
          "total_pages": { "type": "integer" },
          "has_next": { "type": "boolean" },
          "has_previous": { "type": "boolean" },
          "next_page": { "type": ["integer", "null"] },
          "previous_page": { "type": ["integer", "null"] }
        }
      },
      "WebhookPayload": {
        "type": "object",
        "required": ["invoice_id", "status"],
        "properties": {
          "invoice_id": { "type": "string" },
          "amount": { "type": ["string", "number"], "description": "Invoice amount in USD." },
          "description": { "type": ["string", "null"] },
          "status": { "$ref": "#/components/schemas/InvoiceStatus" },
          "coin": { "type": ["string", "null"], "description": "Lowercase coin code in real events; uppercase (BTC) in Webhook Tester events." },
          "coin_amount": { "type": ["string", "number", "null"] },
          "coin_received": { "type": ["string", "number"], "description": "Total confirmed so far; 0 if none." },
          "coin_address": { "type": ["string", "null"] },
          "redirect_url": { "type": ["string", "null"] },
          "created_at": { "type": "string", "description": "YYYY-MM-DD HH:MM:SS (server time), not ISO-8601." }
        }
      }
    }
  }
}
