{"openapi":"3.1.0","info":{"title":"Cryptrum Payment Gateway API","version":"1.0.0","description":"Developer-facing API for the Cryptrum crypto payment gateway.\n\n## Authentication\n\nFor the **Gateway** product (payments + wallet) use:\n\n- **`X-Api-Key: <raw key>`** — programmatic, per-project. Get one from\n  the merchant dashboard (Projects → API Keys → New). Shown ONCE; store securely.\n\nFor the **Address Tracker** product (per-webhook endpoints under `/api/webhook/*`) use:\n\n- **`X-Auth-Token: <token>`** — issued once when you create a tracker webhook\n  (see `POST /api/webhook/create`).\n\n> The merchant dashboard itself uses a `Authorization: Bearer <JWT>` session\n> token. You do **not** need this for server-side integration — it's only\n> accepted on a couple of dashboard-style endpoints (create-webhook,\n> list-webhooks). Integrators should use `X-Api-Key` everywhere.\n\n## Idempotency\n\n`POST /api/payment`, `POST /api/merchant/wallet/payout`, and the batch payout\nendpoint accept an **`Idempotency-Key`** header (or `idempotencyKey` body\nfield). Retries with the same key return the original result; collisions with\na key bound to a different project return **409**.\n\n## Errors\n\nAll errors look like:\n```json\n{ \"success\": false, \"error\": \"human-readable message\" }\n```\n\n## Webhooks\n\nWhen you create a payment with a `webhookUrl` (or set one on the project),\nwe POST signed JSON events to that URL on every state change. Headers\n(`X-Webhook-Signature`, `X-Webhook-Timestamp`, `X-Webhook-Event`), full\nevent-type catalog, HMAC-SHA256 verification snippets for Node / Python / PHP,\nretry policy and idempotency advice all live in the **`webhooks`** tag below.\n\n## What you can't do via this API\n\n**Withdrawals are dashboard-only.** Moving merchant-owned funds OUT of your\nwallet — i.e. `/api/merchant/wallet/withdraw` — is intentionally **not**\nexposed via this API. Withdrawals can only be initiated from the merchant\ndashboard at [app.cryptrumpay.com](https://app.cryptrumpay.com) with\nmandatory **2FA verification (TOTP authenticator)**. This protects you against\nAPI-key compromise: an attacker who steals your `X-Api-Key` cannot drain your\nwallet because they cannot complete 2FA.\n\nIf your business case genuinely needs programmatic withdrawal (e.g. automated\ntreasury sweeps), use the **auto-payout** feature instead — see\n[per-coin settings](#tag/wallet) where you can configure\n`autopayoutPolicy: \"limit\"` to sweep balance above a threshold to a pre-approved\ndestination address automatically.","contact":{"name":"Cryptrum Support"},"license":{"name":"Proprietary"}},"servers":[{"url":"http://cryptrum-api:3000","description":"Default"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"payments","description":"Create + query invoices, refund (Gateway product — X-Api-Key). State transitions emit signed webhooks — see the **webhooks** tag for delivery details."},{"name":"wallet","description":"Balances, deposit addresses, and payouts to end-users (Gateway product — X-Api-Key). Withdrawals to your own wallet are dashboard-only with 2FA. Payout lifecycle is delivered via webhooks — see the **webhooks** tag."},{"name":"account","description":"Phase 26 Account Payment Gateway — Binance-style permanent deposit addresses per end-user. You supply a unique `externalId` per customer; we mint one address per chain that lives forever and credit deposits to a running balance. Every inbound transfer emits `account.deposit.received` then `account.deposit.confirmed` webhooks with a multi-currency fiat snapshot frozen at detection time. Use this surface when your product has persistent user identities (exchanges, neobanks, gaming, P2P) and the one-shot Payments API does not fit."},{"name":"swap-bridge","description":"Phase 28 — Same-chain swap (via 1inch) and cross-chain bridge (via Across). Manual operations from your main wallet (`POST /swap/execute`, `POST /bridge/execute`) plus auto-convert at collection time (set per-coin via `PATCH /wallet/coins/:chain/:symbol/settings` with `autoConvertEnabled` + `autoConvertDestChain` + `autoConvertDestToken`) plus payout-with-swap (atomic `POST /wallet/payout` with src/dst fields, gated by your `payoutSwapEnabled` toggle). Tron not supported by providers; auto-convert option hidden for Tron coins."},{"name":"tracker","description":"Address tracker. Create a webhook with your X-Api-Key (or dashboard JWT) — receive back a per-webhook X-Auth-Token. All subsequent management (add/remove addresses, list transactions, deactivate) uses that X-Auth-Token. Perfect for per-end-user watch lists."},{"name":"webhooks","description":"Signed HTTP callbacks delivered to your project's `paymentWebhookUrl` (or the per-payment `webhookUrl` override) on every gateway state change. Lifecycle covers payment creation through refund, payouts to end-users, and merchant withdrawals.\n\n---\n\n### Transport\n\nEvery delivery is a `POST` request with `Content-Type: application/json` and the following headers:\n\n| Header | Value | Notes |\n|---|---|---|\n| `X-Webhook-Signature` | Hex HMAC-SHA256 of `${timestamp}.${JSON.stringify(body)}` | Empty string if the project has no signing secret configured |\n| `X-Webhook-Timestamp` | Unix epoch **milliseconds**, stringified | Use this both as part of the signed string and as the freshness check |\n| `X-Webhook-Event` | Event name (e.g. `payment.paid`) | Same value as the body's `event` field; convenient for routing without parsing JSON |\n\n### Body shape\n\n```json\n{\n  \"event\": \"payment.paid\",\n  \"webhookId\": \"pr_8h2k4m9n\",\n  \"data\": {\n    \"resource\": \"payment\",\n    \"paymentId\": \"pay_lzx2m1a4b7c8d9e0\",\n    \"publicId\": \"pc_5b39f1a7c2e4abc123\",\n    \"chain\": \"tron\",\n    \"token\": \"USDT\",\n    \"status\": \"paid\",\n    \"expectedAmount\": \"49.985007\",\n    \"receivedAmount\": \"49.985007\",\n    \"txHash\": \"0x...\",\n    \"timestamp\": \"2026-06-03T13:42:01.234Z\"\n  }\n}\n```\n\n- `event` — one of the 10 event names below.\n- `webhookId` — the project's public `projectId` (so multi-project consumers can route to the right secret).\n- `data` — resource-specific payload. The `resource` field is one of `payment`, `payout`, or `withdrawal`.\n\n### Signing algorithm\n\n```\nsignature = hex( HMAC-SHA256( secret, timestamp + \".\" + JSON.stringify(body) ) )\n```\n\nThe secret is the raw 64-hex-char string you received once when creating the project (or when calling `POST /api/merchant/projects/{projectId}/rotate-webhook-secret`). It is stored AES-256-GCM-encrypted on our side and never sent over the wire after the initial reveal. Use a **constant-time comparison** when verifying — see snippets below.\n\n> ⚠️ Sign the **raw request body** (the exact bytes you receive), not a re-serialized version. Different JSON serializers reorder keys and rewrite whitespace, which breaks the signature.\n\n### Event types\n\n| Event | Resource | When it fires |\n|---|---|---|\n| `payment.detected` | payment | A deposit tx matching this payment was seen on-chain (0 confirmations) |\n| `payment.confirming` | payment | Tx now has confirmations but not yet `requiredConfirmations` |\n| `payment.paid` | payment | Confirmations reached threshold; received amount equals expected amount |\n| `payment.overpaid` | payment | Confirmations reached threshold; received > expected (excess held for resolution) |\n| `payment.refunded` | payment | A refund payout was broadcast for this payment |\n| `payment.rate_recalculated` | payment | Fiat-mode payment had its locked rate refreshed (rare, manual-trigger) |\n| `payment.expired` | payment | Payment passed `expiresAt` with no deposit (or only an underpayment) |\n| `payout.completed` | payout | Single or batch payout row settled on-chain |\n| `payout.failed` | payout | Payout broadcast failed permanently (after gateway-worker retries) |\n| `withdrawal.completed` | withdrawal | Dashboard-initiated withdrawal settled on-chain |\n| `withdrawal.failed` | withdrawal | Dashboard-initiated withdrawal failed permanently |\n| `account.deposit.received` | account_deposit | First on-chain sighting of an inbound transfer to an account's permanent address (status = `detected`) — Phase 26 [Account API](#tag/account) |\n| `account.deposit.confirmed` | account_deposit | Confirmations reached `requiredConfirmations` — account balance credited (status = `confirmed`) |\n\n### Retry policy\n\nDelivery retries on any non-2xx response or timeout. Defaults:\n\n- `WEBHOOK_TIMEOUT` — 10 000 ms per attempt\n- `WEBHOOK_MAX_RETRIES` — 5 additional attempts after the first\n- `WEBHOOK_RETRY_DELAY` — base delay 1 000 ms, **exponential backoff** (`base * 2^attempt`)\n\nAll three knobs are env-tunable platform-wide. After exhausting retries the failure is logged and the event is dropped — fix your endpoint and re-trigger via `POST /api/merchant/projects/{projectId}/test-webhook` for verification.\n\n### Idempotency\n\nThe same event may be delivered more than once (network blip retries, scanner re-orgs around block boundaries). Dedupe on `(data.paymentId | data.payoutId | data.withdrawalId, event)` — a hash of those two fields makes a perfectly good idempotency key. Return 2xx immediately once the event is persisted; do the actual work in a background queue.\n\n### Replay protection\n\nReject deliveries whose `X-Webhook-Timestamp` is older than ~5 minutes from your server's wall clock. Combined with the HMAC this prevents an attacker who recorded a single valid request from replaying it days later. Make sure your server time is NTP-synced.\n\n---\n\n### Verification snippets\n\n#### Node.js / Express\n\n```javascript\nconst crypto = require('crypto');\nconst express = require('express');\nconst app = express();\n\n// IMPORTANT: capture the raw body BEFORE JSON.parse so the signature still matches.\napp.use('/webhooks/cryptrum', express.raw({ type: 'application/json' }));\n\napp.post('/webhooks/cryptrum', (req, res) => {\n  const signature = req.header('X-Webhook-Signature') || '';\n  const timestamp = req.header('X-Webhook-Timestamp') || '';\n  const event     = req.header('X-Webhook-Event') || '';\n  const rawBody   = req.body; // Buffer\n\n  // 1. Freshness window — reject anything older than 5 minutes\n  if (Math.abs(Date.now() - parseInt(timestamp, 10)) > 5 * 60 * 1000) {\n    return res.status(401).end();\n  }\n\n  // 2. Recompute the HMAC over \"timestamp.rawBody\"\n  const expected = crypto\n    .createHmac('sha256', process.env.CRYPTRUM_WEBHOOK_SECRET)\n    .update(timestamp + '.' + rawBody.toString('utf8'))\n    .digest('hex');\n\n  // 3. Constant-time compare\n  const sigBuf = Buffer.from(signature, 'hex');\n  const expBuf = Buffer.from(expected, 'hex');\n  if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {\n    return res.status(401).end();\n  }\n\n  const { data } = JSON.parse(rawBody.toString('utf8'));\n  // 4. Idempotency — skip if (paymentId|payoutId|withdrawalId, event) already processed\n  // 5. 2xx fast; do real work in a queue\n  return res.status(200).end();\n});\n```\n\n#### Python / Flask\n\n```python\nimport hmac, hashlib, os, time\nfrom flask import Flask, request, abort\n\napp = Flask(__name__)\nSECRET = os.environ[\"CRYPTRUM_WEBHOOK_SECRET\"].encode()\n\n@app.post(\"/webhooks/cryptrum\")\ndef cryptrum_webhook():\n    signature = request.headers.get(\"X-Webhook-Signature\", \"\")\n    timestamp = request.headers.get(\"X-Webhook-Timestamp\", \"\")\n    event     = request.headers.get(\"X-Webhook-Event\", \"\")\n    raw_body  = request.get_data()  # bytes, BEFORE json parsing\n\n    # 1. Freshness window — reject anything older than 5 minutes\n    if abs(int(time.time() * 1000) - int(timestamp or \"0\")) > 5 * 60 * 1000:\n        abort(401)\n\n    # 2. Recompute the HMAC over \"timestamp.raw_body\"\n    expected = hmac.new(\n        SECRET,\n        msg=(timestamp + \".\" + raw_body.decode(\"utf-8\")).encode(),\n        digestmod=hashlib.sha256,\n    ).hexdigest()\n\n    # 3. Constant-time compare\n    if not hmac.compare_digest(expected, signature):\n        abort(401)\n\n    # 4. Idempotency + 5. 2xx fast — queue real work\n    return \"\", 200\n```\n\n#### PHP\n\n```php\n<?php\n$secret    = getenv('CRYPTRUM_WEBHOOK_SECRET');\n$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';\n$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';\n$event     = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';\n$rawBody   = file_get_contents('php://input'); // BEFORE json_decode\n\n// 1. Freshness window — reject anything older than 5 minutes\nif (abs((int) (microtime(true) * 1000) - (int) $timestamp) > 5 * 60 * 1000) {\n    http_response_code(401); exit;\n}\n\n// 2. Recompute the HMAC over \"timestamp.rawBody\"\n$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);\n\n// 3. Constant-time compare\nif (!hash_equals($expected, $signature)) {\n    http_response_code(401); exit;\n}\n\n// 4. Idempotency + 5. 2xx fast — queue real work\nhttp_response_code(200);\n```\n\n---\n\n### Managing the signing secret\n\n- Generated **once** when you create a project (`POST /api/merchant/projects` — field `webhookSecret` in the response).\n- Rotate at any time via `POST /api/merchant/projects/{projectId}/rotate-webhook-secret`. Old secret is **immediately invalidated** — deploy the new one before rotating.\n- Verify your endpoint without waiting for a real payment via `POST /api/merchant/projects/{projectId}/test-webhook`. The test event is signed identically and carries `X-Webhook-Test: true` plus `data.isTest: true` so you can short-circuit business logic."},{"name":"public","description":"Public — no auth"},{"name":"Public discovery","description":"Unauthenticated endpoints — list what the platform supports so integrators can wire up without signing up."}],"x-tagGroups":[{"name":"Gateway","tags":["payments","wallet"]},{"name":"Account API","tags":["account"]},{"name":"Swap & Bridge","tags":["swap-bridge"]},{"name":"Webhooks","tags":["webhooks"]},{"name":"Tracker","tags":["tracker"]},{"name":"Misc","tags":["public","Public discovery"]}],"paths":{"/api/payment/quote":{"post":{"tags":["payments"],"summary":"Get a price quote without creating a payment","description":"Returns the current price for an amount + chain + token + (optional) fiat currency, **without** creating a payment or reserving an address. Useful for showing the user \"$50 USD = X USDT\" before they click Pay.\n\nFor `pricingMode: \"direct_crypto\"`, `rate` is `null` and `expectedAmount` equals `amount`.\n\nQuotes are advisory — the actual rate is locked at payment creation time and may differ slightly if exchange rates moved.\n\n> 💡 The valid `chain` and `token` values for this endpoint come from `GET /api/public/chains` and `GET /api/public/tokens` — the public-discovery endpoints under the **Public discovery** tag.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteRequest"},"example":{"chain":"tron","token":"USDT","pricingMode":"fiat","amount":"50.00","fiatCurrency":"usd"}}}},"responses":{"200":{"description":"Quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"},"example":{"success":true,"rate":"1.0003","expectedAmount":"49.985007","expectedAmountRaw":"49985007","decimals":6,"source":"kraken","validUntil":"2026-05-29T13:35:56.789Z"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/payment/quote \\\n  -H \"X-Api-Key: $CRYPTRUM_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"chain\":\"tron\",\"token\":\"USDT\",\"pricingMode\":\"fiat\",\"amount\":\"50.00\",\"fiatCurrency\":\"usd\"}'"},{"lang":"JavaScript","label":"Node.js","source":"const res = await fetch('https://api.cryptrumpay.com/api/payment/quote', {\n  method: 'POST',\n  headers: { 'X-Api-Key': process.env.CRYPTRUM_API_KEY, 'Content-Type': 'application/json' },\n  body: JSON.stringify({ chain: 'tron', token: 'USDT', pricingMode: 'fiat', amount: '50.00', fiatCurrency: 'usd' }),\n});\nconst { rate, expectedAmount } = await res.json();\nconsole.log(`$50 USD = ${expectedAmount} USDT @ ${rate}`);"}]}},"/api/payment":{"post":{"tags":["payments"],"summary":"Create a payment (invoice)","description":"Creates a new payment invoice. We derive a unique per-payment HD deposit address, lock the exchange rate (if `pricingMode: \"fiat\"`), and return a hosted `checkoutUrl` you can redirect the customer to.\n\n> 💡 The valid `chain` and `token` values come from `GET /api/public/chains` and `GET /api/public/tokens` (Public discovery tag) — no auth needed; safe to call from a frontend during integration.\n\n### Pricing modes\n\n| Mode | What you pass | What we return |\n|---|---|---|\n| `direct_crypto` | `amount` in token units (e.g. `\"50.000000\"` USDT) | Same as input — `expectedAmount` = `amount` |\n| `fiat` | `amount` in `fiatCurrency` (e.g. `\"50.00\"` USD) | We convert at the locked rate; returns `exchangeRate` + raw `expectedAmount` in token units |\n\n### Idempotency\n\nSend a unique `Idempotency-Key` header (or `idempotencyKey` body field). A retry with the same key returns the original payment (`200 OK` + `idempotent: true`) instead of duplicating it. Collisions with a key bound to a different project return **409**.\n\n### Lifecycle\n\nAfter creation, the payment moves through:\n\n`pending → detected → confirming → paid` (happy path)\n\nYou'll get an HMAC-signed webhook at every transition — **see the [webhooks](#tag/webhooks) section for headers, signing algorithm, event catalog, and Node / Python / PHP verification snippets.** If the customer pays less, more, or never, the terminal state is `underpaid` / `overpaid` / `expired` respectively.\n\n### Redirect URLs\n\nIf you pass `successUrl` / `cancelUrl`, the checkout page automatically sends the customer back to your site after payment with a rich query string (`paymentId`, `orderId`, `amount`, `status`, `reason`).\n\n> ⚠️ **The redirect query string is NOT proof of payment.** Always verify via the signed webhook OR re-fetch via `GET /api/payment/{paymentId}`.","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string","example":"order-12345"},"description":"Safe retries — same key returns the original payment."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePaymentRequest"},"example":{"chain":"tron","token":"USDT","pricingMode":"fiat","amount":"50.00","fiatCurrency":"usd","orderId":"ORDER-12345","webhookUrl":"https://your-server.com/webhooks/cryptrum","successUrl":"https://yoursite.com/order/12345/thank-you","cancelUrl":"https://yoursite.com/order/12345/cancelled","expiresInMinutes":60,"customerName":"Riya Sharma","customerEmail":"riya@example.com","metadata":{"sku":"premium-plan"}}}}},"responses":{"200":{"description":"Idempotent replay","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentResponse"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentResponse"},"example":{"success":true,"payment":{"paymentId":"pay_lzx2m1a4b7c8d9e0","publicId":"pc_5b39f1a7c2e4abc123","chain":"tron","token":"USDT","depositAddress":"TXyz1234567890abc","expectedAmount":"49.985007","exchangeRate":"1.0003","status":"pending","expiresAt":"2026-05-29T13:34:56.789Z","orderId":"ORDER-12345","successUrl":"https://yoursite.com/order/12345/thank-you","cancelUrl":"https://yoursite.com/order/12345/cancelled"},"checkoutUrl":"https://pay.cryptrumpay.com/checkout/pc_5b39f1a7c2e4abc123"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Idempotency key collision (different project)"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/payment \\\n  -H \"X-Api-Key: $CRYPTRUM_API_KEY\" \\\n  -H \"Idempotency-Key: order-12345\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"chain\": \"tron\",\n    \"token\": \"USDT\",\n    \"pricingMode\": \"fiat\",\n    \"amount\": \"50.00\",\n    \"fiatCurrency\": \"usd\",\n    \"orderId\": \"ORDER-12345\",\n    \"webhookUrl\": \"https://your-server.com/webhooks/cryptrum\",\n    \"successUrl\": \"https://yoursite.com/order/12345/thank-you\",\n    \"cancelUrl\":  \"https://yoursite.com/order/12345/cancelled\"\n  }'"},{"lang":"JavaScript","label":"Node.js","source":"const res = await fetch('https://api.cryptrumpay.com/api/payment', {\n  method: 'POST',\n  headers: {\n    'X-Api-Key': process.env.CRYPTRUM_API_KEY,\n    'Idempotency-Key': 'order-12345',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    chain: 'tron',\n    token: 'USDT',\n    pricingMode: 'fiat',\n    amount: '50.00',\n    fiatCurrency: 'usd',\n    orderId: 'ORDER-12345',\n    webhookUrl: 'https://your-server.com/webhooks/cryptrum',\n    successUrl: 'https://yoursite.com/order/12345/thank-you',\n    cancelUrl:  'https://yoursite.com/order/12345/cancelled',\n  }),\n});\nconst { payment, checkoutUrl } = await res.json();\n// Redirect the customer to checkoutUrl"},{"lang":"Python","label":"Python","source":"import os, requests\n\nresp = requests.post(\n    \"https://api.cryptrumpay.com/api/payment\",\n    headers={\n        \"X-Api-Key\": os.environ[\"CRYPTRUM_API_KEY\"],\n        \"Idempotency-Key\": \"order-12345\",\n    },\n    json={\n        \"chain\": \"tron\",\n        \"token\": \"USDT\",\n        \"pricingMode\": \"fiat\",\n        \"amount\": \"50.00\",\n        \"fiatCurrency\": \"usd\",\n        \"orderId\": \"ORDER-12345\",\n        \"webhookUrl\": \"https://your-server.com/webhooks/cryptrum\",\n        \"successUrl\": \"https://yoursite.com/order/12345/thank-you\",\n        \"cancelUrl\":  \"https://yoursite.com/order/12345/cancelled\",\n    },\n)\ndata = resp.json()\nprint(data[\"checkoutUrl\"])"},{"lang":"PHP","label":"PHP","source":"<?php\n$ch = curl_init(\"https://api.cryptrumpay.com/api/payment\");\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        \"X-Api-Key: \" . getenv(\"CRYPTRUM_API_KEY\"),\n        \"Idempotency-Key: order-12345\",\n        \"Content-Type: application/json\",\n    ],\n    CURLOPT_POST => true,\n    CURLOPT_POSTFIELDS => json_encode([\n        \"chain\" => \"tron\",\n        \"token\" => \"USDT\",\n        \"pricingMode\" => \"fiat\",\n        \"amount\" => \"50.00\",\n        \"fiatCurrency\" => \"usd\",\n        \"orderId\" => \"ORDER-12345\",\n        \"webhookUrl\" => \"https://your-server.com/webhooks/cryptrum\",\n        \"successUrl\" => \"https://yoursite.com/order/12345/thank-you\",\n        \"cancelUrl\"  => \"https://yoursite.com/order/12345/cancelled\",\n    ]),\n]);\n$resp = json_decode(curl_exec($ch), true);\nheader(\"Location: \" . $resp[\"checkoutUrl\"]);"}]},"get":{"tags":["payments"],"summary":"List payments","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending","detected","confirming","paid","underpaid","overpaid","expired","failed","refunded"]}},{"name":"chain","in":"query","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentList"}}}}}}},"/api/payment/{paymentId}":{"get":{"tags":["payments"],"summary":"Get one payment","parameters":[{"name":"paymentId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/payment/{paymentId}/refund":{"post":{"tags":["payments"],"summary":"Refund a paid / underpaid / overpaid payment","description":"Queues a refund payout for an already-paid payment. On settlement, a `payment.refunded` webhook is delivered — see the [webhooks](#tag/webhooks) section for delivery details.","parameters":[{"name":"paymentId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["toAddress"],"properties":{"toAddress":{"type":"string","description":"Destination address (omit `amount` to refund the entire received amount)"},"amount":{"type":"string","description":"Decimal amount; defaults to receivedAmount"},"reason":{"type":"string"}}}}}},"responses":{"201":{"description":"Refund queued — listen for `payment.refunded` webhook"}}}},"/api/payment/refunds/list":{"get":{"tags":["payments"],"summary":"List refunds","parameters":[{"name":"status","in":"query","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":25}}],"responses":{"200":{"description":"List"}}}},"/api/payment/public/{publicId}":{"get":{"tags":["public"],"security":[],"summary":"Public checkout status (no auth)","description":"Returns the customer-facing projection (customerProjection) for the checkout page to render. `customerName` and `customerEmail` are intentionally NOT included in this response, even when set on the payment — they are PII and are only returned by the authenticated merchant endpoint `GET /api/payment/{paymentId}`. The checkout page should poll this endpoint to render real-time status, deposit progress, and the deposits[] timeline.","parameters":[{"name":"publicId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Public status snapshot — customerProjection (no PII).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerPayment"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/public/chains":{"get":{"operationId":"listChains","tags":["Public discovery"],"security":[],"summary":"List currently enabled blockchain networks","description":"Returns the canonical set of chains this deployment is wired up for. Use the `chain` values from this response as the `chain` field when calling `POST /api/payment` or `POST /api/payment/quote` — passing a chain not in this list will be rejected.\n\nTRON is always present; the EVM chains (ethereum, bsc, polygon, arbitrum, base) are surfaced only when the operator has configured RPC/WS endpoints for them.\n\nNo auth required.","responses":{"200":{"description":"List of supported chains","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicChainList"},"example":{"success":true,"chains":[{"chain":"tron","name":"TRON","nativeSymbol":"TRX","chainIdNum":null,"isEvm":false,"defaultConfirmations":19},{"chain":"ethereum","name":"Ethereum","nativeSymbol":"ETH","chainIdNum":1,"isEvm":true,"defaultConfirmations":12},{"chain":"bsc","name":"BNB Smart Chain","nativeSymbol":"BNB","chainIdNum":56,"isEvm":true,"defaultConfirmations":12},{"chain":"polygon","name":"Polygon","nativeSymbol":"MATIC","chainIdNum":137,"isEvm":true,"defaultConfirmations":30},{"chain":"arbitrum","name":"Arbitrum One","nativeSymbol":"ETH","chainIdNum":42161,"isEvm":true,"defaultConfirmations":8},{"chain":"base","name":"Base","nativeSymbol":"ETH","chainIdNum":8453,"isEvm":true,"defaultConfirmations":10}]}}}},"500":{"description":"Internal server error"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl https://api.cryptrumpay.com/api/public/chains"},{"lang":"JavaScript","label":"Node.js","source":"const res = await fetch('https://api.cryptrumpay.com/api/public/chains');\nconst { chains } = await res.json();\nchains.forEach(c => console.log(`${c.chain} (${c.nativeSymbol})`));"}]}},"/api/public/tokens":{"get":{"operationId":"listTokens","tags":["Public discovery"],"security":[],"summary":"List all supported tokens (optionally scoped to one chain)","description":"Returns every token (native + ERC20/TRC20) currently in the registry, grouped per chain. Use the `(chain, symbol)` pair from this list as the `chain` + `token` fields when calling `POST /api/payment` or `POST /api/payment/quote` — passing a combination not in this list will be rejected.\n\nEach chain is guaranteed to include its native pseudo-token (e.g. TRX for tron, ETH for ethereum) even if no native row is present in the database registry — the response is the canonical \"what can I pay with?\" source.\n\nNo auth required.","parameters":[{"name":"chain","in":"query","required":false,"schema":{"type":"string","example":"ethereum"},"description":"Optional — restrict the response to a single chain. Must match a chain from `/api/public/chains` or the call returns 400."}],"responses":{"200":{"description":"List of supported tokens","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTokenList"},"example":{"success":true,"tokens":[{"chain":"tron","symbol":"TRX","contractAddress":null,"decimals":6,"isNative":true,"label":"TRON"},{"chain":"tron","symbol":"USDT","contractAddress":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t","decimals":6,"isNative":false,"label":"Tether USD (TRC-20)"},{"chain":"ethereum","symbol":"ETH","contractAddress":null,"decimals":18,"isNative":true,"label":"Ether"},{"chain":"ethereum","symbol":"USDT","contractAddress":"0xdac17f958d2ee523a2206206994597c13d831ec7","decimals":6,"isNative":false,"label":"Tether USD"}]}}}},"400":{"description":"Unsupported chain query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"error":"Unsupported chain"}}}},"500":{"description":"Internal server error"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"# All tokens\ncurl https://api.cryptrumpay.com/api/public/tokens\n\n# Just one chain\ncurl https://api.cryptrumpay.com/api/public/tokens?chain=ethereum"},{"lang":"JavaScript","label":"Node.js","source":"const res = await fetch('https://api.cryptrumpay.com/api/public/tokens?chain=tron');\nconst { tokens } = await res.json();\ntokens.forEach(t => console.log(`${t.chain}/${t.symbol} (${t.isNative ? 'native' : t.contractAddress})`));"}]}},"/api/merchant/wallet":{"get":{"tags":["wallet"],"summary":"All balances + system credit","responses":{"200":{"description":"OK"}}}},"/api/merchant/wallet/balance":{"get":{"tags":["wallet"],"summary":"Single-token balance","parameters":[{"name":"chain","in":"query","required":true,"schema":{"type":"string"}},{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/merchant/wallet/addresses":{"get":{"tags":["wallet"],"summary":"Deposit addresses (TRON + EVM)","responses":{"200":{"description":"OK"}}}},"/api/merchant/wallet/transactions":{"get":{"tags":["wallet"],"summary":"Wallet ledger","parameters":[{"name":"chain","in":"query","schema":{"type":"string"}},{"name":"token","in":"query","schema":{"type":"string"}},{"name":"type","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"}}}},"/api/merchant/wallet/payout":{"post":{"tags":["wallet"],"summary":"Single payout to an end-user (gas sponsored, optional swap-in-payout)","description":"Sends `amount` of `token` on `chain` to `toAddress`. Your wallet balance is debited 1:1 — **no on-chain fee** is deducted. Gas is sponsored by Cryptrum (billed via CPT consumption, not deducted from the transfer).\n\nThe call returns immediately with `status: \"requested\"` — the on-chain broadcast happens asynchronously. Listen for the `payout.completed` webhook event for the final tx hash; `payout.failed` fires if the broadcast exhausts retries. **See the [webhooks](#tag/webhooks) section for delivery details, signing algorithm, and verification snippets.**\n\n### Phase 28 — Payout-with-swap (atomic)\n\nOptional. When enabled in your wallet settings (`payoutSwapEnabled = true`), include `srcChain` + `srcToken` + `srcAmount` to convert source funds to the destination token and deliver to `toAddress` in a **single on-chain operation** (no intermediate state). Same-chain uses 1inch swap; cross-chain uses Across bridge. Recipient receives whatever the conversion yields (exact-input mode — output is market-determined, bounded by `slippage`).\n\nField rules:\n- `amount` is **mutually exclusive** with `srcAmount` (400 `INVALID_PAYOUT_MODE` if both)\n- If you send any of `srcChain/srcToken/srcAmount` you must send ALL three (400 `INVALID_PAYOUT_MODE` if partial)\n- Both `srcToken` and the destination `token` must be in your merchant-enabled token list (admin-enabled ∩ your overrides) — 400 `TOKEN_NOT_ENABLED` otherwise\n- Tron source or dest → 400 `SWAP_NOT_SUPPORTED_FOR_CHAIN` (providers EVM-only)\n- `payoutSwapEnabled` toggle MUST be ON — else 400 `SWAP_IN_PAYOUT_DISABLED`\n- Master `payoutEnabled` toggle MUST be ON — else 503 `PAYOUT_DISABLED`\n\n### Idempotency\n\nAlways send an `Idempotency-Key` (or pass `idempotencyKey` in body). On-chain transactions are irreversible — without idempotency, network retries can double-pay your customer.","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string","example":"payout-user-42-2026-05-29"},"description":"**Strongly recommended.** Prevents double-pay on retry."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutRequest"},"example":{"chain":"tron","token":"USDT","toAddress":"TXyzRecipientAddressHere","amount":"10.00"}}}},"responses":{"201":{"description":"Queued","content":{"application/json":{"example":{"success":true,"payout":{"payoutId":"po_abc123","status":"requested","amount":"10.00","toAddress":"TXyzRecipientAddressHere"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Insufficient balance"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/merchant/wallet/payout \\\n  -H \"X-Api-Key: $CRYPTRUM_API_KEY\" \\\n  -H \"Idempotency-Key: payout-user-42-2026-05-29\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"chain\":\"tron\",\"token\":\"USDT\",\"toAddress\":\"TXyz...\",\"amount\":\"10.00\"}'"},{"lang":"JavaScript","label":"Node.js","source":"const res = await fetch('https://api.cryptrumpay.com/api/merchant/wallet/payout', {\n  method: 'POST',\n  headers: {\n    'X-Api-Key': process.env.CRYPTRUM_API_KEY,\n    'Idempotency-Key': `payout-user-${userId}-${Date.now()}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ chain: 'tron', token: 'USDT', toAddress, amount: '10.00' }),\n});\nconst { payout } = await res.json();"}]}},"/api/merchant/wallet/payout/batch":{"post":{"tags":["wallet"],"summary":"Batch payout (1-100 recipients in one call)","description":"Send up to **100 payouts** in a single API call. All destinations must be on the same `chain` + `token`. Total amount is deducted atomically (no per-row fee) — if your available balance is less than `total`, the **entire batch is rejected** with 403 and zero funds move.\n\nEach row can carry its own `idempotencyKey` for partial-retry safety. Listen for individual `payout.completed` (or `payout.failed`) webhook events per row — **see the [webhooks](#tag/webhooks) section for delivery details**.\n\nCommon use cases: payroll, mass refunds, creator/streamer payouts, marketplace seller settlement.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutBatchRequest"},"example":{"chain":"tron","token":"USDT","destinations":[{"toAddress":"TAlice1234567890abcdef","amount":"10.00","idempotencyKey":"pay-row-1"},{"toAddress":"TBob1234567890abcdef","amount":"25.50","idempotencyKey":"pay-row-2"},{"toAddress":"TCarol1234567890abcdef","amount":"100","idempotencyKey":"pay-row-3"}]}}}},"responses":{"201":{"description":"Batch queued","content":{"application/json":{"example":{"success":true,"batch":{"totalAmount":"135.50","acceptedRows":3,"rejectedRows":0},"payouts":[{"payoutId":"po_aa","toAddress":"TAlice...","amount":"10.00","status":"requested"},{"payoutId":"po_bb","toAddress":"TBob...","amount":"25.50","status":"requested"},{"payoutId":"po_cc","toAddress":"TCarol...","amount":"100","status":"requested"}]}}}},"403":{"description":"Insufficient balance for total — whole batch rejected"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/merchant/wallet/payout/batch \\\n  -H \"X-Api-Key: $CRYPTRUM_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"chain\": \"tron\",\n    \"token\": \"USDT\",\n    \"destinations\": [\n      {\"toAddress\":\"TAlice...\",\"amount\":\"10.00\"},\n      {\"toAddress\":\"TBob...\",\"amount\":\"25.50\"},\n      {\"toAddress\":\"TCarol...\",\"amount\":\"100\"}\n    ]\n  }'"},{"lang":"JavaScript","label":"Node.js","source":"// Pay 50 creators in one call\nconst destinations = creators.map((c, i) => ({\n  toAddress: c.tronAddress,\n  amount: c.earnings.toFixed(2),\n  idempotencyKey: `payout-cycle-${cycleId}-row-${i}`,\n}));\n\nconst res = await fetch('https://api.cryptrumpay.com/api/merchant/wallet/payout/batch', {\n  method: 'POST',\n  headers: { 'X-Api-Key': process.env.CRYPTRUM_API_KEY, 'Content-Type': 'application/json' },\n  body: JSON.stringify({ chain: 'tron', token: 'USDT', destinations }),\n});\nconst { batch, payouts } = await res.json();\nconsole.log(`${batch.acceptedRows} payouts queued, total ${batch.totalAmount}`);"}]}},"/api/merchant/wallet/payouts":{"get":{"tags":["wallet"],"summary":"Recent payouts","responses":{"200":{"description":"OK"}}}},"/api/merchant/swap-bridge/swap/quote":{"post":{"tags":["swap-bridge"],"summary":"Quote a same-chain swap (1inch)","description":"Returns a price preview for swapping `srcToken` to `destToken` on the same chain via 1inch DEX aggregator. **No on-chain action** — useful for showing the user \"100 USDT = X USDC\" before they commit.\n\nFor the actual on-chain execution, follow up with `POST /swap/execute` using the same params.\n\nTron not supported (provider EVM-only).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapQuoteRequest"},"example":{"srcChain":"ethereum","srcToken":"USDT","srcAmountRaw":"100000000","destToken":"USDC"}}}},"responses":{"200":{"description":"Quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapBridgeQuoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/api/merchant/swap-bridge/swap/execute":{"post":{"tags":["swap-bridge"],"summary":"Execute a same-chain swap from your main wallet","description":"Broadcasts a 1inch swap from your merchant main wallet on the same chain. Pre-broadcast simulation (eth_call) catches reverts before gas is spent. Output lands back in your main wallet — same-chain swaps are atomic (no intermediate state).\n\nReturns immediately with `txnId` (the SwapBridgeTransaction ledger row). Track final status via `GET /history` or look at the ledger row directly.\n\nCPT billed at `swap` event weight × the chain×tokenType multiplier (admin-configurable). Provider fee (1inch `fee` param) deducted from output automatically if admin has set `swap_bridge.swap_fee > 0`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapExecuteRequest"},"example":{"srcChain":"ethereum","srcToken":"USDT","srcAmountRaw":"100000000","destToken":"USDC","slippage":1}}}},"responses":{"200":{"description":"Queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapBridgeExecuteResponse"},"example":{"success":true,"txnId":"sbt_abc123","quote":{"dstAmount":"99800000"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"description":"Insufficient CPT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/merchant/swap-bridge/swap/execute \\\n  -H \"X-Api-Key: $CRYPTRUM_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"srcChain\":\"ethereum\",\"srcToken\":\"USDT\",\"srcAmountRaw\":\"100000000\",\"destToken\":\"USDC\",\"slippage\":1.0}'"}]}},"/api/merchant/swap-bridge/bridge/quote":{"post":{"tags":["swap-bridge"],"summary":"Quote a cross-chain bridge (Across Protocol)","description":"Returns a fee + amount estimate for bridging from `srcChain` to `destChain` via Across. Includes relayer fee + LP fee + suggested fillDeadline. `srcChain` MUST differ from `destChain` — use `/swap/quote` for same-chain.\n\nTron not supported.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeQuoteRequest"},"example":{"srcChain":"ethereum","srcToken":"USDC","srcAmountRaw":"100000000","destChain":"polygon","destToken":"USDC"}}}},"responses":{"200":{"description":"Quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapBridgeQuoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/merchant/swap-bridge/bridge/execute":{"post":{"tags":["swap-bridge"],"summary":"Execute a cross-chain bridge from your main wallet","description":"Broadcasts an Across SpokePool depositV3 from your merchant main wallet on `srcChain`. Relayers fill on `destChain` typically within ~2 seconds.\n\n**Async settlement:** the response returns immediately with `txnId` and the ledger row in `status: 'pending'`. Our async cron polls Across `/deposit/status` every minute and flips the row to `succeeded` (with destination fill tx hash + actual output amount) once the relayer has filled. Track via `GET /history` or look at the ledger row directly.\n\nCPT billed at `bridge` event weight on broadcast. Provider fees (Across LP + relayer + optional `appFee` markup) deducted from output automatically.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeExecuteRequest"},"example":{"srcChain":"ethereum","srcToken":"USDC","srcAmountRaw":"100000000","destChain":"polygon","destToken":"USDC","slippage":1}}}},"responses":{"200":{"description":"Queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapBridgeExecuteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"description":"Insufficient CPT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/merchant/swap-bridge/history":{"get":{"tags":["swap-bridge"],"summary":"List your swap + bridge transactions","description":"Paginated list of every swap/bridge op across all modes — manual operations from the panel, auto-convert at collection time, and payout-with-swap. Provider fee fields are HIDDEN from this view (admin-only).","parameters":[{"name":"mode","in":"query","schema":{"type":"string","enum":["manual","auto","payout"]}},{"name":"kind","in":"query","schema":{"type":"string","enum":["swap","bridge"]}},{"name":"chain","in":"query","schema":{"type":"string"},"description":"Matches either sourceChain OR destChain"},{"name":"status","in":"query","schema":{"type":"string","enum":["pending","succeeded","failed"]}},{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapBridgeHistoryResponse"}}}}}}},"/api/merchant/swap-bridge/platform-fees":{"get":{"tags":["swap-bridge"],"summary":"Read the platform fees currently in effect","description":"Transparency endpoint — returns the platform-level fee percentages your operations will pay. Receiver addresses + provider API keys are admin-only and NOT returned here.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwapBridgePlatformFeesResponse"},"example":{"success":true,"swapFeePercent":0.3,"bridgeFeePercent":0.5,"defaultSlippage":1,"autoConvertMarkupPercent":10}}}}}}},"/api/merchant/projects/{projectId}/rotate-webhook-secret":{"post":{"tags":["webhooks"],"summary":"Rotate the project payment-webhook signing secret","description":"Generates a fresh 32-byte (64-hex-char) HMAC signing secret for this project and returns it **once**. The old secret is invalidated immediately — every webhook delivery after this call signs with the new secret only.\n\n### Operational order\n\n1. Call this endpoint and capture `webhookSecret` from the response.\n2. Deploy the new secret to your webhook receiver (env var, secret manager, etc.).\n3. Once the receiver is live, you're done — no second call needed; the rotation is already in effect.\n\nIf you forget to redeploy, every subsequent delivery will fail signature verification on your side and we'll retry per the standard policy (5 retries, exponential backoff). Use `POST /api/merchant/projects/{projectId}/test-webhook` to confirm the new secret is wired up correctly.\n\n> ⚠️ The plaintext secret is shown ONCE. We store it AES-256-GCM-encrypted; it cannot be recovered. If you lose it, rotate again.\n\nSee the **webhooks** tag for the full HMAC signing algorithm and verification snippets.","security":[{"BearerAuth":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"},"example":"pr_8h2k4m9n"}],"responses":{"200":{"description":"Rotated — new secret returned once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateWebhookSecretResponse"},"example":{"success":true,"message":"Webhook secret rotated. Copy it now — it will not be shown again.","webhookSecret":"4f2e8d1c9b3a7e6f0d2c8a5b1e9f4d7c2a8b5e1f9d4c7a2b8e5f1d4c7a2b8e5f","rotatedAt":"2026-06-03T13:42:01.234Z"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Project not owned by the calling merchant"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/merchant/projects/pr_8h2k4m9n/rotate-webhook-secret \\\n  -H \"Authorization: Bearer $MERCHANT_JWT\""},{"lang":"JavaScript","label":"Node.js","source":"const res = await fetch(\n  `https://api.cryptrumpay.com/api/merchant/projects/${projectId}/rotate-webhook-secret`,\n  { method: 'POST', headers: { Authorization: `Bearer ${jwt}` } }\n);\nconst { webhookSecret } = await res.json();\n// Save webhookSecret to your secret manager immediately — shown ONCE."},{"lang":"Python","label":"Python","source":"import os, requests\n\nresp = requests.post(\n    f\"https://api.cryptrumpay.com/api/merchant/projects/{project_id}/rotate-webhook-secret\",\n    headers={\"Authorization\": f\"Bearer {os.environ['MERCHANT_JWT']}\"},\n)\nsecret = resp.json()[\"webhookSecret\"]\n# Persist secret to your secret manager NOW — it cannot be recovered later."}]}},"/api/merchant/projects/{projectId}/test-webhook":{"post":{"tags":["webhooks"],"summary":"Send a synthetic signed webhook to your endpoint","description":"Builds and POSTs a fully-signed test payload for any of the 10 supported event types to either the project's configured `paymentWebhookUrl` or an optional one-off URL passed in the body. The payload is signed with the project's **real** webhook secret, so a successful 2xx response proves your verification logic is wired correctly.\n\n### Telltale fields\n\nTest deliveries are otherwise indistinguishable from production deliveries, except:\n\n- The request carries an extra `X-Webhook-Test: true` header.\n- The body's `data.isTest` is `true`.\n- All identifiers use the `*_test_xxx` placeholder pattern (e.g. `paymentId: \"pay_test_xxx\"`).\n\nUse either signal to short-circuit your business logic — typically: verify the signature normally, then return 2xx without persisting or kicking off downstream work.\n\n### Response shape\n\nThe response describes how the call went so you can debug from one place — `statusCode`, `durationMs`, the exact signature + timestamp used, the literal payload sent, and a 500-character preview of your endpoint's response body.\n\nSee the **webhooks** tag for header definitions, signing algorithm, event-type catalog, and Node / Python / PHP verification snippets.","security":[{"BearerAuth":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"},"example":"pr_8h2k4m9n"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestWebhookRequest"},"example":{"event":"payment.paid","url":"https://staging.your-server.com/webhooks/cryptrum"}}}},"responses":{"200":{"description":"Delivery attempted (check `delivered` + `statusCode`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestWebhookResponse"},"example":{"success":true,"delivered":true,"statusCode":200,"durationMs":142,"signature":"7c3f0e8a1b2d4f6c5a9b3e7d1f0c8a4b6e2d5f9c7a3b1e8d4f0c6a9b5e2d3f7c","timestamp":"1748965321234","payload":{"event":"payment.paid","webhookId":"pr_8h2k4m9n","data":{"isTest":true,"resource":"payment","paymentId":"pay_test_xxx","status":"paid","chain":"bsc","token":"USDT","expectedAmount":"10.00","receivedAmount":"10.00","timestamp":"2026-06-03T13:42:01.234Z"}},"responseBody":"{\"received\":true}"}}}},"400":{"description":"Invalid event name, or no webhook URL available (neither body `url` nor project default)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Project not owned by the calling merchant"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/merchant/projects/pr_8h2k4m9n/test-webhook \\\n  -H \"Authorization: Bearer $MERCHANT_JWT\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event\":\"payment.paid\"}'"},{"lang":"JavaScript","label":"Node.js","source":"// Fire a synthetic payment.paid to staging while smoke-testing the receiver\nconst res = await fetch(\n  `https://api.cryptrumpay.com/api/merchant/projects/${projectId}/test-webhook`,\n  {\n    method: 'POST',\n    headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      event: 'payment.paid',\n      url:   'https://staging.your-server.com/webhooks/cryptrum',\n    }),\n  }\n);\nconst { delivered, statusCode, durationMs } = await res.json();\nconsole.log(`Test delivery: ${statusCode} in ${durationMs}ms (delivered=${delivered})`);"},{"lang":"Python","label":"Python","source":"import os, requests\n\nresp = requests.post(\n    f\"https://api.cryptrumpay.com/api/merchant/projects/{project_id}/test-webhook\",\n    headers={\"Authorization\": f\"Bearer {os.environ['MERCHANT_JWT']}\"},\n    json={\"event\": \"payment.overpaid\"},\n)\nresult = resp.json()\nprint(\"delivered:\", result[\"delivered\"], \"status:\", result[\"statusCode\"])"}]}},"/api/merchant/webhooks":{"get":{"tags":["tracker"],"summary":"List my webhooks (decrypts authToken for use with /api/webhook/* endpoints)","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookList"}}}}}}},"/api/webhook/create":{"post":{"tags":["tracker"],"summary":"Create a tracker webhook (ownership-checked)","description":"Creates a new tracker webhook scoped to a project. Pick **any one** of these auth methods:\n\n| Header | Use case | Project owner |\n|---|---|---|\n| **`X-Api-Key`** | Programmatic — your server creates webhooks for each of your end-users (recommended for SDK integration) | The API key's project (locked) |\n| **`Authorization: Bearer <JWT>`** | Logged-in merchant via the dashboard UI | Body `projectId` if given, else merchant's first active project |\n| **`X-Admin-Key`** | Platform admin | Body `projectId` or `merchantId` required |\n\n> When using `X-Api-Key`, **`projectId` in the body is ignored** — the key already locks ownership to its own project. This prevents API-key compromise from creating webhooks under a different project.\n\n### What you get back\n\nA one-time payload containing:\n- `webhookId` — public identifier\n- `authToken` (raw, **shown ONCE**) — use as `X-Auth-Token` on every subsequent `/api/webhook/*` call\n- `secretKey` (raw, **shown ONCE**) — used to HMAC-verify incoming webhook deliveries\n\nStore both **immediately** and securely. They cannot be retrieved later.\n\n### Plan gating\n\nThe merchant's plan must include tracker access (`hasTracker`) and allow the requested chain. The total webhook count must stay under `maxWebhooks` for the plan.","security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"},"example":{"url":"https://your-server.com/webhooks/tracker","name":"User #42 deposit watcher","chain":"tron","metadata":{"userId":42,"accountKind":"savings"}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookResponse"},"example":{"success":true,"webhook":{"webhookId":"wh_abc123def456","authToken":"a1b2c3d4e5f6...","secretKey":"f6e5d4c3b2a1...","url":"https://your-server.com/webhooks/tracker","name":"User #42 deposit watcher","chain":"tron","startBlock":65000000},"warning":"Save authToken and secretKey NOW — they cannot be retrieved later."}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/webhook/create \\\n  -H \"X-Api-Key: $CRYPTRUM_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://your-server.com/webhooks/tracker\",\n    \"name\": \"User #42 deposit watcher\",\n    \"chain\": \"tron\",\n    \"metadata\": {\"userId\": 42}\n  }'"},{"lang":"JavaScript","label":"Node.js","source":"// Server-side: create a per-user tracker webhook\nasync function createTrackerForUser(user) {\n  const res = await fetch('https://api.cryptrumpay.com/api/webhook/create', {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': process.env.CRYPTRUM_API_KEY,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n      url:   `https://your-server.com/webhooks/user/${user.id}`,\n      name:  `Deposit watcher for user #${user.id}`,\n      chain: 'tron',\n      metadata: { userId: user.id },\n    }),\n  });\n  const { webhook } = await res.json();\n  // CRITICAL: Save these immediately — shown ONCE.\n  await db.userTracker.create({\n    userId: user.id,\n    webhookId: webhook.webhookId,\n    authToken: webhook.authToken,   // → X-Auth-Token for subsequent calls\n    secretKey: webhook.secretKey,   // → HMAC-verify incoming events\n  });\n}"},{"lang":"Python","label":"Python","source":"import os, requests\n\nresp = requests.post(\n    \"https://api.cryptrumpay.com/api/webhook/create\",\n    headers={\"X-Api-Key\": os.environ[\"CRYPTRUM_API_KEY\"]},\n    json={\n        \"url\": \"https://your-server.com/webhooks/tracker\",\n        \"name\": \"User #42 deposit watcher\",\n        \"chain\": \"tron\",\n        \"metadata\": {\"userId\": 42},\n    },\n)\nwebhook = resp.json()[\"webhook\"]\n# Save webhook[\"authToken\"] and webhook[\"secretKey\"] NOW — shown once."}]}},"/api/webhook/get":{"get":{"tags":["tracker"],"summary":"Get the webhook this token belongs to","security":[{"TrackerTokenAuth":[]}],"responses":{"200":{"description":"Webhook"}}}},"/api/webhook/update":{"post":{"tags":["tracker"],"summary":"Update webhook settings (url, name, maxAddresses)","security":[{"TrackerTokenAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"name":{"type":"string"},"maxAddresses":{"type":"integer","minimum":1}}}}}},"responses":{"200":{"description":"OK"}}}},"/api/webhook/activate":{"post":{"tags":["tracker"],"summary":"Resume webhook delivery (sets isActive=true)","security":[{"TrackerTokenAuth":[]}],"responses":{"200":{"description":"OK"}}}},"/api/webhook/deactivate":{"post":{"tags":["tracker"],"summary":"Pause webhook delivery (sets isActive=false)","security":[{"TrackerTokenAuth":[]}],"responses":{"200":{"description":"OK"}}}},"/api/webhook/regenerate-secret":{"post":{"tags":["tracker"],"summary":"Rotate authToken + secretKey (old ones immediately invalid)","security":[{"TrackerTokenAuth":[]}],"responses":{"200":{"description":"New credentials (shown once)"}}}},"/api/webhook/test":{"post":{"tags":["tracker"],"summary":"Fire a test event to your webhook URL","security":[{"TrackerTokenAuth":[]}],"responses":{"200":{"description":"Delivery result"}}}},"/api/webhook/address/list":{"get":{"tags":["tracker"],"summary":"List watched addresses (paginated)","security":[{"TrackerTokenAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"search","in":"query","schema":{"type":"string"},"description":"Substring match on address"}],"responses":{"200":{"description":"List"}}}},"/api/webhook/address/add":{"post":{"tags":["tracker"],"summary":"Add one address","security":[{"TrackerTokenAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address"],"properties":{"address":{"type":"string","description":"TRON base58 (T...) or EVM 0x... (lowercased server-side)"},"label":{"type":"string"}}}}}},"responses":{"201":{"description":"Added"},"409":{"description":"Already added"}}}},"/api/webhook/address/add-bulk":{"post":{"tags":["tracker"],"summary":"Add up to 500 addresses in one call","security":[{"TrackerTokenAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["addresses"],"properties":{"addresses":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":500}}}}}},"responses":{"200":{"description":"Inserted + skipped counts"}}}},"/api/webhook/address/update":{"post":{"tags":["tracker"],"summary":"Update an address label / active state","security":[{"TrackerTokenAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address"],"properties":{"address":{"type":"string"},"label":{"type":"string"},"isActive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"OK"}}}},"/api/webhook/address/remove":{"post":{"tags":["tracker"],"summary":"Remove one address","security":[{"TrackerTokenAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address"],"properties":{"address":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}},"/api/webhook/address/remove-bulk":{"post":{"tags":["tracker"],"summary":"Remove many addresses in one call","security":[{"TrackerTokenAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["addresses"],"properties":{"addresses":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":500}}}}}},"responses":{"200":{"description":"Removed count"}}}},"/api/webhook/transaction/list":{"get":{"tags":["tracker"],"summary":"List detected transactions for this webhook (paginated)","security":[{"TrackerTokenAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"direction","in":"query","schema":{"type":"string","enum":["incoming","outgoing"]}},{"name":"address","in":"query","schema":{"type":"string"},"description":"Filter by matched address"},{"name":"webhookSent","in":"query","schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"List"}}}},"/api/account":{"post":{"tags":["account"],"summary":"Create or fetch an account (idempotent on externalId)","description":"Create a customer account with **your own** unique `externalId`. We mint one permanent deposit address per chain (Tron + every enabled EVM chain) and return the full address set. Calling again with the same `externalId` returns the EXISTING account — never creates a duplicate.\n\nThis endpoint is server-to-server only — your customer never calls it. Typically you call it the moment a new user signs up on your platform, then store the returned `accountId` against their user row.\n\n`externalId` rules:\n- Unique **per-merchant** (different merchants can use the same value)\n- URL-safe — only `A-Z 0-9 . - _` (we use it in path params)\n- Max 255 chars","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAccountRequest"},"example":{"externalId":"user_42","email":"user@example.com","name":"John Doe","metadata":{"tier":"gold"}}}}},"responses":{"200":{"description":"Idempotent replay — existing account returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}},"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST https://api.cryptrumpay.com/api/account \\\n  -H \"X-Api-Key: $CRYPTRUM_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"externalId\":\"user_42\",\"email\":\"user@example.com\",\"name\":\"John Doe\"}'"}]}},"/api/account/{externalId}":{"get":{"tags":["account"],"summary":"Get account + permanent deposit addresses","description":"Returns the same shape as the POST response. Use this when you need to re-fetch addresses (e.g. you lost them) or check whether an account exists.","parameters":[{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/account/{externalId}/payment-methods":{"get":{"tags":["account"],"summary":"List payment methods (chain + token + deposit address) for a \"Deposit page\" UI","description":"Returns one row per (chain, token) that THIS merchant currently accepts, with the address to send to. Result is the intersection of:\n\n1. **Admin-enabled tokens** (`TokenRegistry.isActive = true`)\n2. **Merchant's overrides** — the merchant can disable any token from their Wallet → Coin detail → \"Coin active\" toggle. Disabled tokens are hidden here.\n\n> 💡 If you instead want the PLATFORM-WIDE list (ignoring per-merchant overrides) for general discovery, call `GET /api/public/tokens`.","parameters":[{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPaymentMethodsResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/account/{externalId}/deposits":{"get":{"tags":["account"],"summary":"List deposits (paginated, filterable)","description":"Every inbound on-chain transfer to this account becomes a deposit row. Sorted by `detectedAt` desc. Each row includes the multi-currency fiat snapshot frozen at detection and a webhook delivery summary.","parameters":[{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["detected","confirming","confirmed","swept","failed"]}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"minimum":1}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"maximum":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDepositListResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/account/{externalId}/deposits/latest":{"get":{"tags":["account"],"summary":"Get the most recent deposit (missed-webhook recovery)","description":"Convenience endpoint when your server was down and you want to catch up. Returns the single most recent deposit (same shape as a list row) or `null` if the account has never received funds.","parameters":[{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK (deposit may be null)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDepositLatestResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/account/{externalId}/deposits/{depositId}":{"get":{"tags":["account"],"summary":"Get one deposit + full per-attempt webhook audit","description":"Returns the deposit detail PLUS the full `webhookAttempts[]` audit trail — every delivery attempt (delivered / retry_scheduled / exhausted), HTTP status, error message, and next retry timestamp. Use this when reconciling a specific deposit to prove what the webhook did.","parameters":[{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}},{"name":"depositId","in":"path","required":true,"schema":{"type":"string","example":"dep_a1b2c3d4e5f6"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDepositDetailResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}}},"webhooks":{"payment.detected":{"post":{"tags":["webhooks"],"summary":"payment.detected — first sighting of a deposit tx for this payment (0 confirmations)","description":"Fired when the scanner first sees a transaction sending the expected token to the payment's deposit address. `data.confirmations` will be 0 or 1. See the [webhooks](#tag/webhooks) tag for headers, signing, and verification snippets.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPaymentEvent"}}}},"responses":{"200":{"description":"Return 2xx within `WEBHOOK_TIMEOUT` (default 10s) to ack."}}}},"payment.confirming":{"post":{"tags":["webhooks"],"summary":"payment.confirming — tx has confirmations but not yet `requiredConfirmations`","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPaymentEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"payment.paid":{"post":{"tags":["webhooks"],"summary":"payment.paid — confirmations reached threshold, received amount = expected amount","description":"Terminal happy-path event. Safe to fulfil the order when this arrives.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPaymentEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"payment.overpaid":{"post":{"tags":["webhooks"],"summary":"payment.overpaid — confirmations reached threshold, received > expected","description":"Treat the order as paid; the overage is parked for admin resolution.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPaymentEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"payment.refunded":{"post":{"tags":["webhooks"],"summary":"payment.refunded — refund payout settled for this payment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPaymentEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"payment.rate_recalculated":{"post":{"tags":["webhooks"],"summary":"payment.rate_recalculated — fiat-mode payment had its locked exchange rate refreshed","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPaymentEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"payment.expired":{"post":{"tags":["webhooks"],"summary":"payment.expired — passed `expiresAt` with no deposit (or only an underpayment)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPaymentEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"payout.completed":{"post":{"tags":["webhooks"],"summary":"payout.completed — a payout to an end-user settled on-chain","description":"Fires per row for single payouts and once per row for batch payouts. `data.txHash` is the on-chain hash.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayoutEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"payout.failed":{"post":{"tags":["webhooks"],"summary":"payout.failed — payout broadcast failed permanently after gateway-worker retries","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayoutEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"withdrawal.completed":{"post":{"tags":["webhooks"],"summary":"withdrawal.completed — dashboard-initiated withdrawal settled on-chain","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookWithdrawalEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"withdrawal.failed":{"post":{"tags":["webhooks"],"summary":"withdrawal.failed — dashboard-initiated withdrawal failed permanently","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookWithdrawalEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"account.deposit.received":{"post":{"tags":["webhooks"],"summary":"account.deposit.received — first detection of a deposit to an account address (status=detected)","description":"Phase 26 [Account API](#tag/account). Fires the moment the scanner sees a transaction sending into one of the account's permanent deposit addresses. `data.confirmations` will be 0 or 1; the customer has not yet had their balance credited. The companion `account.deposit.confirmed` event fires once confirmations reach `requiredConfirmations`.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookAccountDepositEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}},"account.deposit.confirmed":{"post":{"tags":["webhooks"],"summary":"account.deposit.confirmed — confirmations reached threshold; balance credited","description":"Phase 26 [Account API](#tag/account). Terminal-customer-facing event for an account deposit. By the time you receive this, the account balance has been incremented atomically in our DB. The deposit is also enqueued for instant on-chain sweep to your merchant main wallet (no separate sweep event fires — internal).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookAccountDepositEvent"}}}},"responses":{"200":{"description":"2xx to ack"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Per-project key for the Payment Gateway. Get one from the merchant dashboard (Projects → API keys)."},"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Merchant dashboard JWT — used internally by the official merchant-panel UI. You do NOT need this for server-side integration; use X-Api-Key instead. Listed here only because a couple of merchant-dashboard endpoints (create-webhook, list-webhooks) accept it."},"TrackerTokenAuth":{"type":"apiKey","in":"header","name":"X-Auth-Token","description":"Per-webhook auth for the Address Tracker. Returned ONCE on POST /api/webhook/create."}},"schemas":{"QuoteRequest":{"type":"object","required":["chain","token","amount"],"properties":{"chain":{"type":"string","enum":["tron","ethereum","bsc","polygon"]},"token":{"type":"string","example":"USDT"},"pricingMode":{"type":"string","enum":["direct_crypto","fiat"],"default":"direct_crypto"},"amount":{"type":"string","example":"50.00"},"fiatCurrency":{"type":"string","example":"usd","description":"Required when pricingMode=fiat"}}},"QuoteResponse":{"type":"object","properties":{"success":{"type":"boolean"},"quote":{"type":"object","properties":{"chain":{"type":"string"},"token":{"type":"string"},"decimals":{"type":"integer"},"pricingMode":{"type":"string"},"fiatCurrency":{"type":"string"},"rate":{"type":"number","nullable":true},"expectedAmount":{"type":"string"},"expectedAmountRaw":{"type":"string"},"validUntil":{"type":"string","format":"date-time","nullable":true}}}}},"CreatePaymentRequest":{"type":"object","required":["chain","token","amount","customerName","customerEmail"],"properties":{"chain":{"type":"string","enum":["tron","ethereum","bsc","polygon"]},"token":{"type":"string","example":"USDT"},"pricingMode":{"type":"string","enum":["direct_crypto","fiat"],"default":"direct_crypto"},"amount":{"type":"string","example":"50.00"},"fiatCurrency":{"type":"string","example":"usd"},"orderId":{"type":"string","example":"ORDER-12345"},"customerName":{"type":"string","maxLength":255,"example":"John Doe","description":"Customer name (merchant-supplied). Required. Returned only on merchant-authenticated GET /api/payment/:id, never exposed via the public checkout endpoint or webhook payload."},"customerEmail":{"type":"string","format":"email","maxLength":255,"example":"customer@example.com","description":"Customer email. Required. A payment-received notification is dispatched on terminal success (paid/overpaid). Returned only on merchant-authenticated GET /api/payment/:id; redacted from public checkout responses and webhook payloads."},"webhookUrl":{"type":"string","example":"https://your-server.com/webhook"},"successUrl":{"type":"string","example":"https://yoursite.com/order/12345/thank-you","description":"Browser redirect target after payment succeeds (paid/overpaid). The checkout page appends ?paymentId=...&publicId=...&orderId=...&chain=...&token=...&amount=...&status=paid to this URL. Must be https (or http on localhost). Max 500 chars. UX only — verify the actual payment status via the signed webhook or GET /api/payment/:id; do not trust the query string."},"cancelUrl":{"type":"string","example":"https://yoursite.com/order/12345/cancelled","description":"Browser redirect target when the customer cancels OR the payment terminally fails. Receives the same query params as successUrl, plus reason=user_cancelled|expired|underpaid|failed|refunded. Same security caveat: UX only, verify server-side."},"expiresInMinutes":{"type":"integer","minimum":1,"maximum":1440,"default":15},"metadata":{"type":"object"},"idempotencyKey":{"type":"string","description":"Alt to Idempotency-Key header"}}},"PaymentResponse":{"type":"object","properties":{"success":{"type":"boolean"},"idempotent":{"type":"boolean","description":"true when this was a retry of a prior create"},"payment":{"$ref":"#/components/schemas/Payment"},"checkoutUrl":{"type":"string"}}},"PaymentList":{"type":"object","properties":{"success":{"type":"boolean"},"total":{"type":"integer"},"page":{"type":"integer"},"payments":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}}}},"Payment":{"type":"object","description":"Merchant-authenticated projection (merchantProjection). Includes customer PII. Returned by POST /api/payment and GET /api/payment/:paymentId — never by the public checkout endpoint.","properties":{"paymentId":{"type":"string"},"publicId":{"type":"string"},"chain":{"type":"string"},"token":{"type":"string"},"contractAddress":{"type":"string","nullable":true},"tokenDecimals":{"type":"integer"},"pricingMode":{"type":"string"},"fiatCurrency":{"type":"string","nullable":true},"fiatAmount":{"type":"string","nullable":true},"exchangeRate":{"type":"string","nullable":true},"expectedAmount":{"type":"string"},"expectedAmountRaw":{"type":"string"},"receivedAmount":{"type":"string"},"remainingAmount":{"type":"string","description":"expectedAmount minus receivedAmount, floored at 0. Useful for partial-payment UIs."},"deposits":{"type":"array","description":"Per-deposit audit trail (every incoming tx detected for this payment).","items":{"type":"object","properties":{"txHash":{"type":"string"},"amount":{"type":"string"},"amountRaw":{"type":"string"},"blockNumber":{"type":"string","nullable":true},"confirmations":{"type":"integer"},"detectedAt":{"type":"string","format":"date-time"},"fromAddress":{"type":"string","nullable":true}}}},"status":{"type":"string","enum":["pending","detected","confirming","paid","underpaid","overpaid","expired","failed","refunded"]},"confirmations":{"type":"integer"},"requiredConfirmations":{"type":"integer"},"depositAddress":{"type":"string","nullable":true},"expiresAt":{"type":"string","format":"date-time"},"paidAt":{"type":"string","format":"date-time","nullable":true},"orderId":{"type":"string","nullable":true},"customerName":{"type":"string","nullable":true,"description":"PII — only present when merchant supplied at create time. Never exposed via the public checkout endpoint or webhook payload."},"customerEmail":{"type":"string","format":"email","nullable":true,"description":"PII — only present when merchant supplied at create time. Drives the payment_received customer email on terminal success. Never exposed via the public checkout endpoint or webhook payload."},"successUrl":{"type":"string","nullable":true},"cancelUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"CustomerPayment":{"type":"object","description":"Unauthenticated public checkout projection (customerProjection). Subset of Payment with customerName/customerEmail intentionally redacted. Returned by GET /api/payment/public/:publicId.","properties":{"paymentId":{"type":"string"},"publicId":{"type":"string"},"orderId":{"type":"string","nullable":true},"chain":{"type":"string"},"token":{"type":"string"},"depositAddress":{"type":"string","nullable":true},"expectedAmount":{"type":"string"},"receivedAmount":{"type":"string"},"remainingAmount":{"type":"string"},"deposits":{"type":"array","items":{"type":"object","properties":{"txHash":{"type":"string"},"amount":{"type":"string"},"amountRaw":{"type":"string"},"blockNumber":{"type":"string","nullable":true},"confirmations":{"type":"integer"},"detectedAt":{"type":"string","format":"date-time"},"fromAddress":{"type":"string","nullable":true}}}},"status":{"type":"string","enum":["pending","detected","confirming","paid","underpaid","overpaid","expired","failed","refunded"]},"confirmations":{"type":"integer"},"requiredConfirmations":{"type":"integer"},"expiresAt":{"type":"string","format":"date-time"},"paidAt":{"type":"string","format":"date-time","nullable":true},"successUrl":{"type":"string","nullable":true},"cancelUrl":{"type":"string","nullable":true}}},"PayoutRequest":{"type":"object","required":["chain","token","toAddress"],"description":"Normal payout: provide `amount`. Phase 28 payout-with-swap: provide `srcChain` + `srcToken` + `srcAmount` instead of `amount`.","properties":{"chain":{"type":"string","description":"Destination chain (recipient lands here)"},"token":{"type":"string","description":"Destination token (recipient receives this)"},"toAddress":{"type":"string"},"amount":{"type":"string","example":"100.00","description":"Exact dst-token amount. Mutually exclusive with srcAmount."},"srcChain":{"type":"string","description":"Source chain — funds come from here. Requires payoutSwapEnabled."},"srcToken":{"type":"string","description":"Source token. Both srcToken AND token must be in your enabled token list."},"srcAmount":{"type":"string","description":"Exact INPUT amount the merchant spends. Recipient receives whatever conversion yields (bounded by slippage)."},"slippage":{"type":"number","minimum":0,"maximum":50,"default":1,"description":"Slippage tolerance in percent (defaults to admin-configured value, usually 1%)."}}},"PayoutBatchRequest":{"type":"object","required":["chain","token","destinations"],"properties":{"chain":{"type":"string"},"token":{"type":"string"},"destinations":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["toAddress","amount"],"properties":{"toAddress":{"type":"string"},"amount":{"type":"string"},"idempotencyKey":{"type":"string"}}}}}},"ErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"}}},"PublicChain":{"type":"object","properties":{"chain":{"type":"string","example":"ethereum","description":"Canonical chain id — pass this as the `chain` field on `POST /api/payment`."},"name":{"type":"string","example":"Ethereum"},"nativeSymbol":{"type":"string","example":"ETH"},"chainIdNum":{"type":"integer","nullable":true,"example":1,"description":"EIP-155 numeric chain id (null for TRON)."},"isEvm":{"type":"boolean","example":true},"defaultConfirmations":{"type":"integer","example":12,"description":"Confirmations required by default before `payment.paid` is emitted."}}},"PublicChainList":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"chains":{"type":"array","items":{"$ref":"#/components/schemas/PublicChain"}}}},"PublicToken":{"type":"object","properties":{"chain":{"type":"string","example":"ethereum"},"symbol":{"type":"string","example":"USDT","description":"Pass this as the `token` field on `POST /api/payment`."},"contractAddress":{"type":"string","nullable":true,"example":"0xdac17f958d2ee523a2206206994597c13d831ec7","description":"null for native tokens (TRX, ETH, BNB, MATIC)."},"decimals":{"type":"integer","example":6},"isNative":{"type":"boolean","example":false},"label":{"type":"string","example":"Tether USD","description":"Human display name suitable for UI dropdowns."}}},"PublicTokenList":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"tokens":{"type":"array","items":{"$ref":"#/components/schemas/PublicToken"}}}},"CreateWebhookRequest":{"type":"object","required":["url","name"],"properties":{"url":{"type":"string","format":"uri","example":"https://your-server.com/tracker-events"},"name":{"type":"string","example":"Hot wallet monitor"},"chain":{"type":"string","enum":["tron","ethereum","bsc","polygon"],"default":"tron"},"projectId":{"type":"string","description":"Optional — defaults to the first active project of the caller."},"merchantId":{"type":"string","description":"Admin-mode only — pick the merchant whose project receives this webhook."},"metadata":{"type":"object"}}},"CreateWebhookResponse":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"},"webhook":{"$ref":"#/components/schemas/Webhook"},"credentials":{"type":"object","description":"Shown ONCE — never retrievable later. Send `authToken` as `X-Auth-Token` on every per-webhook call. Use `secretKey` to verify HMAC signatures on incoming event POSTs.","properties":{"authToken":{"type":"string","example":"4f2e8d..."},"secretKey":{"type":"string","example":"9a3c7b..."}}}}},"Webhook":{"type":"object","properties":{"webhookId":{"type":"string","example":"wh_lzx2m1a4b7c8d9e0"},"name":{"type":"string"},"url":{"type":"string","format":"uri"},"chain":{"type":"string","enum":["tron","ethereum","bsc","polygon"]},"isActive":{"type":"boolean"},"maxAddresses":{"type":"integer"},"startBlock":{"type":"integer"},"projectId":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"CreateAccountRequest":{"type":"object","required":["externalId"],"properties":{"externalId":{"type":"string","maxLength":255,"pattern":"^[A-Za-z0-9._-]+$","description":"YOUR unique id for this customer (your DB primary key, opaque to Cryptrum). Unique per-merchant.","example":"user_42"},"email":{"type":"string","format":"email","nullable":true,"example":"user@example.com"},"name":{"type":"string","maxLength":255,"nullable":true,"example":"John Doe"},"metadata":{"type":"object","additionalProperties":true,"nullable":true,"description":"Arbitrary key/value bag. Returned verbatim in get/list responses.","example":{"tier":"gold","referrer":"instagram"}}}},"AccountAddress":{"type":"object","properties":{"chain":{"type":"string","enum":["tron","ethereum","bsc","polygon","arbitrum","base"],"example":"tron"},"address":{"type":"string","example":"TUDsCm5BCYAtxxxxxxxxxxxxxxx"},"derivationIndex":{"type":"integer","description":"HD index used to derive this address (>= 1_000_000 by design).","example":1000042}}},"Account":{"type":"object","properties":{"accountId":{"type":"string","example":"acc_lzx2m1a4b7c8d9e0"},"externalId":{"type":"string","example":"user_42"},"email":{"type":"string","nullable":true,"example":"user@example.com"},"name":{"type":"string","nullable":true,"example":"John Doe"},"metadata":{"type":"object","additionalProperties":true,"nullable":true},"createdAt":{"type":"string","format":"date-time"},"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AccountAddress"}}}},"AccountResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"account":{"$ref":"#/components/schemas/Account"}}},"AccountPaymentMethod":{"type":"object","properties":{"chain":{"type":"string","example":"tron"},"token":{"type":"string","example":"USDT"},"address":{"type":"string","example":"TUDsCm5BCYAtxxxxxxxxxxxxxxx"},"contractAddress":{"type":"string","nullable":true,"example":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"},"decimals":{"type":"integer","example":6},"isNative":{"type":"boolean","example":false},"displayName":{"type":"string","example":"Tether USD (TRC20)"},"iconUrl":{"type":"string","nullable":true,"format":"uri"}}},"AccountPaymentMethodsResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"paymentMethods":{"type":"array","items":{"$ref":"#/components/schemas/AccountPaymentMethod"}}}},"FiatSnapshot":{"type":"object","description":"Single-currency snapshot. `amount` is the fiat value at detection time; `rate` is `1 token = N units of this fiat`.","properties":{"amount":{"type":"string","example":"10.50"},"rate":{"type":"string","example":"1.0001"}}},"FiatSnapshotMap":{"type":"object","description":"Multi-currency snapshot frozen at detection. Each key is a fiat code (ISO 4217 uppercase). The set of keys is driven by the platform's `SUPPORTED_FIATS` env (defaults to `usd,eur,gbp,inr` — same set as the admin Rate Sources page). Adding a new fiat needs only an env change; old rows keep their original keys. May be `null` or empty when every rate lookup failed at detection (rare).","additionalProperties":{"$ref":"#/components/schemas/FiatSnapshot"},"example":{"USD":{"amount":"10.50","rate":"1.0001"},"EUR":{"amount":"9.65","rate":"0.921"},"GBP":{"amount":"8.30","rate":"0.792"},"INR":{"amount":"875.20","rate":"83.40"}}},"AccountDepositWebhookSummary":{"type":"object","properties":{"sent":{"type":"boolean"},"attempts":{"type":"integer"},"lastStatus":{"type":"string","nullable":true,"enum":["delivered","retry_scheduled","exhausted",null]},"lastAttemptAt":{"type":"string","format":"date-time","nullable":true},"lastError":{"type":"string","nullable":true}}},"AccountDeposit":{"type":"object","description":"Single account deposit row. Fiat snapshot is FROZEN at first detection — never recomputed even if the price moves before confirmation.","properties":{"depositId":{"type":"string","example":"dep_a1b2c3d4e5f6"},"chain":{"type":"string","example":"tron"},"token":{"type":"string","example":"USDT"},"contractAddress":{"type":"string","nullable":true,"example":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"},"decimals":{"type":"integer","example":6},"amount":{"type":"string","example":"10.50"},"amountRaw":{"type":"string","example":"10500000"},"fiatSnapshots":{"allOf":[{"$ref":"#/components/schemas/FiatSnapshotMap"}],"nullable":true},"fiatAt":{"type":"string","format":"date-time","nullable":true,"description":"When the fiat snapshot was captured (usually within ~100ms of `detectedAt`)."},"fromAddress":{"type":"string","nullable":true,"example":"TXyz1234567890abc"},"toAddress":{"type":"string","example":"TUDsCm5BCYAtxxxxxxxxxxxxxxx"},"txHash":{"type":"string","example":"f3c8e9a1b2c3d4e5f6..."},"blockNumber":{"type":"string","nullable":true,"example":"63245678"},"confirmations":{"type":"integer","example":28},"requiredConfirmations":{"type":"integer","example":19},"status":{"type":"string","enum":["detected","confirming","confirmed","swept","failed"]},"sweepTxHash":{"type":"string","nullable":true,"description":"On-chain hash of the sweep tx that moved this deposit to your main wallet. Null until swept."},"sweptAt":{"type":"string","format":"date-time","nullable":true},"detectedAt":{"type":"string","format":"date-time"},"confirmedAt":{"type":"string","format":"date-time","nullable":true},"webhook":{"$ref":"#/components/schemas/AccountDepositWebhookSummary"}}},"AccountDepositListResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"deposits":{"type":"array","items":{"$ref":"#/components/schemas/AccountDeposit"}},"total":{"type":"integer","example":47},"page":{"type":"integer","example":1},"limit":{"type":"integer","example":25},"pages":{"type":"integer","example":2}}},"AccountDepositLatestResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"deposit":{"allOf":[{"$ref":"#/components/schemas/AccountDeposit"}],"nullable":true}}},"AccountDepositWebhookAttempt":{"type":"object","description":"One delivery attempt against this deposit. Persisted to `account_deposit_webhook_attempts` from inside the worker loop, so retries that exhausted are still here for audit.","properties":{"attemptNumber":{"type":"integer","example":1},"attemptedAt":{"type":"string","format":"date-time"},"httpStatus":{"type":"integer","nullable":true,"example":200},"errorMessage":{"type":"string","nullable":true},"outcome":{"type":"string","enum":["delivered","retry_scheduled","exhausted"]},"nextRetryAt":{"type":"string","format":"date-time","nullable":true}}},"AccountDepositDetailResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"deposit":{"allOf":[{"$ref":"#/components/schemas/AccountDeposit"},{"type":"object","properties":{"webhookAttempts":{"type":"array","items":{"$ref":"#/components/schemas/AccountDepositWebhookAttempt"}}}}]}}},"SwapQuoteRequest":{"type":"object","required":["srcChain","srcToken","srcAmountRaw","destToken"],"properties":{"srcChain":{"type":"string","enum":["ethereum","bsc","polygon","arbitrum","base","optimism","avalanche"],"example":"ethereum"},"srcToken":{"type":"string","example":"USDT"},"srcAmountRaw":{"type":"string","description":"Source amount in raw atomic units (e.g. 100000000 = 100 USDT @ 6 decimals)","example":"100000000"},"destToken":{"type":"string","example":"USDC"}}},"SwapExecuteRequest":{"allOf":[{"$ref":"#/components/schemas/SwapQuoteRequest"},{"type":"object","properties":{"slippage":{"type":"number","minimum":0,"maximum":50,"default":1,"description":"Percent. Defaults to admin-configured value if omitted."}}}]},"BridgeQuoteRequest":{"type":"object","required":["srcChain","srcToken","srcAmountRaw","destChain","destToken"],"properties":{"srcChain":{"type":"string","enum":["ethereum","bsc","polygon","arbitrum","base","optimism","avalanche"]},"srcToken":{"type":"string","example":"USDC"},"srcAmountRaw":{"type":"string","example":"100000000"},"destChain":{"type":"string","enum":["ethereum","bsc","polygon","arbitrum","base","optimism","avalanche"],"example":"polygon"},"destToken":{"type":"string","example":"USDC"}}},"BridgeExecuteRequest":{"allOf":[{"$ref":"#/components/schemas/BridgeQuoteRequest"},{"type":"object","properties":{"slippage":{"type":"number","minimum":0,"maximum":50,"default":1}}}]},"SwapBridgeQuoteResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"quote":{"type":"object","additionalProperties":true,"description":"Provider-specific quote object — 1inch returns `{dstAmount, gas, protocols, ...}`; Across returns `{lpFee, relayerFee, expectedOutputAmount, ...}`. Shape varies by provider."}}},"SwapBridgeExecuteResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"txnId":{"type":"string","description":"Ledger row id (sbt_xxx). Track final status via `/history` filter `txnId=...` or just poll the ledger.","example":"sbt_a1b2c3"},"quote":{"type":"object","additionalProperties":true,"description":"Same provider-specific quote returned at execute time"}}},"SwapBridgeTransactionLedgerRow":{"type":"object","description":"A single swap/bridge ledger row as returned to the merchant (provider fee + receiver address fields HIDDEN — those are admin-only).","properties":{"txnId":{"type":"string","example":"sbt_a1b2c3"},"mode":{"type":"string","enum":["manual","auto","payout"]},"kind":{"type":"string","enum":["swap","bridge"]},"provider":{"type":"string","enum":["1inch","across"]},"sourceChain":{"type":"string"},"sourceToken":{"type":"string"},"sourceAmountRaw":{"type":"string"},"destChain":{"type":"string"},"destToken":{"type":"string"},"destAmountRaw":{"type":"string","nullable":true,"description":"Filled in by async cron once dest fill confirmed (bridges) or immediately on success (swaps)."},"destAddress":{"type":"string","description":"Recipient — merchant main wallet for manual mode, custom-address or main for auto-convert, external recipient for payout-with-swap."},"txHashes":{"type":"object","additionalProperties":true,"nullable":true,"description":"{sourceTxHash, destTxHash?, approvalTxHash?, depositId?}"},"status":{"type":"string","enum":["pending","succeeded","failed"]},"errorMessage":{"type":"string","nullable":true},"accountDepositId":{"type":"string","nullable":true,"description":"Set when mode=auto and triggered by an Account API deposit"},"paymentId":{"type":"string","nullable":true,"description":"Set when mode=auto and triggered by an invoicing Payment"},"payoutId":{"type":"string","nullable":true,"description":"Set when mode=payout"},"createdAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time","nullable":true}}},"SwapBridgeHistoryResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"transactions":{"type":"array","items":{"$ref":"#/components/schemas/SwapBridgeTransactionLedgerRow"}},"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"}}},"SwapBridgePlatformFeesResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"swapFeePercent":{"type":"number","description":"Platform fee deducted from 1inch swap output (0-3%)","example":0.3},"bridgeFeePercent":{"type":"number","description":"Platform fee deducted from Across bridge output (0-100%, but typically <1%)","example":0.5},"defaultSlippage":{"type":"number","description":"Default slippage tolerance in percent","example":1},"autoConvertMarkupPercent":{"type":"number","description":"CPT markup applied when auto-convert is enabled at collection (e.g. 10 = +10% sweep weight)","example":10}}},"WebhookPaymentEvent":{"type":"object","description":"Outbound webhook body for any `payment.*` event. The signature lives in the `X-Webhook-Signature` header; verify it against `${timestamp}.${rawBody}` — see the [webhooks](#tag/webhooks) tag.","properties":{"event":{"type":"string","example":"payment.paid"},"webhookId":{"type":"string","description":"Public `projectId` of the receiving project","example":"pr_8h2k4m9n"},"data":{"type":"object","properties":{"resource":{"type":"string","enum":["payment"]},"paymentId":{"type":"string","example":"pay_lzx2m1a4b7c8d9e0"},"publicId":{"type":"string","example":"pc_5b39f1a7c2e4abc123"},"orderId":{"type":"string","nullable":true,"example":"ORDER-12345"},"chain":{"type":"string","example":"tron"},"token":{"type":"string","example":"USDT"},"contractAddress":{"type":"string","nullable":true},"depositAddress":{"type":"string","nullable":true,"example":"TXyz1234567890abc"},"expectedAmount":{"type":"string","example":"49.985007"},"expectedAmountRaw":{"type":"string","example":"49985007"},"receivedAmount":{"type":"string","example":"49.985007"},"receivedAmountRaw":{"type":"string","example":"49985007"},"status":{"type":"string","enum":["pending","detected","confirming","paid","underpaid","overpaid","expired","failed","refunded"]},"confirmations":{"type":"integer","example":12},"requiredConfirmations":{"type":"integer","example":12},"txHash":{"type":"string","nullable":true},"fiatCurrency":{"type":"string","nullable":true,"example":"usd"},"fiatAmount":{"type":"string","nullable":true,"example":"50.00"},"exchangeRate":{"type":"string","nullable":true,"example":"1.0003"},"timestamp":{"type":"string","format":"date-time"}}}}},"WebhookPayoutEvent":{"type":"object","description":"Outbound webhook body for `payout.completed` / `payout.failed`.","properties":{"event":{"type":"string","example":"payout.completed"},"webhookId":{"type":"string","example":"pr_8h2k4m9n"},"data":{"type":"object","properties":{"resource":{"type":"string","enum":["payout"]},"payoutId":{"type":"string","example":"po_abc123"},"chain":{"type":"string","example":"tron"},"token":{"type":"string","example":"USDT"},"toAddress":{"type":"string","example":"TXyzRecipientAddressHere"},"amount":{"type":"string","example":"10.00"},"amountRaw":{"type":"string","example":"10000000"},"fee":{"type":"string","example":"0.15"},"status":{"type":"string","example":"completed"},"txHash":{"type":"string","nullable":true},"timestamp":{"type":"string","format":"date-time"}}}}},"WebhookWithdrawalEvent":{"type":"object","description":"Outbound webhook body for `withdrawal.completed` / `withdrawal.failed`.","properties":{"event":{"type":"string","example":"withdrawal.completed"},"webhookId":{"type":"string","example":"pr_8h2k4m9n"},"data":{"type":"object","properties":{"resource":{"type":"string","enum":["withdrawal"]},"withdrawalId":{"type":"string","example":"wd_abc123"},"chain":{"type":"string","example":"bsc"},"token":{"type":"string","example":"USDT"},"toAddress":{"type":"string","example":"0xDEADBEEF..."},"amount":{"type":"string","example":"500.00"},"amountRaw":{"type":"string","example":"500000000000000000000"},"status":{"type":"string","example":"completed"},"txHash":{"type":"string","nullable":true},"timestamp":{"type":"string","format":"date-time"}}}}},"RotateWebhookSecretResponse":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"},"webhookSecret":{"type":"string","description":"Raw 64-hex-char HMAC signing key. Shown ONCE. Use as the secret in `HMAC-SHA256(secret, timestamp + \".\" + JSON.stringify(body))`."},"rotatedAt":{"type":"string","format":"date-time"}}},"TestWebhookRequest":{"type":"object","required":["event"],"properties":{"event":{"type":"string","description":"Which lifecycle event to simulate. Must be one of the 10 supported events.","enum":["payment.detected","payment.confirming","payment.paid","payment.overpaid","payment.refunded","payment.rate_recalculated","payment.expired","payout.completed","payout.failed","withdrawal.completed","withdrawal.failed"]},"url":{"type":"string","format":"uri","description":"Optional one-off destination URL. Falls back to the project's saved `paymentWebhookUrl` if omitted. Useful for hitting a staging or ngrok endpoint without changing the project setting."}}},"TestWebhookResponse":{"type":"object","properties":{"success":{"type":"boolean"},"delivered":{"type":"boolean","description":"true if your endpoint responded with a 2xx status"},"statusCode":{"type":"integer","nullable":true,"description":"HTTP status returned by your endpoint, or null on network error"},"durationMs":{"type":"integer"},"signature":{"type":"string","description":"Exact value sent in X-Webhook-Signature"},"timestamp":{"type":"string","description":"Exact value sent in X-Webhook-Timestamp (epoch ms, stringified)"},"payload":{"type":"object","description":"Literal JSON body that was POSTed"},"responseBody":{"type":"string","nullable":true,"description":"First 500 chars of your endpoint's response body"},"error":{"type":"string","description":"Network-level error message if delivery threw before getting a response"}}},"WebhookAccountDepositEvent":{"type":"object","description":"Outbound webhook body for `account.deposit.received` and `account.deposit.confirmed` (Phase 26 Account API). Same `${timestamp}.${rawBody}` HMAC-SHA256 signing scheme as `payment.*` events.","properties":{"event":{"type":"string","enum":["account.deposit.received","account.deposit.confirmed"]},"webhookId":{"type":"string","description":"Public `projectId` of the receiving project (resolved via the deposit's merchant's oldest active project).","example":"pr_8h2k4m9n"},"data":{"type":"object","properties":{"resource":{"type":"string","enum":["account_deposit"]},"accountId":{"type":"string","example":"acc_lzx2m1a4b7c8d9e0"},"externalId":{"type":"string","description":"YOUR id for this customer — use it to look them up in your DB.","example":"user_42"},"depositId":{"type":"string","example":"dep_a1b2c3d4e5f6"},"chain":{"type":"string","example":"tron"},"token":{"type":"string","example":"USDT"},"contractAddress":{"type":"string","nullable":true,"example":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"},"amount":{"type":"string","example":"10.50"},"amountRaw":{"type":"string","example":"10500000"},"fiatSnapshots":{"allOf":[{"$ref":"#/components/schemas/FiatSnapshotMap"}],"nullable":true,"description":"Frozen at FIRST detection (not at confirmation event time). Use whichever fiat your accounting needs."},"fiatAt":{"type":"string","format":"date-time","nullable":true},"fromAddress":{"type":"string","nullable":true},"toAddress":{"type":"string","description":"The account's permanent address (same value for native + tokens on the same chain)."},"txHash":{"type":"string"},"blockNumber":{"type":"string","nullable":true,"example":"63245678"},"confirmations":{"type":"integer","example":28},"requiredConfirmations":{"type":"integer","example":19},"timestamp":{"type":"string","format":"date-time","description":"When this webhook was BUILT (not when the deposit was detected — that's `fiatAt`)."}},"required":["resource","accountId","externalId","depositId","chain","token","amount","amountRaw","toAddress","txHash","confirmations","requiredConfirmations","timestamp"]}},"required":["event","webhookId","data"]},"WebhookList":{"type":"object","properties":{"success":{"type":"boolean"},"webhooks":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","properties":{"addressCount":{"type":"integer"},"transactionCount":{"type":"integer"},"authToken":{"type":"string","description":"Decrypted authToken for this caller — use it as X-Auth-Token on subsequent /api/webhook/* calls."}}}]}}}}},"responses":{"BadRequest":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"PaymentRequired":{"description":"Payment Required — merchant CPT balance is zero. Top up via /api/merchant/billing/purchase.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}