Accept stablecoin payments without building custody.

Create deposit addresses, receive signed lifecycle events, and move merchant funds through one environment-scoped API.

USDT & USDCBNB Smart ChainPolygon
  1. 1AuthenticateUse a sandbox API key
  2. 2Create addressAttach your customer reference
  3. 3Confirm depositSimulate funds in sandbox
  4. 4Read balanceCredit your customer
  5. 5Go liveKeep the same request contract

Your first sandbox payment

Ask your Kepler Cash operator for a sandbox API key. Sandbox uses the production API shape but never derives a live address, calls an RPC endpoint, signs, or broadcasts.

Create a deposit address
curl -X POST https://kepler.cash/api/v1/deposit-addresses \
  -H "Authorization: Bearer $KEPLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customerRef": "customer-123",
    "coin": "USDT",
    "network": "polygon"
  }'

Save the returned depositId. In sandbox, the returned address begins with cs-sandbox: and must never receive real funds.

Confirm the sandbox deposit
curl -X POST https://kepler.cash/api/v1/sandbox/deposits/DEP_ID/confirm \
  -H "Authorization: Bearer $KEPLER_API_KEY" \
  -H "Idempotency-Key: confirm-order-123" \
  -H "Content-Type: application/json" \
  -d '{"amount":"100.00","sources":1}'
Read the merchant balance
curl https://kepler.cash/api/v1/balances \
  -H "Authorization: Bearer $KEPLER_API_KEY"

Authentication and environments

Send the API key from your backend. Never expose it in browser JavaScript, mobile bundles, URLs, logs, or support messages.

Required header
Authorization: Bearer YOUR_API_KEY

Live and sandbox keys are separate. The key determines the environment; request bodies cannot override it. Responses include environment so your application can verify where the operation occurred.

ScopeAllows
readBalances, ledger, and resource status
depositsCreating customer deposit addresses
payoutsPayout quotes and customer payouts
settlementsMerchant treasury settlements
embedCreating deposit-widget tokens

Deposit lifecycle

Use your own opaque customerRef to associate a Kepler Cash deposit with a customer or order. Kepler Cash attributes and credits merchant funds; your application remains authoritative for the customer's internal balance.

Credited deposit response
{
  "environment": "live",
  "depositId": "dep_...",
  "status": "credited",
  "coin": "USDT",
  "network": "polygon",
  "gross": "100.00",
  "providerFee": "1.00",
  "netCredit": "99.00",
  "txHash": "0x..."
}

Credit the customer only after receiving deposit.credited or confirming the deposit status through GET /api/v1/deposits/{id}.

Customer payouts

Request a quote first, then create the payout from your backend. Every creation request requires a unique idempotency key. Repeating the same key and body returns the original operation; changing the body returns a conflict.

Create a payout
curl -X POST https://kepler.cash/api/v1/payouts \
  -H "Authorization: Bearer $KEPLER_API_KEY" \
  -H "Idempotency-Key: payout-order-456" \
  -H "Content-Type: application/json" \
  -d '{
    "customerRef": "customer-123",
    "coin": "USDT",
    "network": "bsc",
    "amount": "25.00",
    "address": "0x..."
  }'
Amounts are decimal strings. Do not send JSON numbers or use floating-point arithmetic for balances and fees.

Merchant settlements

Settlements move merchant balance to a destination configured by the Kepler Cash operator. Merchants cannot override the destination address in the request.

Create a settlement
curl -X POST https://kepler.cash/api/v1/settlements \
  -H "Authorization: Bearer $KEPLER_API_KEY" \
  -H "Idempotency-Key: settlement-2026-09-06" \
  -H "Content-Type: application/json" \
  -d '{"coin":"USDC","network":"polygon","amount":"250.00"}'

Verify webhooks

Verify the signature against the exact raw request body before parsing JSON. Reject stale timestamps and deduplicate events by the stable id.

Signature headers
X-CS-Custody-Event: evt_...
X-CS-Custody-Timestamp: 1788643800
X-CS-Custody-Version: 2026-09-01
X-CS-Custody-Signature: v1=<hex hmac-sha256>
Node.js verification
import { createHmac, timingSafeEqual } from "node:crypto";

const signed = `${timestamp}.${rawBody}`;
const expected = createHmac("sha256", webhookSecret)
  .update(signed, "utf8")
  .digest("hex");
const received = signature.replace(/^v1=/, "");

const valid =
  expected.length === received.length &&
  timingSafeEqual(Buffer.from(expected), Buffer.from(received));

Endpoint reference

MethodPathScopePurpose
GET/api/v1/balancesreadRead available and reserved balances
GET/api/v1/ledgerreadList immutable ledger entries
POST/api/v1/deposit-addressesdepositsCreate a deposit address
GET/api/v1/deposits/{id}readRead a deposit
POST/api/v1/payouts/quotepayoutsPreview payout fees
POST/api/v1/payoutspayoutsCreate a customer payout
GET/api/v1/payouts/{id}readRead a payout
POST/api/v1/settlementssettlementsCreate a merchant settlement
GET/api/v1/settlements/{id}readRead a settlement

Errors and retries

Error response
{
  "error": {
    "code": "insufficient_balance",
    "message": "Available USDT balance is lower than the required debit.",
    "requestId": "req_..."
  }
}

Retry transient 5xx responses with backoff and the same idempotency key. Do not blindly retry validation, authentication, insufficient-balance, or idempotency-conflict responses. Keep the requestId for support.