Totopay
OverviewCustomersDepositsWebhook EndpointsBalancePaymentsMerchant balancesSwapTest Mode
Dashboard

On this page

  • Developer guide
  • All endpoints
  • Authentication
  • Quick start
Customers
  • GET/customers/exists
  • POST/customers
  • GET/customers
  • GET/customers/:id
Deposits
  • GET/deposits
  • GET/deposits/:id
  • GET/customers/:customerId/deposits
Webhook Endpoints
  • POST/webhook-endpoints
  • GET/webhook-endpoints
Balance
  • GET/balance
Payments
  • POST/request-payment
  • GET/payments/:token
  • GET/payments
Merchant balances
  • GET/merchant-balances
Swap
  • GET/swap/pairs
  • POST/swap/quote
  • POST/swap/execute
  • GET/swap/history
Test Mode
  • POST/test/simulate-deposit
Webhooks

API Reference

Accept crypto payments with a Stripe-like developer experience. Create customers, get permanent wallet addresses, and receive webhooks when deposits arrive.

Base URL:https://api.totopay.net/api/v1

Quick start

  1. Sign up and copy API keys from Settings (sk_live_ / sk_test_).
  2. Check customer — GET /customers/exists?email=...
  3. Create customer — POST /customers (skip if exists)
  4. Webhook — POST /webhook-endpoints for deposit.completed
  5. Test — sk_test_ + POST /test/simulate-deposit
All endpointsCustomersDepositsWebhook EndpointsBalancePaymentsMerchant balancesSwapTest Mode

Developer guide

Request format

All gateway URLs start with https://api.totopay.net/api/v1. Send JSON bodies with Content-Type: application/json on POST requests. Successful responses are JSON objects; errors return an error string (and sometimes a code).

Common headers

HeaderExampleWhen to use
AuthorizationBearer sk_live_... or sk_test_...Preferred for server-to-server
x-api-keysk_live_... or sk_test_...Alternative to Authorization
Content-Typeapplication/jsonRequired on POST/PUT bodies
Cookiech_session=...Dashboard browser session
X-ModetestWith session cookie: use test data instead of live

Live vs test mode

Live (sk_live_*)

Mainnet addresses and real funds. Customers, deposits, and webhooks are production data. Configure IP allowlist in Settings if enabled.

Test (sk_test_*)

Isolated sandbox. Same API shape; use POST /test/simulate-deposit instead of sending real crypto. Dashboard: add X-Mode: test with session cookie.

Typical error responses

Most errors return JSON:
{
  "error": "Human-readable message"
}

Subscription errors also include:
{
  "error": "Active subscription required",
  "code": "SUBSCRIPTION_REQUIRED"
}

Idempotency tips

  • Customers: call GET /customers/exists before POST /customers to avoid 409 duplicates.
  • Deposits: use transaction_hash from webhooks as your idempotency key when crediting users.
  • Webhooks: return HTTP 200 quickly; verify HMAC on the raw body before parsing JSON.

All endpoints

19 routes · Base: https://api.totopay.net/api/v1
MethodPathSectionSummary
GET/api/v1/customers/existsCustomersCheck if a customer exists by email or external_id
POST/api/v1/customersCustomersCreate customer and generate wallet addresses
GET/api/v1/customersCustomersList customers with pagination and filters
GET/api/v1/customers/:idCustomersGet one customer with all deposit addresses
GET/api/v1/depositsDepositsList all deposits for your merchant
GET/api/v1/deposits/:idDepositsGet a single deposit by ID
GET/api/v1/customers/:customerId/depositsDepositsList deposits for one customer
POST/api/v1/webhook-endpointsWebhook EndpointsRegister a webhook URL and event subscriptions
GET/api/v1/webhook-endpointsWebhook EndpointsList registered webhook endpoints
GET/api/v1/balanceBalanceMerchant balance summary
POST/api/v1/request-paymentPaymentsCreate a hosted payment link
GET/api/v1/payments/:tokenPaymentsPublic checkout data (no authentication)
GET/api/v1/paymentsPaymentsList payment links (dashboard session)
GET/api/v1/merchant-balancesMerchant balancesPer-asset merchant balance rows
GET/api/v1/swap/pairsSwapList supported swap pairs
POST/api/v1/swap/quoteSwapGet a swap quote
POST/api/v1/swap/executeSwapExecute a quoted swap
GET/api/v1/swap/historySwapSwap transaction history
POST/api/v1/test/simulate-depositTest ModeInstantly create a completed test deposit

Supported networks

Ethereum
ethereum · ETH
BNB Chain
bsc · BNB
USDT ERC-20
ethereum
USDT BEP-20
bsc
USDT TRC-20
tron

Authentication

Every /api/v1/* route (except public checkout) requires proof that the caller is your merchant account. Use an API key for backend integrations; use the dashboard session cookie only for browser-based tools.

  • API keys are created in Settings and start with sk_live_ or sk_test_.
  • Pass the key as Authorization: Bearer … or x-api-key: … (never expose live keys in frontend code).
  • Most gateway routes need an active subscription; otherwise you get HTTP 403 with code: SUBSCRIPTION_REQUIRED.
  • If you configured allowed IPs for live keys, requests from other IPs are rejected.
Authorization: Bearer sk_live_YOUR_API_KEY
x-api-key: sk_live_YOUR_API_KEY

# Browser / dashboard (optional test data):
Cookie: ch_session=...
X-Mode: test
Live
sk_live_ — mainnet, real funds. IP allowlist applies when configured.
Test
sk_test_ — isolated test data. Use simulate-deposit.

Customers

Customers are your end-users. Each customer gets permanent multi-chain deposit addresses. Manage them with the endpoints below — there is no update or delete API; create once and reuse the same id.

  • Live (sk_live_*) and test (sk_test_*) data are fully isolated — the same email can exist once in each mode.
  • Duplicate protection: POST returns 409 if external_id or email already exists. Use GET /exists before POST for idempotent registration.
  • Typical integration: (1) exists? (2) create if needed (3) show addresses (4) listen for deposit.* webhooks.

Recommended customer flow

  1. GET /customers/exists?email=... — skip create if exists: true
  2. POST /customers — returns 201 with addresses, or 409 if email/external_id already used
  3. GET /customers/:id — fetch addresses anytime

Live and test customers are isolated (sk_live_* vs sk_test_* / X-Mode: test).

Lightweight lookup before creating a customer. Answers whether your merchant already has a customer record in the current mode (live vs test). This avoids duplicate wallet generation and prevents 409 errors on POST /customers when you implement idempotent sign-up.

Authentication

Bearer sk_live_* or sk_test_* (or x-api-key header). Dashboard session cookie also works; add X-Mode: test for sandbox data.

How it works

  • Provide at least one query parameter: external_id or email.
  • If both are provided, external_id is evaluated first; email is only used when external_id is absent or empty after trimming.
  • Matching is exact on trimmed strings, scoped to your merchant_id and is_live flag (derived from API key or X-Mode).
  • The customer object in the response is a summary (id, external_id, email) — use GET /customers/:id for full addresses.
  • Recommended as step 1 in registration: exists → create only when exists is false.
  • POST /request-payment also resolves customers by email internally (creates if missing) — use /exists when you manage customers yourself.

HTTP Status Codes

200Lookup completed (check exists field — both true and false are valid 200 responses)
400Missing both external_id and email
401Invalid or missing API key / session
403Subscription required or IP not allowlisted

Query parameters

NameTypeRequiredDescription
external_idstringnoYour application’s user ID for this person (e.g. user_123 from your database)
emailstringnoCustomer email address (case-sensitive match on stored value)

Response fields

NameTypeRequiredDescription
existsbooleanyestrue if a matching customer was found
matchstringyes"external_id" or "email" — which field matched
customerobject | nullyesSummary row when exists is true; otherwise null
customer.iduuidnoGateway customer UUID — store this in your system
customer.external_idstring | nullnoYour ID if set at creation
customer.emailstring | nullnoEmail if set at creation

Example request

curl -G "https://api.totopay.net/api/v1/customers/exists" \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  --data-urlencode "email=john@example.com"

Example response (success)

// Found
{
  "exists": true,
  "match": "email",
  "customer": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "external_id": "user_123",
    "email": "john@example.com"
  }
}

// Not found
{
  "exists": false,
  "match": "email",
  "customer": null
}

Example errors

// 400 — no lookup key
{ "error": "Provide external_id or email" }

// 401
{ "error": "Invalid API key" }

// 403 subscription
{ "error": "Active subscription required", "code": "SUBSCRIPTION_REQUIRED" }

Deposits

When crypto arrives at a customer address, the platform creates a deposit record and advances it through statuses. You are notified via webhooks; these REST endpoints let you query history.

  • Statuses: pending → confirming → completed (or failed).
  • A flat $2 USD service fee is deducted per completed deposit (see amount_usd, service_fee_usd, net_amount_usd).
  • You do not create deposits via API — they are detected on-chain automatically.

Webhook Endpoints

Register HTTPS URLs to receive server-to-server event notifications. All deliveries are signed with HMAC-SHA256. See the Webhook Signature Verification section for verification code and retry rules.

  • Store the whsec_... secret immediately when creating an endpoint — it is only returned once.
  • Subscribe only to events you handle; valid types are listed on POST /webhook-endpoints.
  • Test and live endpoints are separate (based on API key or X-Mode: test).

Balance

Aggregated USD totals and per-currency breakdown for your merchant account.

Payments

Hosted checkout links: create a payment URL, send it to your customer, and get notified when they pay. Combines customer resolution, USDT checkout, and optional per-link webhooks.

  • Minimum amount is $5.00 USD.
  • If email is provided, the API finds or creates a gateway customer and uses their permanent addresses on the pay page.
  • GET /payments/:token is public (no auth) — safe for checkout pages.

Merchant balances

Per-asset balance ledger rows for gateway accounting.

Swap

Convert between supported assets from your merchant treasury. Quote and execute require subscription; listing pairs does not.

Test Mode

Test mode uses sk_test_* keys and isolated data. Simulate deposits without sending real crypto — ideal for CI and staging.

  • Create a test customer with sk_test_*, then call simulate-deposit with that customer_id.
  • Webhook endpoints created under test mode only receive test events.

Webhooks — verification & delivery

TotoPay sends HTTP POST requests to your server when events occur (deposits, new customers, completed payment links). Every payload is JSON with shape { "event": "...", "data": { }, "timestamp": "..." }. You must verify the HMAC signature before trusting the body.

Delivery rules
  • Headers: X-Webhook-Signature (hex), X-Webhook-Timestamp (ISO-8601), User-Agent: CryptoHub-Webhook/1.0
  • Signature: HMAC_SHA256(secret, timestamp + "." + rawJsonBody) — use the exact raw bytes, not re-serialized JSON
  • Respond with HTTP 2xx within 10 seconds; process business logic asynchronously
  • Retries: up to 5 attempts with backoff 1s → 5s → 15s → 1m → 5m on 5xx, timeouts, and 429
  • HTTP 4xx (except 429) = permanent failure, no further retries
  • Per-payment webhook_url on request-payment uses the same signature but a separate webhook_secret from the create response
// Example inbound body
{
  "event": "deposit.completed",
  "data": { "id": "...", "transaction_hash": "0x...", "amount_usd": 100, ... },
  "timestamp": "2026-05-22T12:00:00.123Z"
}

Node.js

const crypto = require('crypto');

function verifyWebhook(rawBodyString, signatureHex, secret, timestamp) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + rawBodyString)
    .digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(signatureHex, 'utf8'), Buffer.from(expected, 'utf8'));
  } catch {
    return false;
  }
}

// Use express.raw() or capture raw body before json() for exact bytes:
app.post('/webhooks/totopay', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  const ts = req.headers['x-webhook-timestamp'];
  const sig = req.headers['x-webhook-signature'];
  if (!verifyWebhook(raw, sig, process.env.WEBHOOK_SECRET, ts)) {
    return res.status(400).send('Invalid signature');
  }
  const body = JSON.parse(raw);
  if (body.event === 'deposit.completed') {
    console.log('Deposit confirmed:', body.data?.transaction_hash);
  }
  if (body.event === 'payment.completed') {
    console.log('Payment link paid:', body.data?.token, body.data?.paid_network);
  }
  res.status(200).send('OK');
});

Python

import hmac, hashlib

def verify_webhook(raw_body: bytes, signature: str, secret: str, timestamp: str) -> bool:
    msg = f"{timestamp}.{raw_body.decode('utf-8')}".encode("utf-8")
    expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)

# Pass request.get_data() as raw_body before JSON parsing:
@app.route('/webhooks/totopay', methods=['POST'])
def handle_webhook():
    raw = request.get_data()
    ts = request.headers.get('X-Webhook-Timestamp', '')
    sig = request.headers.get('X-Webhook-Signature', '')
    if not verify_webhook(raw, sig, WEBHOOK_SECRET, ts):
        return 'Invalid signature', 400
    body = request.get_json(silent=True) or {}
    if body.get('event') == 'deposit.completed':
        print('Deposit confirmed:', body.get('data', {}).get('transaction_hash'))
    if body.get('event') == 'payment.completed':
        print('Payment link paid:', body.get('data', {}).get('token'), body.get('data', {}).get('paid_network'))
    return 'OK', 200

Webhook Event Types

deposit.detectedA new deposit has been detected on-chain (0 confirmations).
deposit.confirmingThe deposit has at least 1 confirmation but is not yet fully confirmed.
deposit.completedThe deposit has reached the required number of confirmations.
deposit.failedDeposit marked failed after being tracked (e.g. subscription monthly deposit limit).
customer.createdEmitted after POST /customers succeeds (includes addresses when generated).
payment.completedHosted payment link (POST /request-payment) confirmed on-chain; includes token, paid_network, transaction_hash.

TotoPay API v1 — Full reference: cryptohub/docs/API.md — Questions? support@totopay.net