Kaskade Payment Solution Docs
API Reference · v1

Kaskade Payment Solution API

The Kaskade Payment Solution API lets you accept and send cryptocurrency from your own app or backend. Price in USD, let customers pay in 23 coins across 14 networks, reconcile with signed webhooks, and pay out to external wallets — all behind one stable, JSON-over-HTTPS contract.

Availability tags. Live means callable today and frozen under V1 — build against it. Beta means live but still moving.Planned means not built: no endpoint, no committed shape, and nothing to integrate against yet. Every V1 endpoint on this page is Live; the only Planned items are the off-the-shelf platform plugins.

Infrastructure & stability

Kaskade Payment Solution is a contract-first API. The endpoints, fields and webhook events on this page are the product — the settlement engine behind them is an implementation detail that you never touch. Your keys are Kaskade Payment Solution keys, your objects are Kaskade Payment Solution objects, and your webhooks are signed with your Kaskade Payment Solution secret.

That separation is deliberate. Today settlement runs on a third-party custody provider; as volume grows we move to self-custody on an MPC platform (e.g. Fireblocks-class infrastructure) with our own on-chain wallets, treasury and payout signing. None of that changes a single request you make.The base URL, authentication, payment object, statuses and webhook payloads are guaranteed stable across that migration.

What is guaranteed stable
FieldTypeRequiredDescription
Base URL & versionrequiredhttps://kaskade.com/api/v1 stays; breaking changes only ever ship under a new version prefix.
AuthenticationrequiredBearer keys and HMAC signing are unaffected by backend changes.
Object shapesrequiredExisting fields are never removed or repurposed; we only add new optional fields.
Webhook eventsrequiredEvent names, payload shape and signature scheme stay constant.
StatusesrequiredThe payment status vocabulary is fixed (new states, if any, are additive).
Bottom line: integrate once. When we swap or in-source the crypto infrastructure, your code keeps working with no migration on your side.

Base URL & environments

All endpoints are served over HTTPS from one versioned base URL. Plain HTTP is rejected.

Base URL
https://kaskade.com/api/v1
Conventions
FieldTypeRequiredDescription
Content-TypeheaderrequiredAll request and response bodies are application/json.
AmountsnumberoptionalFiat amounts are decimal USD. Crypto amounts are decimal in the coin's own unit.
TimestampsstringoptionalISO-8601 in UTC, e.g. 2026-06-20T12:34:56.000Z.
IDsstringoptionalOpaque strings — never parse or assume a format.

Versioning & changelog

The current version is v1, pinned in the URL path. We treat the following as backward-compatible and may ship them without a version bump:

  • Adding new endpoints, optional request fields, or response fields.
  • Adding new webhook event types or new enum values.
  • Changing an endpoint's availability tag from Planned → Beta → Live.

Anything breaking (removing a field, changing a type) would only ship under a new prefix such as /api/v2, and v1 would keep running. Material changes are listed in the dashboard changelog.

Quick start

  1. Create an API key. In your dashboard, open Developers and create a key. Copy the secret — shown only once.
  2. Create a payment. POST a USD price and the coin to pay in; you get a deposit address and the exact crypto amount.
  3. Collect payment. Show the address (or our hosted page) to your customer.
  4. Fulfil on webhook. Act when you receive a payment.updated event with status finished.
curl -X POST https://kaskade.com/api/v1/payments \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "priceUsd": 50,
    "payCurrency": "btc",
    "orderId": "ORDER-123",
    "orderDescription": "Premium plan"
  }'

Which endpoint should I use?

Two endpoints create a request for money. They differ in who shows the payment page.

Both support the same two ways of saying how much, described in fiat mode vs coin mode below. Everything else — webhooks, statuses, settlement — is identical.

Pick a row

You wantCallBody
Hosted invoice — exactly 100 USDT on TRONPOST /invoices{ "payAmount": "100", "payCurrency": "usdttrc20" }
Hosted invoice — exactly 100 USDT on EthereumPOST /invoices{ "payAmount": "100", "payCurrency": "usdterc20" }
Direct payment — exactly 0.005 BTCPOST /payments{ "payAmount": "0.005", "payCurrency": "btc" }
Direct payment — exactly 0.25 ETHPOST /payments{ "payAmount": "0.25", "payCurrency": "eth" }
Hosted invoice — $100, customer pays in cryptoPOST /invoices{ "amountUsd": 100 } — add payCurrency to lock it to one coin
Direct payment — $50, customer pays in BTCPOST /payments{ "priceUsd": 50, "payCurrency": "btc" }
There is no POST /payment-links in this API. Payment Links are a dashboard feature for sharing a link without writing code. Over the API, the hosted equivalent is POST /invoices, which returns the same kind of shareable URL.

Fiat mode vs coin mode

Every request is priced in one of two things. The amount field you send isthe mode — there is no pricingMode to set.

Fiat-denominatedCoin-denominated
SendpriceUsd (payments)
amountUsd (invoices)
payAmount + payCurrency (both)
Example{ "amountUsd": 100, "payCurrency": "usdttrc20" }{ "payAmount": "100", "payCurrency": "usdttrc20" }
Priced inYour base currency. Kaskade calculates the coin amount from the live rate.The coin itself. You state the coin amount; no rate is involved.
Customer owesWhatever 100 USD is worth in that coin at quote time — about 100 USDT, not exactly 100.Exactly 100 USDT on TRON. Nothing added, nothing rounded.
TypeJSON number — 100Decimal string"100"
Payable inAny supported coin, unless you set payCurrency.That coin and network only — see same-asset rule.
Response says"pricingMode": "fiat""pricingMode": "crypto"
Send exactly one amount. priceUsd or payAmount on a payment; amountUsd or payAmount on an invoice. Sending both, or neither, is a validation_error — we will not guess which one you meant to charge.

USDT: always name the network

USDT is one name over several unrelated blockchains. A ticker here always encodes asset + network, so there is no separate network field to send — and "usdt" on its own is not valid, because it does not say which chain the money should arrive on.

You meanpayCurrencyNetwork
USDT on Tron (TRC-20)usdttrc20Tron
USDT on Ethereum (ERC-20)usdterc20Ethereum
USDT, network unspecifiedusdtrejected

usdttrc20 and usdterc20 are different rails and are never interchangeable. A TRC-20 request is only ever settled by USDT arriving on Tron, and an ERC-20 request only by USDT arriving on Ethereum; USDT sent on the other chain does not pay it. Get the exact spelling of any ticker from GET /currenciesrather than guessing.

Authentication

Every request must be authenticated. Two schemes grant the same access — use whichever fits your stack.

  • Bearer key — simplest; send your secret key in a header.
  • Request signing (HMAC) — strongest; sign each request so the key is never transmitted.

Bearer keys

Pass your key (ks_live_…) in the Authorization header. x-api-key: ks_live_… is also accepted.

Header
Authorization: Bearer ks_live_YOUR_API_KEY
Keep keys server-side. A live key can move money — never ship it in a browser, mobile app or public repo. Revoking and rotating are instant in the dashboard.

Request signing (HMAC)

Sign each request with your key's client secret; the secret never leaves your server. Enable a signing secret on any key in the dashboard — your Client ID is the key's Public ID.

Headers
FieldTypeRequiredDescription
X-Kaskade-ClientstringrequiredYour Client ID (the key's Public ID).
X-Kaskade-TimestampstringrequiredISO-8601 UTC time, within 5 minutes of server time (replay protection).
X-Kaskade-SignaturestringrequiredBase64 HMAC-SHA256 of the canonical string, keyed with your client secret.

Build the canonical string by concatenating six parts in order, with no separators:

canonical =
BOM + METHOD + fullURL + clientId + timestamp + rawBody
Parts
FieldTypeRequiredDescription
BOMconstantrequiredThe Unicode byte-order mark .
METHODstringrequiredUpper-case HTTP method, e.g. POST.
fullURLstringrequiredFull request URL as called, e.g. https://kaskade.com/api/v1/payments.
clientIdstringrequiredSame as the X-Kaskade-Client header.
timestampstringrequiredSame as the X-Kaskade-Timestamp header.
rawBodystringrequiredExact JSON body; empty string "" for GET.
import crypto from "crypto";

const clientId     = "YOUR_CLIENT_ID";   // = your key's Public ID
const clientSecret = "ks_sk_YOUR_CLIENT_SECRET";         // shown once when you enable signing

const method = "POST";
const url    = "https://kaskade.com/api/v1/payments";        // the full URL, exactly as called
const ts     = new Date().toISOString();
const body   = JSON.stringify({ priceUsd: 50, payCurrency: "btc", orderId: "ORDER-123" });

// canonical = BOM + METHOD + URL + clientId + timestamp + body
const canonical = "\ufeff" + method + url + clientId + ts + body;
const signature = crypto.createHmac("sha256", clientSecret)
  .update(canonical, "utf8").digest("base64");

const res = await fetch(url, {
  method,
  headers: {
    "Content-Type": "application/json",
    "X-Kaskade-Client": clientId,
    "X-Kaskade-Timestamp": ts,
    "X-Kaskade-Signature": signature,
  },
  body,
});

Errors & status codes

Conventional HTTP status codes; errors carry a human-readable error message. Most also carry a stable code to branch on, and a refusal caused by one input names it in field so you do not have to guess which value we rejected. Both are additive — read error alone if that is all you need.

Error response
{
  "error": "The invoice number is too long — 500 characters, and the most is 200.",
  "field": "orderId",
  "code": "validation_error"
}
CodeMeaning
200OK.
400Bad request — invalid params or below the coin's minimum.
401Unauthorized — missing, invalid, or unsigned credentials.
404Not found — resource doesn't exist or isn't yours.
409Conflict — idempotency key reused with a different body.
429Too many requests — slow down (see rate limits).
500Server error.
502Upstream settlement error — safe to retry.

Signing test vector

Unit-test your signer against these fixed values before you send a real request. All of it is fake — the secret is not a credential, and the client id is not an account.

Inputs
FieldTypeRequiredDescription
methodstringoptionalPOST
full URLstringoptionalhttps://kaskade.com/api/v1/payments
clientIdstringoptionalEXAMPLE-CLIENT-ID-0000-0000
timestampstringoptional2026-01-01T00:00:00.000Z
rawBodystringoptional{"priceUsd":50,"payCurrency":"usdttrc20","orderId":"ORDER-1"}
clientSecretstringoptionalks_sk_0000000000000000000000000000000000000000000000

The canonical string begins with a byte-order mark (U+FEFF) and contains no separators. Encode it as UTF-8 — it is 154 bytes for this vector, which is the quickest way to check you have the BOM.

Canonical string (JSON-escaped, so the BOM is visible)
"POSThttps://kaskade.com/api/v1/paymentsEXAMPLE-CLIENT-ID-0000-00002026-01-01T00:00:00.000Z{"priceUsd":50,"payCurrency":"usdttrc20","orderId":"ORDER-1"}"
Expected X-Kaskade-Signature
gu8s/nNUiG7u1vLDWfgQ94C8e91Wy6gw42MI2EHlkPU=

If your implementation produces this signature, it will authenticate. If it does not, the usual causes are a missing BOM, a re-serialized body (sign the exact bytes you send), or hex instead of base64.

Idempotency

To safely retry a POST without creating duplicates, send an Idempotency-Key header with a unique value (e.g. a UUID) per logical operation. We store the first response for 24 hours and replay it — byte for byte — for any retry with the same key and body. Replays carry Idempotent-Replay: true.

Header
Idempotency-Key: 9f1c8a3e-1b2d-4c5f-8a90-abc123def456

Supported on every POST: payments, invoices, payouts, payout destinations, players and both ramp endpoints. It is optional — omit it and nothing changes.

What you can get back
FieldTypeRequiredDescription
409 idempotency_conflicterroroptionalThe same key with a different body. Fix the key or the body — do not retry as-is.
409 idempotency_in_progresserroroptionalThe first attempt is still running. Retry shortly and you will get its response.
409 idempotency_recovery_requirederroroptionalA previous attempt with this key never finished and its outcome is unknown. Check whether the object exists (e.g. list your payments) before retrying with a new key. We will not re-run it, because re-running could create a second object.
503 idempotency_unavailableerroroptionalWe could not honour the key, so nothing was created. Retry the identical request — it is safe.

If you cannot send the header, dedupe on your side using orderId, which we echo back on the payment.

Pagination

List endpoints return the most recent items (currently up to 100, newest first). Cursor pagination via limit and starting_after is on the roadmap. The shape below is our current intent, not a promise — do not build against it until it ships:

Planned envelope
{
  "data": [ /* … items … */ ],
  "has_more": true,
  "next_cursor": "cmq7c0aa0000abc"
}

What you can rely on today: list responses return a plain array under a resource key (e.g. payments), newest first. That is the frozen V1 behaviour. A cursor envelope, if it ships, will be additive and will not change this response.

Rate limits

Kaskade does not currently apply a documented per-key rate limit, and the API returns no X-RateLimit-* headers. There is no published quota to design against, and we would rather say that than publish a number we do not enforce.

Infrastructure in front of the API (our reverse proxy, and whatever sits in front of it) may still shed load under abuse or attack, so a well-behaved client should handle 429 and503 defensively — retry with exponential backoff and jitter — even though neither is part of a contract today.

A per-key limit with published headers may be introduced later. It will be announced in the changelog before it is enforced.

Currencies

Prices are set in USD and converted at a live rate. Customers can pay in any of the 23 coins across 14 networks below. Pass a coin's ticker as payCurrencywhen creating a payment; network-specific tickers select the chain. The live list is always GET /currencies.

usdttrc20USDT · Tron (TRC-20)usdterc20USDT · Ethereum (ERC-20)usdtbscUSDT · BNB Chain (BEP-20)usdttonUSDT · TONusdcUSDC · Ethereum (ERC-20)usdctrc20USDC · Tron (TRC-20)usdcsolUSDC · SolanausdtpolygonUSDT0 · PolygonbtcBTC · BitcoinethETH · EthereumtrxTRX · TronbnbbscBNB · BNB Chain (BEP-20)solSOL · SolanapolPOL · Polygonshiberc20SHIB · Ethereum (ERC-20)xrpXRP · XRP LedgerltcLTC · LitecoindogeDOGE · DogecointonTON · TONadaADA · CardanodotDOT · PolkadotnearNEAR · NEARavaxAVAX · Avalanche C-Chain

Coming soon — not yet transactional

These tickers are reserved and their network identity is fixed, so you can plan against them. They are not accepted yet: a payment created with one of these as payCurrency is refused, no deposit address is issued, and they are absent from GET /currencies until the rail goes live. Do not send funds on these networks.

usdcpolygonUSDC · Polygon · coming soon

List currencies

GET/currenciesLive

Returns the coins your account can accept, with display name and network. For a coin's live minimum payable amount, call Get a currency.

This is where payCurrency values come from — do not guess coin names.The code of each entry is the canonical Kaskade ticker, and it encodes the network as well as the asset: that is why USDT appears as usdttrc20 (Tron) and usdterc20 (Ethereum) rather than as a bare usdt. Every ticker you send to POST /payments or POST /invoices should be a code from this list.

Metadata is not support. GET /currencies/:code answers for any ticker, including coins Kaskade does not accept and a few legacy identifiers (matic, usdcmatic, usdtsol, dai) that resolve to a friendly name only. Getting a name and a network back is not proof that deposits are enabled. The authoritative signals are enabled and depositAvailable — check one of those, never the presence of metadata.

curl https://kaskade.com/api/v1/currencies \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"
Response
{
  "currencies": [
    { "code": "btc",       "name": "Bitcoin",         "network": "bitcoin",  "enabled": true },
    { "code": "usdttrc20", "name": "Tether (TRC-20)", "network": "tron",     "enabled": true },
    { "code": "usdterc20", "name": "Tether (ERC-20)", "network": "ethereum", "enabled": true }
  ]
}

Get a currency

GET/currencies/:codeLive

Metadata for a single coin, including the current minimum payable amount in USD (the same minimum enforced by Create a payment).

curl https://kaskade.com/api/v1/currencies/btc \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"
Response
{
  "currency": { "code": "btc", "name": "Bitcoin", "network": "bitcoin", "minUsd": 18.92, "enabled": true }
}

Rates

Get a live conversion estimate before creating a payment — useful to preview the crypto amount a customer will owe for a given USD price.

Estimate a rate

GET/rates/estimateLive

Quotes how much of currency equals a given USD amount at the current rate.

Query parameters
FieldTypeRequiredDescription
amountnumberrequiredUSD amount to convert.
currencystringrequiredTarget coin ticker (e.g. btc).
curl https://kaskade.com/api/v1/rates/estimate?amount=50&currency=btc \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"
Response
{
  "amountUsd": 50,
  "currency": "btc",
  "estimatedAmount": 0.00052471,
  "rate": 95291.41
}

Fees

Kaskade Payment Solution charges a transparent percentage on successful payments. Your effective rate and fee mode (absorb vs. add-on) are configured per account in the dashboard.

Get fees

GET/feesLive

Returns the fee schedule that applies to your account.

curl https://kaskade.com/api/v1/fees \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"
Response
{
  "platformFeePercent": 1.5,
  "merchantExtraPercent": 0,
  "feeMode": "absorb",
  "currency": "USD"
}

The payment object

A payment is a single request for funds. It is priced either in your base currency — the coin amount is then quoted at the live rate — or in the coin itself, in which case the payer owes exactly the amount you named. pricingMode says which.

FieldTypeRequiredDescription
idstringoptionalKaskade Payment Solution payment id — use it to look the payment up later.
statusstringoptionalCurrent state. See statuses.
pricingModestringoptionalfiat — you named a price and the coin amount was quoted from it. crypto — you named the coin amount itself.
requestedAmountstring | nulloptionalIn coin mode, the exact amount you asked for, as a decimal string. Null in fiat mode.
requestedCurrencystring | nulloptionalIn coin mode, the coin the request was priced in. Null in fiat mode.
amountDueExactstring | nulloptionalThe amount due as a decimal string, in both modes. Same value as payAmount; use this behind a copy button.
priceUsdnumber | nulloptionalAmount charged in your base currency. Null in coin mode, where you named no fiat price.
payCurrencystringoptionalCoin the customer pays in.
payAmountnumberoptionalExact crypto amount due.
payAddressstringoptionalDeposit address to show the customer.
payExtraIdstring | nulloptionalDestination tag / memo. Non-null on chains that route by it (XRP, TON, XLM, …). You must show it to the customer alongside the address — a payment sent without it cannot be credited and the funds are not recoverable by us.
payInHashstring | nulloptionalOn-chain deposit transaction hash, once detected.
actuallyPaidnumberoptionalAmount received so far (handles underpayment).
orderIdstring | nulloptionalYour reference, echoed back.
expiresAtstring | nulloptionalWhen the payment request expires. In fiat mode this is also when the quoted rate lapses; in coin mode there is no rate to lapse — the amount does not move with the market.
createdAtstringoptionalISO-8601 creation time.

Payment statuses

StatusMeaning
waitingCreated; awaiting the customer's funds.
confirmingDeposit seen on-chain; awaiting confirmations.
confirmedConfirmed on-chain; settling.
sendingFunds routing to settlement.
partially_paidUnderpaid; remainder still pending. A fiat-priced payment allows a small shortfall (dust and wallet rounding) before it counts as paid; a coin-priced one does not — the exact amount is the threshold.
finishedSettled. Fulfil the order here.
failedPayment failed.
refundedPayment refunded.
expiredNo funds before the quote expired.

Create a payment

POST/paymentsLive

Creates a payment and returns a deposit address and the exact crypto amount.

Two ways to say what the payment is for. Send exactly one.

  • priceUsd — price it in your base currency. Kaskade quotes the coin amount at the live rate. This is the original behaviour and nothing about it has changed.
  • payAmount — price it in the coin itself, as a decimal string. The payer owes exactly that amount and no exchange rate is involved in producing it.

Sending both, or neither, is a validation_error.

Body parameters
FieldTypeRequiredDescription
priceUsdnumberoptionalFiat mode. Amount to charge in your base currency; the coin amount is quoted from it. Required unless payAmount is given.
payAmountstringoptionalCoin mode. Exact amount of payCurrency as a decimal string, e.g. "0.005". Must be a string, not a number. Required unless priceUsd is given.
payCurrencystringrequiredCoin to pay in (btc, eth, usdttrc20, usdc…). The network is part of the identifier.
orderIdstringoptionalYour order reference (≤ 200 chars). Echoed back and in webhooks.
orderDescriptionstringoptionalHuman-readable description (≤ 500 chars).
curl -X POST https://kaskade.com/api/v1/payments \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "priceUsd": 50,
    "payCurrency": "btc",
    "orderId": "ORDER-123",
    "orderDescription": "Premium plan"
  }'
Response
{
  "payment": {
    "id": "cmq7c1ab0000xyz",
    "npPaymentId": "4912345678",
    "priceUsd": 50,
    "payCurrency": "btc",
    "payAmount": 0.00052471,
    "payAddress": "bc1qexampleaddressxxxxxxxxxxxxxxxxxxxx",
    "payExtraId": null,
    "status": "waiting",
    "expiresAt": "2026-06-20T12:34:56.000Z",
    "pricingMode": "fiat",
    "requestedAmount": null,
    "requestedCurrency": null,
    "amountDueExact": "0.00052471"
  }
}

Pricing in the coin itself

Ask for a coin amount and that is exactly what the payer owes. Kaskade adds nothing to it: the platform fee comes out of your settlement, not out of the customer’s amount, whatever fee mode your account uses.

100 USDT on TRON (TRC-20)
{
  "payAmount": "100",
  "payCurrency": "usdttrc20",
  "orderId": "ORDER-1001"
}
100 USDT on Ethereum (ERC-20)
{
  "payAmount": "100",
  "payCurrency": "usdterc20",
  "orderId": "ORDER-1002"
}
0.005 BTC
{
  "payAmount": "0.005",
  "payCurrency": "btc",
  "orderId": "ORDER-1003"
}
0.25 ETH
{
  "payAmount": "0.25",
  "payCurrency": "eth",
  "orderId": "ORDER-1004"
}

The same request as a copy-paste call — 0.005 BTC, exactly:

curl -X POST https://kaskade.com/api/v1/payments \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payAmount": "0.005",
    "payCurrency": "btc",
    "orderId": "ORDER-1003"
  }'

The response carries the id, the payCurrency, the exact amountDueExact, the payAddress to send to, the status and the expiresAt deadline:

Response
{
  "payment": {
    "id": "cmq7c1ab0000xyz",
    "npPaymentId": "fb_9f2c1d4e5a6b7c8d",
    "pricingMode": "crypto",
    "priceUsd": null,
    "requestedAmount": "100",
    "requestedCurrency": "usdttrc20",
    "payCurrency": "usdttrc20",
    "payAmount": 100,
    "amountDueExact": "100",
    "payAddress": "TExampleAddressxxxxxxxxxxxxxxxxxxxxx",
    "payExtraId": null,
    "status": "waiting",
    "expiresAt": "2026-06-20T12:34:56.000Z"
  }
}

The network is part of the coin identifier

USDT exists on four networks here, so "usdt" on its own names nothing and is refused. Use the identifier from GET /currenciesusdttrc20 (Tron), usdterc20 (Ethereum), usdtbsc (BNB Chain), usdtton (TON).

Refused — ambiguous asset
{
  "payAmount": "100",
  "payCurrency": "usdt"
}

→ 400
{
  "error": "USDT cannot be used to price a request in the coin itself. Price this one in your base currency instead, or choose another coin from GET /v1/currencies.",
  "field": "payCurrency",
  "code": "unsupported_asset"
}

What a coin-denominated payment does and does not do

  • The payer owes the exact amount. 100 USDT means 100 USDT. Nothing is added on top and nothing is rounded off.
  • It settles exactly. The 1% shortfall tolerance that applies to a rate-quoted payment does not apply here — 99.999999 of 100 USDT stays partially_paid. Fiat-priced payments are unchanged.
  • It is payable only in the coin it is priced in. Kaskade does not convert between coins or across networks — see below.
  • Amounts are decimal strings, not numbers. A JSON number cannot carry a coin amount intact, so payAmount must be quoted.
  • Precision is the asset’s own, capped at 8 places in this release. USDT takes 6, BTC takes 8, and ETH — which carries 18 on chain — is accepted to 8 here. Anything finer is refused rather than silently rounded.
  • expiresAt is when the request lapses, not when a rate lapses: an exact coin amount has no rate to expire.
Refused — more precision than the asset carries
{
  "payAmount": "0.0000001",
  "payCurrency": "usdttrc20"
}

→ 400
{
  "error": "USDTTRC20 amounts are accepted to at most 6 decimal places here, which is the chain's own precision.",
  "field": "payAmount",
  "code": "validation_error"
}

Same asset, same network — this release converts nothing

A coin-denominated request is settled by that asset on that network and by nothing else. Kaskade has no conversion engine in V1.1, so there is no rate at which a different coin could be accepted.

Priced asPayable withNot payable with
100 usdttrc20100 USDT on TronUSDT on Ethereum, BNB Chain or TON; any other coin
100 usdterc20100 USDT on EthereumUSDT on Tron, BNB Chain or TON; any other coin
0.005 btc0.005 BTCUSDT, ETH or anything else

USDT that arrives on the wrong network is not lost — it is held in custody and an operator resolves it — but it does not settle the request, and the payment stays unpaid. Because the two USDT rails share a symbol and differ only in the ticker you send, this is the mistake worth guarding in your own integration: pass through the exact payCurrency you asked for rather than a display symbol.

Cross-asset and cross-network conversion is not part of V1.1. If you need a customer to pay in a coin of their choosing, price the request in fiat instead — that is what fiat mode is for.

List payments

GET/paymentsLive

Returns your most recent payments (up to 100), newest first.

curl https://kaskade.com/api/v1/payments \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"
Response
{
  "payments": [
    { "id": "cmq7c1ab0000xyz", "status": "finished", "priceAmount": 50, "payCurrency": "btc", "payInHash": "f4a1…", "createdAt": "2026-06-20T12:00:00.000Z" },
    { "id": "cmq7c0aa0000abc", "status": "waiting",  "priceAmount": 25, "payCurrency": "usdttrc20", "createdAt": "2026-06-20T11:40:00.000Z" }
  ]
}

Retrieve a payment

GET/payments/:idLive

Fetches one payment by id and refreshes its status live before returning, so it always reflects the latest on-chain state.

Path parameters
FieldTypeRequiredDescription
idstringrequiredThe payment id from Create a payment.
curl https://kaskade.com/api/v1/payments/cmq7c1ab0000xyz \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"
Response
{
  "payment": {
    "id": "cmq7c1ab0000xyz",
    "status": "finished",
    "priceAmount": 50,
    "payCurrency": "btc",
    "payAmount": 0.00052471,
    "actuallyPaid": 0.00052471,
    "payAddress": "bc1qexampleaddressxxxxxxxxxxxxxxxxxxxx",
    "payInHash": "f4a1c9b2…",
    "createdAt": "2026-06-20T12:00:00.000Z"
  }
}

The invoice object Live

An invoice is a hosted, shareable request for payment. It produces a checkout URL your customer opens to pay in any supported coin, and emits the same payment.updated webhooks under the hood.

FieldTypeRequiredDescription
idstringoptionalInvoice id.
statusstringoptionaldraft | open | paid | expired | void.
pricingModestringoptionalfiat | crypto. Which of the two amount fields carries the figure.
amountUsdnumber | nulloptionalFiat amount requested, in your base currency. Null on a coin-denominated invoice.
requestedAmountstring | nulloptionalCoin amount requested, as a decimal string. Null in fiat mode.
requestedCurrencystring | nulloptionalCoin the invoice is priced in, and the only coin it can be paid in. Null in fiat mode.
urlstringoptionalHosted checkout URL to share with the customer.
orderIdstring | nulloptionalYour reference.
customerEmailstring | nulloptionalOptional; receipt is emailed here.
paymentIdstring | nulloptionalLinked payment once the customer pays.
expiresAtstring | nulloptionalWhen the invoice expires.
createdAtstringoptionalISO-8601 creation time.

Create an invoice

POST/invoicesLive

Creates a hosted invoice and returns a shareable checkout URL. The invoice.url in the response is a page on kaskade.com that we render — give it to your customer and they pay there. Use this endpoint whenever you would rather not build a payment screen yourself; use POST /payments when you want the raw address and amount instead.

Price the invoice one of two ways. Send exactly one amount.

  • Fiat-denominated — amountUsd. The invoice is priced in your base currency, and Kaskade calculates the cryptocurrency amount due at the live rate when the customer picks a coin. { "amountUsd": 100, "payCurrency": "usdttrc20" } means $100 worth of USDT on Tron — roughly 100 USDT, not exactly 100. Add payCurrency to lock the invoice to one coin, or omit it to let the customer choose any supported coin.
  • Coin-denominated — payAmount + payCurrency. The invoice is priced directly in the coin, as a decimal string. No fiat conversion determines what the customer owes. { "payAmount": "100", "payCurrency": "usdttrc20" } means exactly 100 USDT on TRON, and it is payable in that asset on that network only.

Sending both amounts, or neither, is a validation_error. See fiat mode vs coin mode for the full comparison.

Body parameters
FieldTypeRequiredDescription
amountUsdnumberoptionalFiat mode. Amount to request in your base currency. Required unless payAmount is given.
payAmountstringoptionalCoin mode. Exact amount of payCurrency as a decimal string. Required unless amountUsd is given.
payCurrencystringoptionalFiat mode: optional, locks the invoice to one coin. Coin mode: REQUIRED — it is what the invoice is priced in, and the only coin it can be paid in.
orderIdstringoptionalYour order reference.
orderDescriptionstringoptionalShown on the checkout page.
customerEmailstringoptionalEmails a receipt and reminders.
curl -X POST https://kaskade.com/api/v1/invoices \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amountUsd": 120, "orderId": "INV-2026-014", "customerEmail": "buyer@example.com" }'
Response
{
  "invoice": {
    "id": "inv_8f2a1c",
    "status": "open",
    "amountUsd": 120,
    "url": "https://kaskade.com/i/8f2a1c",
    "orderId": "INV-2026-014",
    "customerEmail": "buyer@example.com",
    "expiresAt": "2026-06-21T12:00:00.000Z",
    "createdAt": "2026-06-20T12:00:00.000Z",
    "pricingMode": "fiat",
    "requestedAmount": null,
    "requestedCurrency": null
  }
}

A hosted invoice priced in the coin

The URL works the same way — share it and your customer pays the exact amount, in that coin only. This is the shortest route to a coin-denominated payment page over the API.

Exactly 100 USDT on TRON (TRC-20):

curl -X POST https://kaskade.com/api/v1/invoices \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payAmount": "100",
    "payCurrency": "usdttrc20",
    "orderId": "INV-1001",
    "orderDescription": "Order #1001",
    "customerEmail": "buyer@example.com"
  }'
Response
{
  "invoice": {
    "id": "inv_8f2a1d",
    "status": "open",
    "pricingMode": "crypto",
    "amountUsd": null,
    "requestedAmount": "100",
    "requestedCurrency": "usdttrc20",
    "payCurrency": "usdttrc20",
    "url": "https://kaskade.com/i/8f2a1d",
    "orderId": "INV-1001",
    "customerEmail": "buyer@example.com",
    "expiresAt": "2026-06-21T12:00:00.000Z",
    "createdAt": "2026-06-20T12:00:00.000Z"
  }
}

Exactly 100 USDT on Ethereum (ERC-20) — same call, one different ticker. This is a different blockchain, not a variant of the one above:

curl -X POST https://kaskade.com/api/v1/invoices \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payAmount": "100",
    "payCurrency": "usdterc20",
    "orderId": "INV-1002"
  }'
Response
{
  "invoice": {
    "id": "inv_8f2a1e",
    "status": "open",
    "pricingMode": "crypto",
    "amountUsd": null,
    "requestedAmount": "100",
    "requestedCurrency": "usdterc20",
    "payCurrency": "usdterc20",
    "url": "https://kaskade.com/i/8f2a1e",
    "orderId": "INV-1002",
    "customerEmail": null,
    "expiresAt": "2026-06-21T12:00:00.000Z",
    "createdAt": "2026-06-20T12:00:00.000Z"
  }
}

The same shape works for any coin with a recorded on-chain precision:

0.005 BTC
{
  "payAmount": "0.005",
  "payCurrency": "btc",
  "orderId": "INV-1003"
}
0.25 ETH
{
  "payAmount": "0.25",
  "payCurrency": "eth",
  "orderId": "INV-1004"
}
A coin-denominated invoice is payable in its own asset and network only. A usdttrc20 invoice is not settled by USDT sent on Ethereum, and a usdterc20invoice is not settled by USDT sent on Tron. See the same-asset rule. Get exact tickers from GET /currencies; "usdt" on its own is rejected.

List invoices

GET/invoicesLive

Returns your invoices, newest first.

curl https://kaskade.com/api/v1/invoices \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"

Retrieve an invoice

GET/invoices/:idLive

Fetch one invoice; its status reconciles from the linked payment before returning.

curl https://kaskade.com/api/v1/invoices/inv_8f2a1c \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"

Get balances

GET/balanceLive

Returns your available balance per coin.

curl https://kaskade.com/api/v1/balance \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"
Response
{
  "balances": {
    "btc": 0.01340000,
    "usdttrc20": 250.50
  }
}

Per-customer deposit addresses

Kaskade has no /wallets/addresses endpoint. An earlier version of this page published one as planned; it was never implemented, and it is not part of V1.

The shipped equivalent is Players: a permanent deposit address per customer, attributed to your own identifier. See that section for the real contract.

The payout object Live

A payout sends crypto from your balance to a destination you have already registered. Creating one is a request, not a transfer: it is reviewed and released before anything reaches the chain, so treat status as the only statement about where the money actually is.

FieldTypeRequiredDescription
idstringoptionalPayout id.
statusstringoptionalpending | approved | submitted | paid | rejected | failed. See below.
currencystringoptionalCoin/network sent.
amountnumberoptionalCrypto amount sent.
addressstringoptionalThe address the funds are sent to, resolved at request time. Present even when destinationId is null, which is how you see where a settlement-wallet payout actually went.
destinationIdstring | nulloptionalThe registered destination used, if any.
destinationstring | nulloptionalThat destination's label.
txHashstring | nulloptionalOn-chain transaction hash once broadcast.
errorstring | nulloptionalWhy it did not proceed, in terms you can act on. Null unless something went wrong.
referencestring | nulloptionalFree text you set when requesting the payout, stored and echoed back unchanged. Kaskade never parses it, and it is not an idempotency key — use Idempotency-Key for that.
createdAtstringoptionalISO-8601 creation time.
paidAtstring | nulloptionalISO-8601 completion time. Set only once status is paid.

Payout status

The normal path is pendingapprovedsubmitted paid. A payout can leave that path at any point for rejected or failed.

What each status means
FieldTypeRequiredDescription
pendingin progressoptionalThe request has been accepted and is waiting on the next step. The amount is already reserved against your balance.
approvedin progressoptionalCleared to go, but not yet handed to the network. Nothing is on chain.
submittedin progressoptionalHanded to the execution workflow and on its way to the chain. Still not confirmed.
paidterminal — successoptionalThe only status that means money moved. txHash and paidAt are set. Fulfil against this and nothing earlier.
rejectedterminaloptionalRefused and will not proceed. The reserved amount returns to your available balance.
failedterminaloptionalExecution failed. The reserved amount returns to your available balance; request again rather than retrying the same payout.

Terminal: paid, rejected and failed. A payout in any of those three will not change again, so you can stop polling it. Success is paid and only paid. There is no sent status — earlier revisions of this page listed one and no such value has ever been returned by the API.

Payout destinations

POST/payout-destinationsLive
GET/payout-destinationsLive

Where you are allowed to send funds: your own wallets, a supplier, an exchange account, cold storage. A payout names a destination by id — never a raw address — because an address has to be approved as a withdrawal destination before any transfer can reach it, and because an API key that could name any address would be a key that empties your balance.

Approval takes a person and is not instant. Watch whitelistStatus(pendingapproved); payable is the same answer as a boolean. Register destinations before you need to pay them.

Body parameters
FieldTypeRequiredDescription
labelstringrequiredWhat to call it. Shown to whoever approves the address.
currencystringrequiredCoin/network, e.g. usdttrc20. Echoed back as currency on the destination.
addressstringrequiredDestination address.
tagstringoptionalMemo / destination tag. Required on the chains that route by one.
curl -X POST https://kaskade.com/api/v1/payout-destinations \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "label": "Acme Supplies", "currency": "usdttrc20", "address": "TXdest…" }'
Response
{
  "destination": {
    "id": "pd_7a2f10",
    "label": "Acme Supplies",
    "currency": "usdttrc20",
    "address": "TXdest…",
    "tag": null,
    "whitelistStatus": "pending",
    "payable": false,
    "createdAt": "2026-08-19T12:00:00.000Z"
  }
}

Create a payout

POST/payoutsLive

Withdraws from your available balance to one of your approved destinations. OmitdestinationId to send to your settlement wallet for that coin.

Creating a payout moves nothing. It is a request: we release it, and the custody policy asks a person again after that. Follow it with the payout.updated webhook or by polling the payout — only paid means it is on chain, and failed means the amount is back in your available balance.

Body parameters
FieldTypeRequiredDescription
currencystringrequiredCoin/network to send.
amountnumberrequiredCrypto amount to send.
destinationIdstringoptionalAn approved destination. Defaults to your settlement wallet for the coin.
referencestringoptionalFree text for your own reconciliation (max 500 chars). Not an idempotency key.
Error codes
FieldTypeRequiredDescription
destination_pending409optionalThe destination is not approved yet.
destination_deleted409optionalThe destination is no longer registered for withdrawals. Add it again.
destination_disabled409optionalThe destination has been retired and is no longer available.
destination_not_authorized409optionalNobody on the account has authorised payouts to this destination yet.
destination_unavailable409optionalThe destination's approval could not be confirmed right now. Retry shortly.
insufficient_balance400optionalMore than the available balance, which already excludes payouts in flight.
no_wallet409optionalNo settlement wallet on file for that coin.
unknown_destination409optionalNo such destination on this account.
direct_settlement409optionalThis account settles straight to its own wallet, so there is no balance here.
payouts_restricted403optionalWithdrawals are restricted on this account.
kyc_required403optionalIdentity verification is not approved yet.
curl -X POST https://kaskade.com/api/v1/payouts \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "currency": "usdttrc20", "amount": 100, "destinationId": "pd_7a2f10", "reference": "WD-771" }'
Response
{
  "payout": {
    "id": "po_5c1a9d",
    "status": "pending",
    "currency": "usdttrc20",
    "amount": 100,
    "address": "TXdest…",
    "destinationId": "pd_7a2f10",
    "destination": "Acme Supplies",
    "txHash": null,
    "error": null,
    "reference": "WD-771",
    "createdAt": "2026-08-19T12:00:00.000Z",
    "paidAt": null
  }
}

List / retrieve payouts

GET/payoutsLive
GET/payouts/:idLive

Your payouts, newest first. Filter with ?status= (pending,approved, submitted, paid, failed,rejected) and cap with ?limit= (max 100).

curl https://kaskade.com/api/v1/payouts?status=submitted \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"

Webhooks

Set a webhook URL in your dashboard; we POST an event every time a payment changes status. This is the reliable way to know when to fulfil — never rely on the customer's browser returning.

POST to your URL
{
  "id": "evt_cmq7c1ab0000xyz_finished",
  "event": "payment.updated",
  "created": "2026-08-19T12:00:00.000Z",
  "data": {
    "payment": {
      "id": "cmq7c1ab0000xyz",
      "status": "finished",
      "orderId": "ORDER-123",
      "priceAmount": 50,
      "payCurrency": "btc",
      "payAmount": 0.001,
      "payInHash": "f4a1c9b2…",
      "pricingMode": "fiat",
      "requestedAmount": null,
      "requestedCurrency": null,
      "amountDueExact": "0.001"
    }
  }
}

Telling the two pricing modes apart. A coin-denominated payment carries pricingMode: "crypto" with requestedAmount and requestedCurrency set — so "100 USDT-TRON was requested and 100 USDT-TRON arrived" is distinguishable from "$100 was requested and roughly that much USDT arrived". In coin mode priceAmount is Kaskade's fiat reference valuation at creation, recorded for your reports; it is not what anyone was asked for. These fields appear on events raised from V1.1.0 onward; a redelivered older event replays its original bytes unchanged.

Every event uses this envelope: id, event, created, and the object nested under data. Read data.payment, data.invoice, data.payout or data.deposit — never a top-level resource key. id is stable per object and transition, which makes it the natural key for de-duplicating retries.

Event types

EventWhen
payment.updatedA payment changes status (incl. finished).Live
invoice.paidA hosted invoice is fully paid.Live
payout.updatedA payout changes status — pending, approved, submitted, paid, rejected or failed.Live
deposit.confirmedA player deposit is confirmed on chain. Casino-gated accounts only.Live

All events share the same envelope (event + resource object) and the same signature scheme, so one handler covers them all.

Verifying signatures

Each delivery carries an x-kaskade-signature header — HMAC-SHA256 (hex) of the raw request body, keyed with your webhook signing secret. Always verify before trusting the event.

import crypto from "crypto";

// Express raw-body handler for POST /webhooks/kaskade
app.post("/webhooks/kaskade", (req, res) => {
  const signature = req.headers["x-kaskade-signature"];
  const expected = crypto
    .createHmac("sha256", process.env.KASKADE_WEBHOOK_SECRET)
    .update(req.rawBody)            // the exact bytes we sent
    .digest("hex");

  if (signature !== expected) return res.status(401).end();

  const { event, payment } = JSON.parse(req.rawBody);
  if (payment.status === "finished") {
    // ✅ fulfil the order (idempotently)
  }
  res.status(200).end();
});

Delivery & retries

Respond 2xx quickly. Anything else counts as a failure.
What we actually do
FieldTypeRequiredDescription
Immediate attemptsup to 3optionalOne delivery plus two retries, with a short increasing backoff (~0.4s, then ~0.8s) and an 8-second per-attempt timeout.
Then, asynchronouslyup to 2 more roundsoptionalA delivery still unacknowledged is picked up again later — at least 15 minutes apart — and each round is itself up to 3 attempts.
Total attemptsup to 9optionalThree rounds of three. After that the delivery stays failed and is not retried automatically again.
Guaranteeat-least-onceoptionalDuplicates are expected and ordering is not guaranteed. Make your handler idempotent.
Manual re-sendavailableoptionalFailed deliveries are listed under Developers in the dashboard, where you can re-send one by hand at any time. A re-send is the same event again, not a new one.

Deduplicate on id. The envelope id identifies the logical event — a resource and the transition it made — and is the same on the first delivery, on every automatic retry and on a manual re-send. Record the ids you have processed and ignore one you have seen. There is no timestamp header and no replay window.

An event never changes. A retry re-sends the original event exactly as it was first raised — same id, same created, same data, byte for byte. If the underlying resource has moved on since (a payout that was submitted is now paid), that is a separate event with its own id, not a rewrite of this one. So two deliveries carrying the same id always carry the same body, and you can safely keep the first and discard the rest.

deposit.confirmed is delivered by the deposit watcher rather than the general worker: it is retried on each pass until your endpoint accepts it, so it is not bounded by the attempt ceiling above. Everything else on this page — envelope, signature, at-least-once, duplicates, ordering — is identical.

Players — permanent deposit addresses

A player is one of your customers with a permanent deposit address. Anything sent to it is attributed to that player and reported to you by webhook — the building block for pay-by-name and account-funding flows.

Requires the Players product on your account — otherwise every endpoint here returns 403. TRON only today. Do not assume multi-chain support; ask us before designing around another network.

Create a player

POST/playersLive

Idempotent by nature: calling it again with the same externalId returns the same player and the same address. An address, once issued, never changes.

Body parameters
FieldTypeRequiredDescription
externalIdstringrequiredYour identifier for the customer (≤ 200 chars).
namestringrequiredDisplay name shown on the deposit page (≤ 120 chars).
coinstringoptionalDeposit coin. Defaults to your account's player coin.
curl -X POST https://kaskade.com/api/v1/players \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c8a3e-1b2d-4c5f-8a90-abc123def456" \
  -d '{ "externalId": "user_4821", "name": "Jamie R" }'
Response
{
  "player": {
    "id": "plr_8f2c1",
    "externalId": "user_4821",
    "name": "Jamie R",
    "coin": "usdttrc20",
    "chain": "tron",
    "depositAddress": "TXexampleaddressxxxxxxxxxxxxxxxxxxx",
    "depositTag": null,
    "depositUrl": "https://kaskade.com/d/acme/jamie-r",
    "creditedTotal": 0,
    "createdAt": "2026-06-20T12:00:00.000Z"
  }
}

depositTag is null on TRON. On any chain that routes by memo it is mandatory — show it beside the address, because a deposit sent without it cannot be attributed.

List players

GET/playersLive

Most recent first, up to 200. No pagination parameters today.

curl https://kaskade.com/api/v1/players \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"

Retrieve a player

GET/players/:idLive
curl https://kaskade.com/api/v1/players/plr_8f2c1 \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"

Deposits fire the deposit.confirmed webhook once confirmed on chain. Verify its signature the same way as every other event.

Fiat ramp

Let a customer buy crypto with a card or bank transfer (on-ramp), or sell it back to fiat (off-ramp). Kaskade creates the order with our ramp partner and returns a hosted checkout URL to redirect the customer to; they complete KYC and payment there.

Availability depends on the customer's country and payment method, and on the ramp being enabled for the platform — otherwise these endpoints return 503. These create real orders: send an Idempotency-Key so a network retry cannot create a duplicate.

Create an on-ramp order

POST/ramp/onrampLive
Body parameters
FieldTypeRequiredDescription
priceUsdnumberrequiredAmount of fiat the customer will spend.
payCurrencystringoptionalCrypto to deliver. Defaults to your account's setting.
fiatstringoptional3-letter fiat code, e.g. USD.
externalCustomerIdstringoptionalYour stable customer id — improves KYC recognition on repeat orders.
redirectUrlstringoptionalWhere to send the customer when they finish.
orderIdstringoptionalYour order reference.
curl -X POST https://kaskade.com/api/v1/ramp/onramp \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c8a3e-1b2d-4c5f-8a90-abc123def456" \
  -d '{ "priceUsd": 100, "orderId": "ORDER-10428" }'

Create an off-ramp order

POST/ramp/offrampLive
Body parameters
FieldTypeRequiredDescription
cryptoAmountnumberrequiredAmount of crypto the customer will sell.
walletAddressstringrequiredThe customer's sending wallet.
payCurrencystringoptionalCrypto being sold.
fiatstringoptional3-letter fiat code to pay out in.
externalCustomerIdstringoptionalYour stable customer id.
redirectUrlstringoptionalWhere to send the customer when they finish.
orderIdstringoptionalYour order reference.

Retrieve a ramp order

GET/ramp/orders/:idLive
curl https://kaskade.com/api/v1/ramp/orders/rmp_1a2b3c \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"

Client libraries

Kaskade does not publish official SDKs today, and none is scheduled. We would rather say that than name packages you cannot install.

The API is plain HTTPS and JSON with bearer auth, so it needs no client library: the code examples below are copy-paste ready, the Postman collection covers every endpoint, and the OpenAPI document can generate a typed client in your language with the generator you already use.

Postman collection

Import the collection to call every endpoint with your key pre-wired. It includes a baseUrl variable and an apiKey variable — set them once and go.

Download collection (.json)

In Postman: Import → Upload Files → select the file, then set the collection variables baseUrl and apiKey.

Code examples

Create a payment

curl -X POST https://kaskade.com/api/v1/payments \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "priceUsd": 50,
    "payCurrency": "btc",
    "orderId": "ORDER-123",
    "orderDescription": "Premium plan"
  }'

Authenticate with request signing

import crypto from "crypto";

const clientId     = "YOUR_CLIENT_ID";   // = your key's Public ID
const clientSecret = "ks_sk_YOUR_CLIENT_SECRET";         // shown once when you enable signing

const method = "POST";
const url    = "https://kaskade.com/api/v1/payments";        // the full URL, exactly as called
const ts     = new Date().toISOString();
const body   = JSON.stringify({ priceUsd: 50, payCurrency: "btc", orderId: "ORDER-123" });

// canonical = BOM + METHOD + URL + clientId + timestamp + body
const canonical = "\ufeff" + method + url + clientId + ts + body;
const signature = crypto.createHmac("sha256", clientSecret)
  .update(canonical, "utf8").digest("base64");

const res = await fetch(url, {
  method,
  headers: {
    "Content-Type": "application/json",
    "X-Kaskade-Client": clientId,
    "X-Kaskade-Timestamp": ts,
    "X-Kaskade-Signature": signature,
  },
  body,
});

Verify a webhook

import crypto from "crypto";

// Express raw-body handler for POST /webhooks/kaskade
app.post("/webhooks/kaskade", (req, res) => {
  const signature = req.headers["x-kaskade-signature"];
  const expected = crypto
    .createHmac("sha256", process.env.KASKADE_WEBHOOK_SECRET)
    .update(req.rawBody)            // the exact bytes we sent
    .digest("hex");

  if (signature !== expected) return res.status(401).end();

  const { event, payment } = JSON.parse(req.rawBody);
  if (payment.status === "finished") {
    // ✅ fulfil the order (idempotently)
  }
  res.status(200).end();
});

Integration options

There's more than one way to accept crypto with Kaskade Payment Solution — from sharing a link in seconds to wiring the full API into your platform. Pick the lightest option that meets your need; you can mix them, and they all settle to the same account and emit the same webhooks.

OptionBest forEffort
Hosted invoiceOne-off bills, freelancers, "send me a payment link"No code
Payment links & buttonsA fixed-price product or donate/checkout button on any siteCopy-paste
POS terminalTaking crypto in person at a counterNo code
DonationsTips, fundraising, "pay what you want"No code
SubscriptionsRecurring/membership billingLow code
REST API + webhooksCustom checkout, marketplaces, full controlDeveloper
Platform pluginsWooCommerce, WHMCS and other off-the-shelf storesInstall
Not sure? If you run a store on a known platform, start with a plugin. If you bill people directly, use a hosted invoice. If you're building your own product, go straight to the API.

Plugins & platforms

Drop-in plugins let popular platforms accept crypto through Kaskade Payment Solution without custom code. Packaged plugins are rolling out; in the meantime every platform below can be wired up in minutes with the Create a payment endpoint and a webhook.

WooCommerce Planned

For WordPress + WooCommerce stores. The official gateway plugin (Planned) adds "Pay with crypto" at checkout, redirects the shopper to a hosted invoice, and marks the order paid on the invoice.paid webhook. Until it ships, integrate with a few lines in your theme or a small custom gateway:

  1. Create an API key and set it in your server config (never in client JS).
  2. On order placement, call POST /invoices with the order total as amountUsd and the WooCommerce order id as orderId.
  3. Redirect the customer to the returned invoice url.
  4. On the invoice.paid webhook (verify the signature), mark the matching order complete.

WHMCS Planned

For hosting and SaaS billing on WHMCS. The official payment-gateway module (Planned) registers Kaskade Payment Solution as a gateway so invoices can be paid in crypto and auto-reconciled. Manual integration mirrors WooCommerce: create a Kaskade Payment Solution invoice from the WHMCS invoice, send the client to the hosted checkout, and apply the payment when invoice.paid arrives (map by orderId = WHMCS invoice id).

Want early access to a plugin? Tell us your platform from the dashboard and we'll prioritise it. The integration recipe above keeps you live today regardless.

Team & multi-user access

Most teams need more than one person — or service — touching the gateway. Here's how access works today and what's coming.

API keys as machine identities Live

Create a separate API key for each app, environment or service that talks to Kaskade Payment Solution. Keys are independent — name them ("production server", "staging", "billing worker"), rotate or revoke any one instantly without affecting the others, and enable request signing per key. This is the practical way to give each part of your stack its own credential and audit trail.

Team seats & roles Live

Invite teammates to the dashboard from Settings → Team. Members are invited by email and set their own password from a single-use link. Each member carries a preset — admin, staff, viewer or custom — and the preset is only a label: actual access is a set of per-area permissions you tick, so custom is the honest choice whenever the presets do not fit. A member is invited, active or disabled, and disabling one keeps their audit trail.

Team seats are a dashboard feature. There is no V1 endpoint for managing them, and a team member is not an API credential — use a per-service API key for automation.

Best practice. One key per service, least privilege, rotate on staff changes, and never share a single key across environments — so revoking one never takes down everything.

Export transaction history

Pull your payments for bookkeeping, reconciliation or accounting in two ways.

From the dashboard (CSV) Live

Open Payment history and click Export CSV to download your full payment history as a spreadsheet-ready file. Columns:

FieldTypeRequiredDescription
idstringoptionalKaskade Payment Solution payment id.
createdAtstringoptionalISO-8601 creation time (UTC).
statusstringoptionalFinal or current status.
price_usdnumberoptionalAmount charged in USD.
pay_currencystringoptionalCoin paid.
pay_amountnumberoptionalCrypto amount due.
actually_paidnumberoptionalCrypto amount received.
order_idstringoptionalYour order reference.
deposit_tx_hashstringoptionalOn-chain deposit TXID, if any.
provider_payment_idstringoptionalInternal settlement reference.

Via the API Live

For automated exports or syncing to your own ledger, page through List paymentsand persist what you need. Combine with webhooks to keep your records live instead of polling.

curl https://kaskade.com/api/v1/payments \
  -H "Authorization: Bearer ks_live_YOUR_API_KEY"

Migrating & support

Already integrated with another crypto gateway? Kaskade Payment Solution maps cleanly: a “create charge” becomes Create a payment, IPNs become webhooks, and your order reference rides along as orderId. Because the contract is provider-agnostic (see why), you integrate once and never re-migrate.

Stuck on something? Open a ticket from your dashboard or contact support.

FAQ

Do I need a separate key for signing?

No. Every API key can use Bearer auth, and you can additionally enable a signing secret on the same key.

What happens to my integration when you change crypto infrastructure?

Nothing. The contract on this page is stable across backend changes — see Infrastructure & stability.

How do I test without real money?

Create a small live payment (e.g. $5+) and pay it on-chain, or reach out for sandbox access.

Which status means “paid”?

finished. Fulfil orders on that webhook, idempotently.