WhatPay API Reference

Multi-chain custody & payment API for ETH, BSC, BASE, POLYGON, TRX, and SOL

Base URL: https://console.whatpay.com/{your-slug}/api

Overview

The WhatPay API allows you to integrate multi-chain cryptocurrency deposits and withdrawals into your application. All API responses use JSON and follow a consistent structure:

{
  "code": 0,        // 0 = success, non-zero = error
  "data": { ... }   // response payload
}
Field type conventions.
  • Time fields (created_at, updated_at, sent_at, confirmed_at, credited_at, approved_at, expires_at, webhook timestamp) are returned as unix seconds (integer), never ISO strings. null if not yet set.
  • BIGINT fields (block_number) are returned as string to avoid JS Number precision loss above 2^53.
  • Amount fields (amount, amount_usd) are returned as string to preserve full decimal precision.

Each merchant project has a unique slug (e.g. acme). All API requests are scoped to your project via the base URL:

https://console.whatpay.com/{your-slug}/api/v1/...

Replace {your-slug} with the slug assigned to your project. You can find it in your console workspace URL.

Supported chains:

ETH BSC BASE POLYGON TRX SOL

API Credentials

API credentials are created in your project console under Settings → API Keys. Each key has a permission level that controls what it can do:

PermissionCapabilities
readQuery addresses, deposits, withdrawals (GET endpoints only)
manageread + assign addresses, submit & cancel withdrawals
approvemanage + approve / reject withdrawal requests

When a key is created, you receive three credentials — store them securely, they are shown only once:

app_idIdentifies your application. Passed in every request as x-app-id. api_secretUsed to sign requests. Never send this in the request body. webhook_secretUsed to verify inbound webhook payloads from WhatPay. Starts with whs_.

Authentication

WhatPay supports two authentication modes. Choose at API key creation time in the console:

ModeAlgorithmServer holdsBest for
HMAC (default)HMAC-SHA256Shared api_secretExisting integrations, internal low-risk traffic
RSA (recommended)SHA256WithRSAOnly your public key. Private key never leaves your machine.Withdrawal / high-value flows, compliance-sensitive customers

Both modes share the same sign-string format and replay-prevention rules. The only difference is the signature algorithm and encoding. Existing HMAC keys keep working — RSA is opt-in per key.

HMAC Mode

HMAC-SHA256 request signing. Include these headers with every request:

HeaderDescription
x-app-idYour application ID (starts with app_)
x-timestampCurrent Unix timestamp (seconds). Request must be within ±300s of server time.
x-nonceRandom string (8–32 chars) for replay prevention
x-signatureHMAC-SHA256 signature (hex)

Signature Calculation

Build the sign string and compute HMAC-SHA256 using your api_secret:

sign_string = timestamp + "\n" + METHOD + "\n" + path_with_query + "\n" + md5(body)

signature = HMAC-SHA256(sign_string, api_secret)
Three common mistakes that cause 401:
  • POST bodyMd5 — For POST requests, use md5(actualRequestBody). For GET/DELETE requests with no body, use md5("") = d41d8cd98f00b204e9800998ecf8427e. The sign string always has exactly 4 parts separated by \n.
  • api_secret encoding — Your api_secret is a 64-character hex string. Use it directly as a string for the HMAC key — do not hex-decode it into bytes first.
  • path — Use the short path only: /v1/subaccount/create. Do not include the slug or /api prefix.
path_with_query — full path including query string if present, e.g. /v1/subaccount/list?page=1&size=20. Strip the slug and /api prefix: the path starts with /v1/.

Example (Node.js)

const crypto = require('crypto');

// API_SECRET is a 64-char hex string — pass it as-is, do NOT Buffer.from(secret, 'hex')
function sign(method, path, body, apiSecret) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce     = crypto.randomBytes(8).toString('hex');
  const bodyStr   = body || '';                                         // '' for GET
  const bodyMd5   = crypto.createHash('md5').update(bodyStr).digest('hex');
  const signStr   = `${timestamp}\n${method.toUpperCase()}\n${path}\n${bodyMd5}`;
  const signature = crypto.createHmac('sha256', apiSecret).update(signStr).digest('hex');
  return { 'x-app-id': APP_ID, 'x-timestamp': timestamp, 'x-nonce': nonce, 'x-signature': signature };
}

// POST example
const postBody = JSON.stringify({ user_id: 'u001' });
const headers  = sign('POST', '/v1/subaccount/create', postBody, API_SECRET);
const res = await fetch('https://console.whatpay.com/{slug}/api/v1/subaccount/create', {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: postBody,
});

// GET example — pass empty string for body
const getHeaders = sign('GET', '/v1/subaccount/list?page=1&size=20', '', API_SECRET);
const res2 = await fetch('https://console.whatpay.com/{slug}/api/v1/subaccount/list?page=1&size=20', {
  method: 'GET',
  headers: getHeaders,
});

Example (Python)

import hmac as hmac_lib, hashlib, time, secrets, json

# api_secret is a 64-char hex string — encode to bytes as UTF-8, do NOT bytes.fromhex()
def sign(method, path, body, api_secret):
    ts       = str(int(time.time()))
    nonce    = secrets.token_hex(8)
    body_str = body or ''                                         # '' for GET
    body_md5 = hashlib.md5(body_str.encode()).hexdigest()
    sign_str = f"{ts}\n{method.upper()}\n{path}\n{body_md5}"
    sig      = hmac_lib.new(api_secret.encode('utf-8'), sign_str.encode('utf-8'), hashlib.sha256).hexdigest()
    return {'x-app-id': APP_ID, 'x-timestamp': ts, 'x-nonce': nonce, 'x-signature': sig}

# POST example
post_body = json.dumps({'user_id': 'u001'})
headers   = sign('POST', '/v1/subaccount/create', post_body, API_SECRET)
headers['Content-Type'] = 'application/json'

# GET example — pass empty string for body
get_headers = sign('GET', '/v1/subaccount/list?page=1&size=20', '', API_SECRET)

RSA Mode (Recommended)

RSA-SHA256 request signing. The server only stores your public key; the private key is generated and held entirely by you. Even if our database is compromised, your signing capability cannot be forged.

1. Prepare a key pair

Two options:

Lose the private key, lose access. The server cannot recover it — by design. Store it in a secrets manager (HashiCorp Vault, AWS Secrets Manager, etc.) with backups. If lost, disable the key in console and create a new one.

2. Headers

Same four headers as HMAC mode. Only the encoding of x-signature differs.

HeaderDescription
x-app-idYour application ID
x-timestampUnix timestamp (seconds), ±300s window
x-nonceRandom string
x-signatureSHA256WithRSA signature, base64-encoded (not hex)

3. Signature calculation

The sign-string is identical to HMAC mode. Only the signing primitive changes:

sign_string = timestamp + "\n" + METHOD + "\n" + path_with_query + "\n" + md5(body)

signature = base64( RSA-SHA256-sign( private_key, sign_string ) )
# RSA padding: PKCS#1 v1.5  (NOT PSS)
# Hash:        SHA-256
# Encoding:    standard base64 (with "=" padding)

4. Example (Node.js)

const crypto = require('node:crypto');
const fs     = require('node:fs');

const PRIVATE_KEY = fs.readFileSync('./whatpay_private_key.pem', 'utf8');

function sign(method, path, body) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce     = crypto.randomBytes(8).toString('hex');
  const bodyStr   = body || '';
  const bodyMd5   = crypto.createHash('md5').update(bodyStr).digest('hex');
  const signStr   = `${timestamp}\n${method.toUpperCase()}\n${path}\n${bodyMd5}`;
  const signature = crypto.sign('sha256', Buffer.from(signStr), {
    key:     PRIVATE_KEY,
    padding: crypto.constants.RSA_PKCS1_PADDING,
  }).toString('base64');
  return { 'x-app-id': APP_ID, 'x-timestamp': timestamp, 'x-nonce': nonce, 'x-signature': signature };
}

const postBody = JSON.stringify({ user_id: 'u001' });
const headers  = sign('POST', '/v1/subaccount/create', postBody);
const res = await fetch('https://console.whatpay.com/{slug}/api/v1/subaccount/create', {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: postBody,
});

5. Example (Python)

Uses the cryptography library (pip install cryptography). Avoids legacy PyCrypto.

import hashlib, time, secrets, base64, json
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

with open('whatpay_private_key.pem', 'rb') as f:
    PRIVATE_KEY = serialization.load_pem_private_key(f.read(), password=None)

def sign(method, path, body):
    ts        = str(int(time.time()))
    nonce     = secrets.token_hex(8)
    body_str  = body or ''
    body_md5  = hashlib.md5(body_str.encode()).hexdigest()
    sign_str  = f"{ts}\n{method.upper()}\n{path}\n{body_md5}".encode()
    sig_bytes = PRIVATE_KEY.sign(sign_str, padding.PKCS1v15(), hashes.SHA256())
    sig       = base64.b64encode(sig_bytes).decode()
    return {'x-app-id': APP_ID, 'x-timestamp': ts, 'x-nonce': nonce, 'x-signature': sig}

post_body = json.dumps({'user_id': 'u001'})
headers   = sign('POST', '/v1/subaccount/create', post_body)
headers['Content-Type'] = 'application/json'

6. Example (Go)

package main

import (
    "crypto"
    "crypto/md5"
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/base64"
    "encoding/hex"
    "encoding/pem"
    "fmt"
    "os"
    "strconv"
    "time"
)

var priv *rsa.PrivateKey

func init() {
    data, _ := os.ReadFile("whatpay_private_key.pem")
    block, _ := pem.Decode(data)
    k, _ := x509.ParsePKCS8PrivateKey(block.Bytes)
    priv = k.(*rsa.PrivateKey)
}

func sign(method, path, body string) map[string]string {
    ts := strconv.FormatInt(time.Now().Unix(), 10)
    nonce := make([]byte, 8); rand.Read(nonce)
    bodyMd5 := fmt.Sprintf("%x", md5.Sum([]byte(body)))
    signStr := ts + "\n" + method + "\n" + path + "\n" + bodyMd5
    h := crypto.SHA256.New(); h.Write([]byte(signStr))
    sigBytes, _ := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, h.Sum(nil))
    return map[string]string{
        "x-app-id": APP_ID, "x-timestamp": ts,
        "x-nonce": hex.EncodeToString(nonce),
        "x-signature": base64.StdEncoding.EncodeToString(sigBytes),
    }
}

7. Verifying your setup

If the server returns 40101 RSA signature verification failed, check in this order:

You can dump both signature strings (client and server) for a side-by-side byte-level diff if mismatch persists — contact support with the request ID.

Error Codes

0Success 40001Invalid request parameters 40004Source address not owned by this wallet (used by client APIs that accept from) 40101Authentication failed (missing headers, invalid signature, or expired timestamp) 40301IP address not in whitelist 40302Insufficient API key permission (e.g. using a read key for a manage endpoint) 40401Resource not found 40901Conflict — biz_order_id already exists for a different request 40902Conflict — biz_order_id was already associated with a failed or rejected withdrawal and cannot be reused. Submit a new biz_order_id (e.g. append _retry1) when retrying. 42201Insufficient balance — source wallet does not have enough funds for this withdrawal 42202Address not in whitelist — your project has a whitelist configured and the to_address is not on it 42203Daily withdrawal limit exceeded for this chain / token combination 42901Rate limit exceeded (200 requests/minute) 50001Internal server error

Deposit Status Flow

Every deposit goes through the following lifecycle. The status field in all deposit responses reflects the current stage.

User sends funds
On-chain TX
status = 0
Confirming
confirmations updating
status = 2
Credited
credited_at set
deposit.credited webhook fires when the deposit reaches the required block confirmations (status=2). WhatPay does not push a separate deposit.pending event — credit your user only when this event arrives.
statusDescriptionconfirmations
0Detected on-chain, waiting for block confirmations0 → required threshold
2Fully confirmed and credited to user balance≥ threshold

Confirmation Thresholds by Chain

ChainRequired ConfirmationsApproximate Time
ETH12 blocks~2.5 minutes
BSC15 blocks~45 seconds
BASE15 blocks~30 seconds
POLYGON30 blocks~60 seconds
ARBITRUM2 blocks~2 seconds
TRX20 blocks~60 seconds
SOL32 slots~32 seconds

Webhooks

Configure your webhook URL when creating API credentials. WhatPay pushes deposit.credited once a deposit reaches its chain-specific confirmation threshold (see the table above). Failed deliveries are retried automatically — see Retry & idempotency.

Security — webhook signature: Each request includes X-Signature and X-Timestamp headers.
Verify: HMAC-SHA256(timestamp + rawBody, webhook_secret) — use your webhook_secret (returned at API key creation, starts with whs_), not your api_secret.

Verify Signature (Node.js)

const crypto = require('crypto');

function verifyWebhook(req, webhookSecret) {
  const ts  = req.headers['x-timestamp'];
  const sig = req.headers['x-signature'];
  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(ts + JSON.stringify(req.body))   // timestamp + raw body string
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

Event: deposit.credited

Fired when the deposit reaches the chain-specific confirmation threshold (see the timing table above). This is the authoritative event — credit the user balance only when this arrives.

{
  "event": "deposit.credited",
  "data": {
    "deposit_no": "DEP-20260523-001",
    "user_id": "user_001",
    "chain": "ETH",
    "address": "0xAbC...",
    "token": "USDT",
    "amount": "100.000000000000000000",
    "tx_hash": "0x1234...",
    "block_number": "22000000",
    "confirmations": 12,
    "status": 2,
    "credited_at": 1748000180
  },
  "timestamp": 1748000180
}

Retry & Idempotency

Any non-2xx response (or connection failure) is queued for retry with exponential backoff:

AttemptWait before retry
1st retry30 seconds
2nd retry2 minutes
3rd retry10 minutes
4th retry1 hour
5th retry6 hours
6th retry24 hours

After 6 failures the delivery is marked abandoned. A 30-second background sweep also rescues any deliveries that were missed (e.g. queue downtime). Use POST /v1/deposit/replay_webhook to manually replay any individual deposit.

Always treat deposit_no as the idempotency key. Because of retries and the periodic sweep, the same deposit.credited event may arrive more than once. Persist deposit_no on first receipt and short-circuit duplicates — do not credit the user twice.
Return 2xx even for "user not found". If WhatPay's user_id doesn't resolve to a user in your system, log it and respond 200 OK. Returning 4xx will trigger WhatPay's 6-step retry chain and may leave the delivery stuck for hours. Dead-letter unknown user_id deliveries on your side instead.

Withdrawal Webhook Events

WhatPay sends up to five events during a withdrawal lifecycle. All events share the same signature scheme as deposit webhooks.

Event: withdraw.approved

Fired when an admin approves the withdrawal and it enters the signing queue.

{
  "event": "withdraw.approved",
  "data": {
    "withdraw_no": "WD20260525A1B2",
    "biz_order_id": "order_001",
    "chain": "ETH",
    "token": "USDT",
    "amount": "500.00",
    "to_address": "0xRecipient...",
    "tx_hash": null,
    "status": 1,
    "fail_reason": null,
    "created_at": 1748000000,
    "confirmed_at": null
  },
  "timestamp": 1748000060
}

Event: withdraw.broadcast

Fired when the signed transaction has been broadcast to the network. tx_hash is now set.

{
  "event": "withdraw.broadcast",
  "data": {
    "withdraw_no": "WD20260525A1B2",
    "biz_order_id": "order_001",
    "chain": "ETH",
    "token": "USDT",
    "amount": "500.00",
    "to_address": "0xRecipient...",
    "tx_hash": "0xabc123def456...",
    "status": 3,
    "fail_reason": null,
    "created_at": 1748000000,
    "confirmed_at": null
  },
  "timestamp": 1748000120
}

Event: withdraw.confirmed

The authoritative success event. The on-chain transaction has been confirmed. Safe to credit the recipient.

{
  "event": "withdraw.confirmed",
  "data": {
    "withdraw_no": "WD20260525A1B2",
    "biz_order_id": "order_001",
    "chain": "ETH",
    "token": "USDT",
    "amount": "500.00",
    "to_address": "0xRecipient...",
    "tx_hash": "0xabc123def456...",
    "status": 4,
    "fail_reason": null,
    "created_at": 1748000000,
    "confirmed_at": 1748000300
  },
  "timestamp": 1748000300
}

Event: withdraw.failed

Fired when signing or broadcast fails. Check fail_reason for details. Contact support if this is unexpected.

{
  "event": "withdraw.failed",
  "data": {
    "withdraw_no": "WD20260525A1B2",
    "biz_order_id": "order_001",
    "chain": "ETH",
    "token": "USDT",
    "amount": "500.00",
    "to_address": "0xRecipient...",
    "tx_hash": null,
    "status": 5,
    "fail_reason": "insufficient funds for gas",
    "created_at": 1748000000,
    "confirmed_at": null
  },
  "timestamp": 1748000130
}

Event: withdraw.rejected

Fired when an admin rejects the withdrawal request. The funds are never moved.

{
  "event": "withdraw.rejected",
  "data": {
    "withdraw_no": "WD20260525A1B2",
    "biz_order_id": "order_001",
    "chain": "ETH",
    "token": "USDT",
    "amount": "500.00",
    "to_address": "0xRecipient...",
    "tx_hash": null,
    "status": 6,
    "fail_reason": "Rejected: suspicious address",
    "created_at": 1748000000,
    "confirmed_at": null
  },
  "timestamp": 1748000090
}

Client Wallet Webhook Events

For every Client Wallet operation that reaches a final state, WhatPay pushes one webhook to your configured webhook_url. Signature scheme is the same as the deposit/withdraw webhooks (X-Signature + X-Timestamp, HMAC-SHA256 of timestamp + rawBody with webhook_secret).

EventTriggerIncludes
client.sign-message.confirmedpersonal_sign completedsignature
client.sign-typed-data.confirmedEIP-712 typed-data signedsignature
client.transfer.confirmedToken transfer on-chain confirmedtx_hash
client.send-tx.confirmedArbitrary EVM tx on-chain confirmedtx_hash
client.{op_type}.failedSigning or broadcast failedfail_reason

Example: client.sign-message.confirmed

The only way to retrieve the raw signature asynchronously — there is no synchronous REST endpoint that returns it during signing. GET /v1/client/operations also exposes signature once the op reaches status=3, so polling is a viable fallback.

{
  "event": "client.sign-message.confirmed",
  "timestamp": 1748100000,
  "data": {
    "op_no": "op_baf50fb89523fa64234e4b7e5cec",
    "op_type": "sign-message",
    "project_id": "algowhirl",
    "wallet_id": 7,
    "from_address": "0x2531BA2fC2876f8dd712FC2Eef4e2fDA81673Eb6",
    "chain": "BASE",
    "token": null,
    "to_address": null,
    "amount": null,
    "status": 3,
    "decided_by": "auto",
    "tx_hash": null,
    "signature": "0xb00cbb8048a0c4f1e8...1b",
    "fail_reason": null,
    "created_at": 1779983462,
    "sent_at":    null,
    "confirmed_at": 1779983463
  }
}

Example: client.transfer.confirmed

{
  "event": "client.transfer.confirmed",
  "timestamp": 1748100120,
  "data": {
    "op_no": "op_d5430c5081c4e99a6868540b3ee8",
    "op_type": "transfer",
    "project_id": "algowhirl",
    "wallet_id": 7,
    "from_address": "0x2531BA2fC2876f8dd712FC2Eef4e2fDA81673Eb6",
    "chain": "BASE",
    "token": "USDC",
    "to_address": "0xRecipient...",
    "amount": "12.500000",
    "status": 3,
    "decided_by": "session_key",
    "tx_hash": "0xabc123def456...",
    "signature": null,
    "fail_reason": null,
    "created_at": 1779983460,
    "sent_at":    1779983464,
    "confirmed_at": 1779983478
  }
}

Example: client.send-tx.failed

{
  "event": "client.send-tx.failed",
  "timestamp": 1748100200,
  "data": {
    "op_no": "op_1cafaf1ca1319f03797befa92d44",
    "op_type": "send-tx",
    "project_id": "algowhirl",
    "wallet_id": 7,
    "from_address": "0x2531BA2fC2876f8dd712FC2Eef4e2fDA81673Eb6",
    "chain": "BASE",
    "token": null,
    "to_address": "0xContract...",
    "amount": null,
    "status": 4,
    "decided_by": "auto",
    "tx_hash": null,
    "signature": null,
    "fail_reason": "insufficient funds for gas",
    "created_at": 1779983470,
    "sent_at":    null,
    "confirmed_at": null
  }
}
Retry policy. Same as deposit/withdraw webhooks — non-2xx responses are retried at 30s → 2min → 10min → 1h → 6h → 24h, abandoned after 6 attempts. A 30s background scan also picks up any missed deliveries.

System Endpoints

GET /v1/system/health No Auth Health check
Response
{ "code": 0, "data": { "status": "ok", "time": "2026-05-23T10:00:00.000Z" } }
GET /v1/system/chains No Auth List supported chains
Response
{
  "code": 0,
  "data": [
    { "chain": "ETH",  "chain_id": 1,    "native_token": "ETH",  "confirmations": 12, "block_time": 12 },
    { "chain": "BSC",  "chain_id": 56,   "native_token": "BNB",  "confirmations": 15, "block_time": 3  },
    { "chain": "BASE", "chain_id": 8453, "native_token": "ETH",  "confirmations": 15, "block_time": 2  },
    { "chain": "POLYGON","chain_id": 137,"native_token": "MATIC","confirmations": 30, "block_time": 2  },
    { "chain": "TRX",  "chain_id": null, "native_token": "TRX",  "confirmations": 20, "block_time": 3  },
    { "chain": "SOL",  "chain_id": null, "native_token": "SOL",  "confirmations": 32, "block_time": 1  }
  ]
}
GET /v1/system/tokens No Auth List supported tokens

Query Parameters

ParameterTypeRequiredDescription
chainstringOptionalFilter by chain (ETH, BSC, BASE, POLYGON, ARBITRUM, TRX, SOL)
Response
{
  "code": 0,
  "data": [
    { "chain": "ETH", "token": "USDT", "token_address": "0xdAC17F...", "decimals": 6, "is_native": 0, "min_deposit": "1.000000" },
    { "chain": "ETH", "token": "ETH",  "token_address": null,          "decimals": 18,"is_native": 1, "min_deposit": "0.001000" }
  ]
}

Sub-account Endpoints

Recommended deposit integration. A sub-account is a multi-chain deposit identity for one of your users. One API call derives deposit addresses on all supported chains (EVM, TRX, SOL) simultaneously and assigns a permanent human-readable unique code (account_no, e.g. WP-BE0A-7C3D-R2). All three endpoints require manage permission on your API key.
POST /v1/subaccount/create Auth Required · manage Create (or retrieve) a sub-account

Creates a sub-account for the given user_id and derives deposit addresses for all chains in one call. Idempotent — calling with the same user_id always returns the same sub-account and addresses.

Request Body

FieldTypeRequiredDescription
user_idstringRequiredYour internal user identifier (1–64 chars). Used as the idempotency key.
labelstringOptionalDisplay name shown in the merchant dashboard (max 64 chars). Defaults to user_id.
Request
POST /v1/subaccount/create
Content-Type: application/json

{ "user_id": "user_001", "label": "Alice" }
Response
{
  "code": 0,
  "data": {
    "account_no": "WP-BE0A-7C3D-R2",
    "sub_account_id": 15,
    "user_id": "user_001",
    "addresses": {
      "EVM": "0xbE0A19E174aAdC6f06952F2299A082B04ac17C3d",
      "TRX": "TXkxBEEQqKFrQL3m2LCsoVDvuRDGLyK7MY",
      "SOL": "4SkUmubQKFPaauHTAoZ9G3iqMphQyVWX2aJvMtpPaDhA"
    }
  }
}

Response Fields

FieldTypeDescription
account_nostringPermanent unique identifier for this sub-account (e.g. WP-BE0A-7C3D-R2). Safe to display to end users.
sub_account_idnumberInternal numeric ID.
user_idstringThe user identifier you provided.
addresses.EVMstringEVM-compatible address — works on ETH, BSC, BASE, and POLYGON.
addresses.TRXstringTRON deposit address.
addresses.SOLstringSolana deposit address.
GET /v1/subaccount/info Auth Required Get sub-account by user_id

Query Parameters

ParameterTypeRequiredDescription
user_idstringRequiredYour internal user identifier
Response
{
  "code": 0,
  "data": {
    "account_no": "WP-BE0A-7C3D-R2",
    "sub_account_id": 15,
    "user_id": "user_001",
    "addresses": {
      "EVM": "0xbE0A19E174aAdC6f06952F2299A082B04ac17C3d",
      "TRX": "TXkxBEEQqKFrQL3m2LCsoVDvuRDGLyK7MY",
      "SOL": "4SkUmubQKFPaauHTAoZ9G3iqMphQyVWX2aJvMtpPaDhA"
    }
  }
}
404 / 40401Sub-account not found — user has not been assigned one yet
GET /v1/subaccount/list Auth Required List all sub-accounts

Query Parameters

ParameterTypeRequiredDescription
pagenumberOptionalPage number (default: 1)
sizenumberOptionalPage size (default: 20, max: 100)
Response
{
  "code": 0,
  "data": {
    "total": 2,
    "page": 1,
    "size": 20,
    "list": [
      {
        "id": 15,
        "sub_index": 1,
        "account_no": "WP-BE0A-7C3D-R2",
        "user_ref": "user_001",
        "label": "Alice",
        "addresses": {
          "EVM": "0xbE0A19E174aAdC6f06952F2299A082B04ac17C3d",
          "TRX": "TXkxBEEQqKFrQL3m2LCsoVDvuRDGLyK7MY",
          "SOL": "4SkUmubQKFPaauHTAoZ9G3iqMphQyVWX2aJvMtpPaDhA"
        }
      }
    ]
  }
}

Address Endpoints

GET /v1/address/balance Auth Required Query on-chain balance

Query Parameters

ParameterTypeRequiredDescription
chainstringRequiredChain name
addressstringRequiredWallet address
tokenstringOptionalToken symbol (omit for native token)
Response
{
  "code": 0,
  "data": { "chain": "ETH", "address": "0xAbc...", "token": "USDT", "balance": "1250.50" }
}

Deposit Endpoints

GET /v1/deposit/list Auth Required List deposit records

Query Parameters

ParameterTypeRequiredDescription
user_idstringOptionalFilter by user
chainstringOptionalFilter by chain
tokenstringOptionalFilter by token
statusnumberOptional0=confirming, 2=credited
startstringOptionalStart date (YYYY-MM-DD)
endstringOptionalEnd date (YYYY-MM-DD)
pagenumberOptionalPage number (default: 1)
sizenumberOptionalPage size (default: 20, max: 100)
Response
{
  "code": 0,
  "data": {
    "total": 1,
    "list": [
      {
        "deposit_no": "DEP-20260523-001",
        "user_id": "user_001",
        "chain": "ETH",
        "address": "0xAbc...",
        "token": "USDT",
        "amount": "100.00",
        "tx_hash": "0x1234...",
        "block_number": "22000000",
        "confirmations": 12,
        "status": 2,
        "created_at": 1747994400,
        "credited_at": 1747994520
      }
    ]
  }
}
GET /v1/deposit/detail Auth Required Get deposit by deposit_no

Query Parameters

ParameterTypeRequiredDescription
deposit_nostringRequiredDeposit order number
Response
{
  "code": 0,
  "data": {
    "deposit_no": "DEP-20260523-001",
    "user_id": "user_001",
    "chain": "ETH",
    "address": "0xAbc...",
    "token": "USDT",
    "amount": "100.00",
    "tx_hash": "0x1234...",
    "block_number": "22000000",
    "confirmations": 12,
    "status": 2,
    "created_at": 1747994400,
    "credited_at": 1747994520
  }
}
POST /v1/deposit/replay_webhook Auth Required Resend deposit webhook notification

Re-queues a confirmed (status=2) deposit for webhook delivery. Useful if your endpoint was unavailable when the deposit was confirmed. Only works on credited deposits.

Request Body

FieldTypeRequiredDescription
deposit_nostringRequiredDeposit order number (must be status=2)
Request
POST /v1/deposit/replay_webhook
Content-Type: application/json

{ "deposit_no": "DEP-20260523-001" }
Response
{ "code": 0, "message": "Queued" }

Withdrawal Flow

Every withdrawal request goes through a risk-tiered approval workflow before the transaction is signed and broadcast.

API Call
Apply
status = 0
Pending
level 2 / 3 only
status = 1
Approved
level 1: instant
status = 2
Signing
status = 3
Broadcast
status = 4
Confirmed
status = 5 Failed — signing or broadcast error (check fail_reason)
status = 6 Rejected — admin rejected; funds never moved
statusNameDescription
0PendingAwaiting admin approval (level 2 or 3 only)
1ApprovedAuto-approved (level 1) or admin approved — queued for signing
2SigningSigner is reconstructing the key and building the transaction
3BroadcastTransaction sent to network — tx_hash is now set
4ConfirmedOn-chain confirmed — terminal success state
5FailedSigning or broadcast error — check fail_reason
6RejectedAdmin rejected — funds were never moved

Approval Levels

LevelUSD AmountApproval RequiredProcessing Time
1< $1,000None — auto-approved immediatelySeconds
2$1,000 – $49,999One admin approval in the consoleMinutes to hours
3≥ $50,000Two separate admin approvalsMinutes to hours
Amount classification uses the USD equivalent at the time of submission (via live price feed). Stablecoins (USDT, USDC, DAI) are always valued at $1.00.

Withdrawal Endpoints

POST /v1/withdraw/apply Auth Required manage Submit a withdrawal request
Approval thresholds:
Amount < $1,000 — auto-approved and broadcast immediately (level 1)
$1,000 – $50,000 — requires one admin approval in the console (level 2)
> $50,000 — requires two-admin approval (level 3)
Idempotent: Submitting the same biz_order_id twice returns the original record without creating a duplicate — as long as that record is still in a pending, approved, signing, or succeeded state.
Failed / rejected records are NOT replayable. Once a withdrawal terminates in the failed or rejected state, its biz_order_id is permanently consumed. Submitting the same biz_order_id again returns:
HTTP 409
{
  "code": 40902,
  "message": "Withdraw bizOrderId already used by failed record (withdrawNo=WD..., lastFailReason=...). Please retry with a new bizOrderId."
}
When retrying after a failure, generate a new ID — a common convention is to append a retry suffix such as {originalBizOrderId}_retry1, _retry2, etc. This guarantees each attempt has its own auditable record on WhatPay's side.
Pre-flight checks (validated before creating the record):
Balance — source wallet must hold sufficient funds (EVM chains checked in real time)
Whitelist — if your project has any whitelist entries, to_address must be on the list (add via the console)
Daily limit — if a per-token daily limit is configured, the request will fail with 42203 if it would be exceeded

Request Body

FieldTypeRequiredDescription
biz_order_idstringRequiredYour unique order ID (max 64 chars)
chainstringRequiredChain name. Canonical values: ETH, BSC, BASE, POLYGON, ARBITRUM, TRX, SOL. Common aliases are normalised on this endpoint: ETHEREUM / MAINNETETH; BNB / BINANCEBSC; MATICPOLYGON; ARBARBITRUM; TRONTRX; SOLANASOL. Any other value returns 40004 Unsupported chain. All endpoints (/v1/client/* and /v1/withdraw/*) accept the chain aliases listed above (TRON, ETHEREUM, MAINNET, BNB, BINANCE, MATIC, ARB, SOLANA, etc.). Note: /v1/client/send-tx, /v1/client/sign-message and /v1/client/sign-typed-data are EVM chains only — passing TRX or SOL returns 40004 Unsupported chain.
tokenstringRequiredToken symbol, e.g. USDT
to_addressstringRequiredRecipient address — EVM: 0x…, TRX: T…, SOL: base58
amountstringRequiredAmount as decimal string, e.g. "100.50"
memostringOptionalMemo / note (max 256 chars)
Request
POST /v1/withdraw/apply
Content-Type: application/json

{
  "biz_order_id": "order_20260523_001",
  "chain": "ETH",
  "token": "USDT",
  "to_address": "0xRecipientAddress...",
  "amount": "100.00"
}
Response
{
  "code": 0,
  "data": {
    "withdraw_no": "WD-20260523-001",
    "status": 0,
    "level": 1,
    "created_at": 1748000000
  }
}

See Withdrawal Flow for the complete status lifecycle (0 → 1 → 2 → 3 → 4, with 5 = failed and 6 = rejected as terminal error states).

GET /v1/withdraw/detail Auth Required Get withdrawal detail

Query Parameters

ParameterTypeRequiredDescription
biz_order_idstringOptionalYour order ID
withdraw_nostringOptionalWhatPay withdrawal number

At least one of biz_order_id or withdraw_no is required.

Response
{
  "code": 0,
  "data": {
    "withdraw_no": "WD-20260523-001",
    "biz_order_id": "order_20260523_001",
    "chain": "ETH",
    "token": "USDT",
    "to_address": "0xRecipientAddress...",
    "amount": "100.00",
    "amount_usd": 100.00,
    "status": 4,
    "level": 1,
    "tx_hash": "0xabc123...",
    "memo": null,
    "created_at": 1748000000,
    "confirmed_at": 1748000500
  }
}
POST /v1/withdraw/cancel Auth Required manage Cancel a pending withdrawal

Only withdrawals with status=0 (pending approval) can be cancelled. Once a withdrawal is approved (status=1) or completed (status=2), it cannot be cancelled.

Request Body

FieldTypeRequiredDescription
biz_order_idstringRequiredYour order ID
Request
POST /v1/withdraw/cancel
Content-Type: application/json

{ "biz_order_id": "order_20260523_001" }
Response
{ "code": 0, "message": "Cancelled" }
GET /v1/withdraw/list Auth Required List withdrawal records

Query Parameters

ParameterTypeRequiredDescription
statusnumberOptionalFilter by status: 0=pending, 1=approved, 2=signing, 3=broadcast, 4=confirmed, 5=failed, 6=rejected
chainstringOptionalFilter by chain
pagenumberOptionalPage number (default: 1)
sizenumberOptionalPage size (default: 20)
Response
{
  "code": 0,
  "data": {
    "total": 2,
    "list": [
      {
        "withdraw_no": "WD-20260523-001",
        "biz_order_id": "order_20260523_001",
        "chain": "ETH",
        "token": "USDT",
        "to_address": "0xRecipient...",
        "amount": "100.00",
        "status": 4,
        "level": 1,
        "tx_hash": "0xabc123...",
        "created_at": 1747994400,
        "confirmed_at": 1747994700
      }
    ]
  }
}

Swap & Bridge API

Give your end-users in-app token swap directly from their WhatPay sub-account wallets. Internally we aggregate quotes from KyberSwap (same-chain EVM swap) and route the trade end-to-end: report quote → execute on-chain → confirm → push swap.* webhook events.

MVP scope: Same-chain swaps on ETH / BSC / BASE / POLYGON / ARBITRUM via KyberSwap. Cross-chain (LiFi / Thorchain) and Solana (Jupiter) are coming in next phases — the API surface will not change, only the supported chain matrix.

Flow

┌────────────┐    quote     ┌────────────┐    execute   ┌────────────┐
│  Customer  │ ────────────▶│  WhatPay   │ ────────────▶│  WhatPay   │
│  business  │              │   Quote    │              │  Executor  │
│   system   │ ◀────────────│  (5 min)   │              │ (broadcast)│
└────────────┘   route_id   └────────────┘              └────────────┘
                                                              │
                                                              │ swap.broadcast / confirmed / failed
                                                              ▼
                                                       Your webhook URL

Status machine

statusMeaningtx_hash filled
0Pending — queued for signing
1Signing — being signed by HD signer
2Broadcast — sent on-chain, awaiting confirmationsrc_tx_hash
3Confirmed — receipt status=successsrc_tx_hash
5Failed — see fail_reasonsrc_tx_hash (if reverted) or null

GET /v1/swap/tokens

List supported chains (call without chain) or list tokens on a chain.

Query

ParamRequiredDescription
chainNoOne of ETH / BSC / BASE / POLYGON / ARBITRUM. If omitted, returns the list of supported chains.

Response (chain=BSC)

{
  "code": 0,
  "data": {
    "chain": "BSC",
    "tokens": [
      {"symbol":"BNB","address":"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE","decimals":18,"isNative":true},
      {"symbol":"USDT","address":"0x55d398326f99059fF775485246999027B3197955","decimals":18},
      {"symbol":"USDC","address":"0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d","decimals":18}
    ]
  }
}

POST /v1/swap/quote

Get a swap quote. The returned route_id is cached server-side and stays valid for 5 minutes. Pass it to /v1/swap/execute within that window.

Body

FieldTypeDescription
from_chainstringe.g. BSC
from_tokenstringToken symbol (e.g. USDT) or contract address
to_chainstringMust equal from_chain (MVP). Cross-chain returns 40020.
to_tokenstringSymbol or address
amount_instringHuman-readable amount, e.g. "1", "0.5"
from_addressstringEVM address of the source sub-account. Must belong to your project.
slippage_bpsnumber10 – 2000 (= 0.1% – 20%). Default 50.

Example

curl -X POST https://api.whatpay.com/v1/swap/quote \
  -H "X-App-Id: app_xxx" -H "X-Timestamp: ..." -H "X-Nonce: ..." -H "X-Signature: ..." \
  -H "X-Project-Slug: algowhirl" -H "Content-Type: application/json" \
  -d '{
    "from_chain":"BSC","from_token":"USDT",
    "to_chain":"BSC","to_token":"USDC",
    "amount_in":"1","from_address":"0x52...","slippage_bps":50
  }'

# Response
{
  "code": 0,
  "data": {
    "route_id": "rt_c7dd9179f7dac5e4e1923078",
    "provider": "KyberSwap",
    "from_chain": "BSC", "from_token": "USDT", "from_token_address": "0x55d...",
    "to_chain": "BSC",   "to_token": "USDC",   "to_token_address": "0x8AC...",
    "amount_in": "1", "amount_in_raw": "1000000000000000000",
    "amount_out_expected": "0.998884",
    "amount_out_min": "0.988895",
    "amount_in_usd": 1.0011, "amount_out_usd": 1.0011, "gas_usd": 0.098,
    "slippage_bps": 50,
    "router_address": "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5",
    "expires_at": 1780499620
  }
}

POST /v1/swap/execute

Submit a swap for on-chain execution. WhatPay HD signer signs the tx, broadcasts it, and pushes status updates through swap.* webhooks. Requires manage permission.

Body

FieldTypeDescription
biz_order_idstringYour internal ID. Unique per project, used for idempotency. If retried after a failure, use a new ID (e.g. append _retry1).
route_idstringFrom the preceding quote. Single-use, deleted after execute. 5-min TTL.
max_slippage_bpsnumberOptional. Must be ≥ the slippage from quote.
memostringOptional. Up to 256 chars, surfaced in webhook payloads.

Response

{
  "code": 0,
  "data": {
    "swap_no": "SWMPY7BO7LF65B0CD4",
    "biz_order_id": "swap_2026060401",
    "status": 0,
    "provider": "KyberSwap",
    "from_chain": "BSC", "from_token": "USDT", "to_token": "USDC",
    "amount_in": "1",
    "amount_out_expected": "0.998884",
    "amount_out_min": "0.988895",
    "slippage_bps": 50,
    "created_at": 1780499320
  }
}
ERC20 approve — if from_token is not the native gas token, the executor first checks allowance(from_address, router) and signs an approve tx if needed. The approve tx is broadcast and confirmed before the swap tx; you do not need to handle it.

Gas funding — the source sub-account must hold enough native token (BNB/ETH/MATIC/POL) to cover gas + value. WhatPay does not auto-fund gas; if balance is insufficient, the swap fails with status=5.

GET /v1/swap/detail

Look up by biz_order_id or swap_no. All timestamps are returned as Unix seconds.

Query

ParamRequiredDescription
biz_order_idOne ofYour internal ID
swap_noOne ofWhatPay-assigned SW... ID

Response (success swap, status=3)

{
  "code": 0,
  "data": {
    "swap_no": "SWMPY7BO7LF65B0CD4",
    "biz_order_id": "swap_2026060401",
    "from_chain": "BSC", "from_token": "USDT",
    "to_chain": "BSC", "to_token": "USDC",
    "from_address": "0x52...", "to_address": "0x52...",
    "amount_in": "1", "amount_out_expected": "0.998884",
    "amount_out_min": "0.988895", "amount_out_actual": "0.998910",
    "slippage_bps": 50,
    "provider": "KyberSwap", "status": 3,
    "src_tx_hash": "0xab12...",
    "dest_tx_hash": null,
    "gas_cost_usd": "0.082",
    "created_at": 1780499320, "signed_at": 1780499322,
    "sent_at": 1780499325, "confirmed_at": 1780499335
  }
}

GET /v1/swap/list

Paginated list, newest first.

Query

ParamDescription
pageDefault 1
sizeDefault 20, max 100
statusFilter by status (0/1/2/3/5)
from_chaine.g. BSC
to_chainsame

Response

{ "code": 0, "data": { "total": 42, "list": [ ... ], "page": 1, "size": 20 } }

Swap Webhooks

Three events are pushed to your webhook_url as a swap progresses. Same HMAC signature scheme as deposit/withdraw webhooks (X-Signature = HMAC-SHA256(timestamp + rawBody, webhook_secret)).

EventTriggered whenKey fields
swap.broadcastTx broadcast on-chain, status=2src_tx_hash, sent_at
swap.confirmedTx confirmed (receipt.status=success), status=3amount_out_actual, confirmed_at
swap.failedPre-broadcast error, on-chain revert, or insufficient gas. status=5fail_reason

Example payload (swap.confirmed)

{
  "event": "swap.confirmed",
  "timestamp": 1780499336,
  "data": {
    "swap_no": "SWMPY7BO7LF65B0CD4",
    "biz_order_id": "swap_2026060401",
    "provider": "KyberSwap",
    "from_chain": "BSC", "from_token": "USDT", "amount_in": "1",
    "to_chain": "BSC", "to_token": "USDC", "amount_out_actual": "0.998910",
    "src_tx_hash": "0xab12...",
    "status": 3,
    "confirmed_at": 1780499335
  }
}

Error Codes (Swap-specific)

40020Cross-chain not supported yet (MVP same-chain only) 40021Token not in supported list for this chain — call /v1/swap/tokens?chain=... 40022from_token equals to_token 40023from_address does not belong to a sub-account in your project 40024amount_in must be > 0 40030route_id expired or not found (re-quote) 40031max_slippage_bps smaller than quote's slippage 50202KyberSwap upstream quote failure 50204KyberSwap upstream build failure

Client Wallet API

The Client API lets your application drive the wallet directly: send transfers, sign messages / EIP-712 typed data, and call arbitrary contracts. Useful when you want to embed wallet operations into your own product without going through the withdrawal approval flow.

All endpoints under /v1/client/* require the same HMAC authentication as the rest of the API. Operations are tied to the wallet bound to your API credential (wallet_id).

Two ways to authorize an operation:
Session Key (auto) — include header X-Session-Key: sk_xxx. If the request matches the key's scope, it is queued and broadcast automatically.
Pending (manual) — without a session key, or out of scope, the operation lands in pending_client_action and waits for an admin to approve it in the console.

Operation status

statusNameDescription
0pendingAwaiting approval (no session key matched)
1queuedApproved / session-key matched — broadcaster will pick up
2sentTransaction broadcast — tx_hash set
3confirmedOn-chain confirmed (for sign-message / typed-data: signature returned)
4failedSign / broadcast error — check fail_reason
5rejectedAdmin rejected the pending action

Session Key

A session key is a pre-authorized signing right. The user (admin with approve permission) creates a key bound to a scope — chains, contract addresses, function selectors, native-token transfer policy, per-tx and daily USD caps. Any operation that fits the scope is signed without manual approval.

Scope fields

FieldTypeDescription
chainsstring[]Allowed chains. Empty = any chain bound to the wallet.
allow_contractsstring[]Allowed target addresses (lowercased). Empty = any.
allow_selectorsstring[]Allowed 4-byte function selectors, e.g. 0xa9059cbb (ERC20 transfer). Empty = any.
deny_native0 | 11 = block native-coin transfers. Default is now 0 (allowed).

risk_reason values (when an operation lands as pending)

risk_reasonMeaning
no_session_keyRequest did not include X-Session-Key
chain_not_in_scopeTarget chain not in scope.chains
contract_not_in_scopeTarget address not in allow_contracts
selector_not_in_scopecalldata selector not whitelisted
native_deniedNative-coin transfer blocked by deny_native
over_per_txAmount exceeds max_per_tx_usd
over_daily_capDaily total would exceed daily_cap_usd

Wallet Endpoints

GET /v1/client/wallet Auth Required Get the wallet's multi-chain hot addresses

Returns the hot address (addr_index=0) for every chain bound to your wallet.

Response
{
  "code": 0,
  "data": {
    "project_id": "proj_default",
    "wallet_id": 2,
    "addresses": [
      { "chain": "EVM", "address": "0x2531BA2fC2876f8dd712FC2Eef4e2fDA81673Eb6", "addr_index": 0 },
      { "chain": "TRX", "address": "TVYnQWsmDiM2AtxYooreYgYgtgRdEN3Kfu", "addr_index": 0 },
      { "chain": "SOL", "address": "7RaqwuvBzmEqWbfqGZiEUYdxfuMefXPHp4k1jaAzmdrR", "addr_index": 0 }
    ]
  }
}
POST /v1/client/transfer Auth Required manage High-level transfer (native or token)

Builds and broadcasts a transfer. Tokens are looked up in token_config; native vs ERC20/TRC20/SPL is auto-detected by the is_native flag.

Request Body

FieldTypeRequiredDescription
chainstringRequiredETH, BSC, BASE, POLYGON, ARBITRUM, TRX, SOL
fromstringOptionalSource address to sign from. Must be a derived address under this wallet (use /v1/client/addresses to list). Defaults to hot address (addr_index=0).
tokenstringRequiredToken symbol, e.g. USDC
tostringRequiredRecipient address
amountstringRequiredDecimal string, e.g. "12.5"
memostringOptionalFree text (max 256 chars)
Response — matched by a session key
{ "code": 0, "data": {
  "op_no": "op_871391ae831bdd3dfe03bcaf2535",
  "status": "queued",
  "decided_by": "session_key",
  "amount_usd": 12.5
} }
Response — no session key / out of scope
{ "code": 0, "data": {
  "op_no": "op_xxxx",
  "status": "pending",
  "decided_by": "pending",
  "risk_reason": "no_session_key",
  "expires_at": 1779964456
} }

Poll GET /v1/client/operations for the final state (status=2 sent → 3 confirmed, with tx_hash populated).

POST /v1/client/send-tx Auth Required manage Send an arbitrary EVM transaction

Low-level EVM transaction: any to, value, data. Gas is estimated automatically unless provided.

EVM only. For TRX / SOL contract calls, use /v1/client/transfer with the appropriate token symbol.

Request Body

FieldTypeRequiredDescription
chainstringRequiredETH, BSC, BASE, POLYGON, ARBITRUM
fromstringOptionalSource address. Defaults to hot address.
tostringRequiredTarget address 0x…
valuestringOptionalWei in hex, e.g. 0x0 (default 0)
datastringOptionalCalldata 0x… (default 0x)
gasstringOptionalManual gas limit (decimal)
Request
POST /v1/client/send-tx
{
  "chain": "BASE",
  "to": "0xUniswapV3Router...",
  "value": "0x0",
  "data": "0xa9059cbb000000000000000000000000..."
}
POST /v1/client/sign-message Auth Required manage Sign an EIP-191 personal_sign message

Sign a UTF-8 message with the wallet's EVM hot key. Used for dApp login (Sign-In-With-Ethereum), webhook verification, etc.

Request Body

FieldTypeRequiredDescription
chainstringRequiredEVM chain (used only for routing — signature is chain-agnostic)
fromstringOptionalSource address. Defaults to hot address.
messagestringRequiredUTF-8 text to sign
Final state — fetch via GET /v1/client/operations
{
  "op_no": "op_871391ae...",
  "op_type": "sign-message",
  "status": 3,
  "signature": "0xb00cbb8048b8a9efd7f7e5d297d887dab4c0bf66c69dbef9aa1fbd814f..."
}
POST /v1/client/sign-typed-data Auth Required manage Sign EIP-712 typed data

Sign structured data (EIP-712). Used for ERC20 Permit, OpenSea listings, Uniswap limit orders, etc.

Request Body

FieldTypeRequiredDescription
chainstringRequiredEVM chain
fromstringOptionalSource address. Defaults to hot address.
domainobjectRequiredEIP-712 domain (name, version, chainId, verifyingContract)
typesobjectRequiredType definitions
primaryTypestringRequiredThe primary type name
messageobjectRequiredThe message payload
Request — USDC Permit example
POST /v1/client/sign-typed-data
{
  "chain": "BASE",
  "domain": { "name":"USD Coin", "version":"2", "chainId":8453,
              "verifyingContract":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" },
  "types": {
    "Permit": [
      {"name":"owner","type":"address"},
      {"name":"spender","type":"address"},
      {"name":"value","type":"uint256"},
      {"name":"nonce","type":"uint256"},
      {"name":"deadline","type":"uint256"}
    ]
  },
  "primaryType": "Permit",
  "message": {
    "owner": "0x2531BA2fC2876f8dd712FC2Eef4e2fDA81673Eb6",
    "spender": "0xRouter...",
    "value": "1000000",
    "nonce": "0",
    "deadline": "1799999999"
  }
}
GET /v1/client/operations Auth Required List operations for this wallet

Query Parameters

ParameterTypeDescription
pagenumberPage number (default 1)
sizenumberPage size (default 20, max 100)
Response
{ "code": 0, "data": { "list": [
  {
    "op_no": "op_xxx", "op_type": "transfer",
    "chain": "BASE", "token": "USDC",
    "to_address": "0x...", "amount": "12.5", "amount_usd": "12.5000",
    "decided_by": "session_key",
    "status": 3,
    "tx_hash": "0xabc...",
    "signature": "0xb00cbb...",
    "fail_reason": null,
    "created_at": 1779931489,
    "sent_at":    1779931493,
    "confirmed_at": 1779931499
  }
] } }
GET /v1/client/pending Auth Required List pending actions awaiting approval

Returns operations with status=0 that have not yet expired. Approval is performed in the admin console.

Session Key Endpoints

POST /v1/client/session-keys Auth Required approve Create a session key
Keep sk_id secret. Anyone who has it can sign within the scope you configured. Treat it like a password.

Request Body

FieldTypeRequiredDescription
namestringOptionalDisplay label (max 64)
scopeobjectOptionalSee Scope fields. Omit to allow any chain / any contract / any selector. Default deny_native is now 0 — native-coin transfers allowed.
max_per_tx_usdnumberOptionalPer-transaction USD cap. Default 1e12 (effectively unlimited).
daily_cap_usdnumberOptionalDaily USD cap. Default 1e12 (effectively unlimited).
ttl_daysnumberOptionalLifetime in days. Default 365, max 3650.
Auto-approval is the default. With an empty body (POST {}) the resulting session key will sign any matching operation without manual approval. Tighten scope + the USD caps if you want a more restrictive key.
Request
POST /v1/client/session-keys
{
  "name": "OpenSea listings",
  "scope": {
    "chains": ["BASE"],
    "allow_contracts": ["0x0000000000000068f116a894984e2db1123eb395"],
    "allow_selectors": ["0xa9059cbb"],
    "deny_native": 1
  },
  "max_per_tx_usd": 100,
  "daily_cap_usd": 500,
  "ttl_days": 30
}
Response
{ "code": 0, "data": {
  "sk_id": "sk_033a4e2f1919aef3425a6d2d36234d3a3f9b",
  "expires_at": "2026-06-25T12:24:16.325Z"
} }
GET /v1/client/session-keys Auth Required List session keys for this wallet

Includes active, revoked, and expired keys. Use used_today_usd to monitor consumption against the daily cap.

DELETE /v1/client/session-keys/:sk_id Auth Required approve Revoke a session key immediately

Once revoked the key cannot be re-enabled. Subsequent requests carrying that sk_id fall back to pending.

Response
{ "code": 0 }

TRX Energy Service

Platform-managed TRX energy rental for all your TRC20 outbound transactions (sweep / withdraw / client-tx). After subscribing, you receive a project-specific TRX deposit address; any TRX you send there is automatically forwarded to the platform's GasStation account and credited to your internal balance. The platform then pays for energy on every TRC20 transfer you do, saving ~87% vs burning TRX. Subscribe once via POST /v1/energy/subscribe; the rest are read-only.

POST/v1/energy/subscribe

One-time call to enable the energy service for your project. Returns a TRX deposit address you can pre-fund. Idempotent — calling twice returns the existing subscription.

Request
POST /v1/energy/subscribe
{
  "low_balance_threshold": "50",       // optional, TRX, default 50
  "notify_webhook_url": "https://..."  // optional, for future low-balance alerts
}
Response
{
  "code": 0,
  "data": {
    "project_id":      "proj_xxxxxxxx",
    "deposit_address": "TEdFXUbhAEkVmYo8e9FjENvTpJeMcq3TT2",
    "enabled":         true,
    "balance_trx":     "0"
  }
}

Send TRX to deposit_address. Within ~60 seconds the platform auto-forwards it to GasStation and your internal balance increases.

GET/v1/energy/subscription

Full subscription state including current balance, totals, and deposit address. Returns null if not subscribed.

Response
{
  "code": 0,
  "data": {
    "project_id":             "proj_xxxxxxxx",
    "enabled":                1,
    "deposit_address":        "TEdFXUbhAE...",
    "balance_trx":            "85.342500",
    "total_topup_trx":        "200.000000",
    "total_consumed_trx":     "114.657500",
    "low_balance_threshold":  "50.000000",
    "notify_webhook_url":     null,
    "created_at":             "2026-05-28 17:25:00"
  }
}
GET/v1/energy/balance

Fast balance-only check. Use this for pre-flight before triggering large batches.

Response
{
  "code": 0,
  "data": {
    "balance_trx":  "85.342500",
    "low_balance":  false,
    "enabled":      true
  }
}
GET/v1/energy/topups

History of TRX top-ups credited to your account. status: 0 = received, 1 = settled to GasStation.

Query ParamTypeNotes
pageintdefault 1
limitintdefault 20, max 100
Response
{
  "code": 0,
  "data": {
    "list": [
      {
        "id":              1,
        "amount_trx":      "50.000000",
        "tx_hash":         "abc...",
        "settled_tx_hash": "def...",
        "status":          1,
        "block_time":      "2026-05-28 18:00:00",
        "created_at":      "2026-05-28 18:00:05"
      }
    ],
    "total": 1, "page": 1, "limit": 20
  }
}
GET/v1/energy/orders

Energy rental orders placed on your behalf. Each successful TRC20 outbound consumes one order.

Query ParamTypeNotes
business_typestringfilter: sweep / withdraw / client_tx
pageintdefault 1
limitintdefault 20, max 100

Status codes: 0 = ordered, 1 = delegated, 2 = failed, 3 = partial, 10 = recovered.

Response (excerpt)
{
  "code": 0,
  "data": {
    "list": [
      {
        "id":               42,
        "business_type":    "withdraw",
        "business_ref":     "wd_12345",
        "receive_address":  "THqUhi...",
        "energy_amount":    64400,
        "duration_code":    "10010",
        "cost_trx":         "1.752000",
        "status":           1,
        "gas_trade_no":     "GAS09442733",
        "created_at":       "2026-05-28 20:20:30"
      }
    ],
    "total": 142, "page": 1, "limit": 20
  }
}
GET/v1/energy/sweep-config

List TRX sweep configs for your project. One row per wallet.

Response
{
  "code": 0,
  "data": [
    {
      "id":                    1,
      "wallet_id":             7,
      "enabled":               1,
      "dest_sub_account_id":   35,
      "dest_address":          "THqUhi...",
      "min_usd":               "5.00",
      "duration_code":         "10010",
      "fallback_burn_enabled": 0,
      "fallback_burn_trx":     "30"
    }
  ]
}
POST/v1/energy/sweep-config

Create or update (upsert by wallet_id) the TRX sweep config. Required scope: manage.

FieldTypeRequiredNotes
wallet_idintYesunique key
dest_addressstringYesTRX base58 (T...)
dest_sub_account_idintNodisplay only
min_usdnumberNodefault 5.00
duration_codeenumNo10010 (10min, cheapest) / 20001 (1h) / 30001 (1d)
fallback_burn_enabledbooleanNoburn TRX if energy rental fails — requires explicit opt-in
fallback_burn_trxstringNoTRX to burn when fallback triggered, default 30
enabledbooleanNodefault true
Response
{ "code": 0, "data": { "ok": true } }

How Energy Costs Are Calculated

Compared with burning TRX (~13.5 TRX warm, ~27 TRX cold), this saves ~87% on every TRC20 outbound.

WhatPay API v1  ·  Need help? Contact your account manager.