WhatPay API Reference
Multi-chain custody & payment API for ETH, BSC, BASE, POLYGON, TRX, and SOL
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
}
- Time fields (
created_at,updated_at,sent_at,confirmed_at,credited_at,approved_at,expires_at, webhooktimestamp) are returned as unix seconds (integer), never ISO strings.nullif not yet set. - BIGINT fields (
block_number) are returned as string to avoid JSNumberprecision 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:
| Permission | Capabilities |
|---|---|
| read | Query addresses, deposits, withdrawals (GET endpoints only) |
| manage | read + assign addresses, submit & cancel withdrawals |
| approve | manage + approve / reject withdrawal requests |
When a key is created, you receive three credentials — store them securely, they are shown only once:
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:
| Mode | Algorithm | Server holds | Best for |
|---|---|---|---|
| HMAC (default) | HMAC-SHA256 | Shared api_secret | Existing integrations, internal low-risk traffic |
| RSA (recommended) | SHA256WithRSA | Only 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:
| Header | Description |
|---|---|
| x-app-id | Your application ID (starts with app_) |
| x-timestamp | Current Unix timestamp (seconds). Request must be within ±300s of server time. |
| x-nonce | Random string (8–32 chars) for replay prevention |
| x-signature | HMAC-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)
- POST bodyMd5 — For POST requests, use
md5(actualRequestBody). For GET/DELETE requests with no body, usemd5("")=d41d8cd98f00b204e9800998ecf8427e. The sign string always has exactly 4 parts separated by\n. - api_secret encoding — Your
api_secretis 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/apiprefix.
/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:
- In-browser (recommended): when creating the key in the console, click "Generate key pair in browser (2048)". The browser uses WebCrypto to generate the pair locally; the private key is downloaded as
whatpay_private_key.pemand never leaves your machine. The public key is auto-filled in the form. - OpenSSL on your server:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out whatpay_private_key.pem openssl rsa -in whatpay_private_key.pem -pubout -out whatpay_public_key.pem # Paste the contents of whatpay_public_key.pem into the console.
2. Headers
Same four headers as HMAC mode. Only the encoding of x-signature differs.
| Header | Description |
|---|---|
| x-app-id | Your application ID |
| x-timestamp | Unix timestamp (seconds), ±300s window |
| x-nonce | Random string |
| x-signature | SHA256WithRSA 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:
- Is the algorithm SHA256WithRSA with PKCS#1 v1.5 padding? PSS will not be accepted.
- Is the signature base64-encoded (with
=padding)? Hex will not be accepted in RSA mode. - Is the sign-string built with
\n(LF) line breaks, not\r\n? - Is the
pathexactly the server-side route (e.g./v1/withdraw/list), without the slug prefix or query-string re-ordering? - Has your local clock drifted more than ±300s from server time? Run
ntpdate/ check OS time sync.
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
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.
deposit.pending event — credit your user only when this event arrives.
| status | Description | confirmations |
|---|---|---|
| 0 | Detected on-chain, waiting for block confirmations | 0 → required threshold |
| 2 | Fully confirmed and credited to user balance | ≥ threshold |
Confirmation Thresholds by Chain
| Chain | Required Confirmations | Approximate Time |
|---|---|---|
| ETH | 12 blocks | ~2.5 minutes |
| BSC | 15 blocks | ~45 seconds |
| BASE | 15 blocks | ~30 seconds |
| POLYGON | 30 blocks | ~60 seconds |
| ARBITRUM | 2 blocks | ~2 seconds |
| TRX | 20 blocks | ~60 seconds |
| SOL | 32 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.
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:
| Attempt | Wait before retry |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes |
| 3rd retry | 10 minutes |
| 4th retry | 1 hour |
| 5th retry | 6 hours |
| 6th retry | 24 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.
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.
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).
| Event | Trigger | Includes |
|---|---|---|
client.sign-message.confirmed | personal_sign completed | signature |
client.sign-typed-data.confirmed | EIP-712 typed-data signed | signature |
client.transfer.confirmed | Token transfer on-chain confirmed | tx_hash |
client.send-tx.confirmed | Arbitrary EVM tx on-chain confirmed | tx_hash |
client.{op_type}.failed | Signing or broadcast failed | fail_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
}
}
System Endpoints
{ "code": 0, "data": { "status": "ok", "time": "2026-05-23T10:00:00.000Z" } }
{
"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 }
]
}
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| chain | string | Optional | Filter by chain (ETH, BSC, BASE, POLYGON, ARBITRUM, TRX, SOL) |
{
"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
account_no, e.g. WP-BE0A-7C3D-R2).
All three endpoints require manage permission on your API key.
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
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | string | Required | Your internal user identifier (1–64 chars). Used as the idempotency key. |
| label | string | Optional | Display name shown in the merchant dashboard (max 64 chars). Defaults to user_id. |
POST /v1/subaccount/create
Content-Type: application/json
{ "user_id": "user_001", "label": "Alice" }
{
"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
| Field | Type | Description |
|---|---|---|
| account_no | string | Permanent unique identifier for this sub-account (e.g. WP-BE0A-7C3D-R2). Safe to display to end users. |
| sub_account_id | number | Internal numeric ID. |
| user_id | string | The user identifier you provided. |
| addresses.EVM | string | EVM-compatible address — works on ETH, BSC, BASE, and POLYGON. |
| addresses.TRX | string | TRON deposit address. |
| addresses.SOL | string | Solana deposit address. |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| user_id | string | Required | Your internal user identifier |
{
"code": 0,
"data": {
"account_no": "WP-BE0A-7C3D-R2",
"sub_account_id": 15,
"user_id": "user_001",
"addresses": {
"EVM": "0xbE0A19E174aAdC6f06952F2299A082B04ac17C3d",
"TRX": "TXkxBEEQqKFrQL3m2LCsoVDvuRDGLyK7MY",
"SOL": "4SkUmubQKFPaauHTAoZ9G3iqMphQyVWX2aJvMtpPaDhA"
}
}
}
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | number | Optional | Page number (default: 1) |
| size | number | Optional | Page size (default: 20, max: 100) |
{
"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
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| chain | string | Required | Chain name |
| address | string | Required | Wallet address |
| token | string | Optional | Token symbol (omit for native token) |
{
"code": 0,
"data": { "chain": "ETH", "address": "0xAbc...", "token": "USDT", "balance": "1250.50" }
}
Deposit Endpoints
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| user_id | string | Optional | Filter by user |
| chain | string | Optional | Filter by chain |
| token | string | Optional | Filter by token |
| status | number | Optional | 0=confirming, 2=credited |
| start | string | Optional | Start date (YYYY-MM-DD) |
| end | string | Optional | End date (YYYY-MM-DD) |
| page | number | Optional | Page number (default: 1) |
| size | number | Optional | Page size (default: 20, max: 100) |
{
"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
}
]
}
}
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| deposit_no | string | Required | Deposit order number |
{
"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
}
}
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
| Field | Type | Required | Description |
|---|---|---|---|
| deposit_no | string | Required | Deposit order number (must be status=2) |
POST /v1/deposit/replay_webhook
Content-Type: application/json
{ "deposit_no": "DEP-20260523-001" }
{ "code": 0, "message": "Queued" }
Withdrawal Flow
Every withdrawal request goes through a risk-tiered approval workflow before the transaction is signed and broadcast.
fail_reason)✕ status = 6 Rejected — admin rejected; funds never moved
| status | Name | Description |
|---|---|---|
| 0 | Pending | Awaiting admin approval (level 2 or 3 only) |
| 1 | Approved | Auto-approved (level 1) or admin approved — queued for signing |
| 2 | Signing | Signer is reconstructing the key and building the transaction |
| 3 | Broadcast | Transaction sent to network — tx_hash is now set |
| 4 | Confirmed | On-chain confirmed — terminal success state |
| 5 | Failed | Signing or broadcast error — check fail_reason |
| 6 | Rejected | Admin rejected — funds were never moved |
Approval Levels
| Level | USD Amount | Approval Required | Processing Time |
|---|---|---|---|
| 1 | < $1,000 | None — auto-approved immediately | Seconds |
| 2 | $1,000 – $49,999 | One admin approval in the console | Minutes to hours |
| 3 | ≥ $50,000 | Two separate admin approvals | Minutes to hours |
Withdrawal Endpoints
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)
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 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.
• 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
| Field | Type | Required | Description |
|---|---|---|---|
| biz_order_id | string | Required | Your unique order ID (max 64 chars) |
| chain | string | Required | Chain name. Canonical values: ETH, BSC, BASE, POLYGON, ARBITRUM, TRX, SOL. Common aliases are normalised on this endpoint: ETHEREUM / MAINNET → ETH; BNB / BINANCE → BSC; MATIC → POLYGON; ARB → ARBITRUM; TRON → TRX; SOLANA → SOL. 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. |
| token | string | Required | Token symbol, e.g. USDT |
| to_address | string | Required | Recipient address — EVM: 0x…, TRX: T…, SOL: base58 |
| amount | string | Required | Amount as decimal string, e.g. "100.50" |
| memo | string | Optional | Memo / note (max 256 chars) |
POST /v1/withdraw/apply
Content-Type: application/json
{
"biz_order_id": "order_20260523_001",
"chain": "ETH",
"token": "USDT",
"to_address": "0xRecipientAddress...",
"amount": "100.00"
}
{
"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).
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| biz_order_id | string | Optional | Your order ID |
| withdraw_no | string | Optional | WhatPay withdrawal number |
At least one of biz_order_id or withdraw_no is required.
{
"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
}
}
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
| Field | Type | Required | Description |
|---|---|---|---|
| biz_order_id | string | Required | Your order ID |
POST /v1/withdraw/cancel
Content-Type: application/json
{ "biz_order_id": "order_20260523_001" }
{ "code": 0, "message": "Cancelled" }
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | number | Optional | Filter by status: 0=pending, 1=approved, 2=signing, 3=broadcast, 4=confirmed, 5=failed, 6=rejected |
| chain | string | Optional | Filter by chain |
| page | number | Optional | Page number (default: 1) |
| size | number | Optional | Page size (default: 20) |
{
"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.
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
| status | Meaning | tx_hash filled |
|---|---|---|
| 0 | Pending — queued for signing | — |
| 1 | Signing — being signed by HD signer | — |
| 2 | Broadcast — sent on-chain, awaiting confirmation | src_tx_hash |
| 3 | Confirmed — receipt status=success | src_tx_hash |
| 5 | Failed — see fail_reason | src_tx_hash (if reverted) or null |
GET /v1/swap/tokens
List supported chains (call without chain) or list tokens on a chain.
Query
| Param | Required | Description |
|---|---|---|
| chain | No | One 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
| Field | Type | Description |
|---|---|---|
| from_chain | string | e.g. BSC |
| from_token | string | Token symbol (e.g. USDT) or contract address |
| to_chain | string | Must equal from_chain (MVP). Cross-chain returns 40020. |
| to_token | string | Symbol or address |
| amount_in | string | Human-readable amount, e.g. "1", "0.5" |
| from_address | string | EVM address of the source sub-account. Must belong to your project. |
| slippage_bps | number | 10 – 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
| Field | Type | Description |
|---|---|---|
| biz_order_id | string | Your internal ID. Unique per project, used for idempotency. If retried after a failure, use a new ID (e.g. append _retry1). |
| route_id | string | From the preceding quote. Single-use, deleted after execute. 5-min TTL. |
| max_slippage_bps | number | Optional. Must be ≥ the slippage from quote. |
| memo | string | Optional. 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
}
}
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
| Param | Required | Description |
|---|---|---|
| biz_order_id | One of | Your internal ID |
| swap_no | One of | WhatPay-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
| Param | Description |
|---|---|
| page | Default 1 |
| size | Default 20, max 100 |
| status | Filter by status (0/1/2/3/5) |
| from_chain | e.g. BSC |
| to_chain | same |
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)).
| Event | Triggered when | Key fields |
|---|---|---|
swap.broadcast | Tx broadcast on-chain, status=2 | src_tx_hash, sent_at |
swap.confirmed | Tx confirmed (receipt.status=success), status=3 | amount_out_actual, confirmed_at |
swap.failed | Pre-broadcast error, on-chain revert, or insufficient gas. status=5 | fail_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)
/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).
• 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
| status | Name | Description |
|---|---|---|
| 0 | pending | Awaiting approval (no session key matched) |
| 1 | queued | Approved / session-key matched — broadcaster will pick up |
| 2 | sent | Transaction broadcast — tx_hash set |
| 3 | confirmed | On-chain confirmed (for sign-message / typed-data: signature returned) |
| 4 | failed | Sign / broadcast error — check fail_reason |
| 5 | rejected | Admin 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
| Field | Type | Description |
|---|---|---|
| chains | string[] | Allowed chains. Empty = any chain bound to the wallet. |
| allow_contracts | string[] | Allowed target addresses (lowercased). Empty = any. |
| allow_selectors | string[] | Allowed 4-byte function selectors, e.g. 0xa9059cbb (ERC20 transfer). Empty = any. |
| deny_native | 0 | 1 | 1 = block native-coin transfers. Default is now 0 (allowed). |
risk_reason values (when an operation lands as pending)
| risk_reason | Meaning |
|---|---|
| no_session_key | Request did not include X-Session-Key |
| chain_not_in_scope | Target chain not in scope.chains |
| contract_not_in_scope | Target address not in allow_contracts |
| selector_not_in_scope | calldata selector not whitelisted |
| native_denied | Native-coin transfer blocked by deny_native |
| over_per_tx | Amount exceeds max_per_tx_usd |
| over_daily_cap | Daily total would exceed daily_cap_usd |
Wallet Endpoints
Returns the hot address (addr_index=0) for every chain bound to your wallet.
{
"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 }
]
}
}
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
| Field | Type | Required | Description |
|---|---|---|---|
| chain | string | Required | ETH, BSC, BASE, POLYGON, ARBITRUM, TRX, SOL |
| from | string | Optional | Source 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). |
| token | string | Required | Token symbol, e.g. USDC |
| to | string | Required | Recipient address |
| amount | string | Required | Decimal string, e.g. "12.5" |
| memo | string | Optional | Free text (max 256 chars) |
{ "code": 0, "data": {
"op_no": "op_871391ae831bdd3dfe03bcaf2535",
"status": "queued",
"decided_by": "session_key",
"amount_usd": 12.5
} }
{ "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).
Low-level EVM transaction: any to, value, data. Gas is estimated automatically unless provided.
/v1/client/transfer with the appropriate token symbol.Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| chain | string | Required | ETH, BSC, BASE, POLYGON, ARBITRUM |
| from | string | Optional | Source address. Defaults to hot address. |
| to | string | Required | Target address 0x… |
| value | string | Optional | Wei in hex, e.g. 0x0 (default 0) |
| data | string | Optional | Calldata 0x… (default 0x) |
| gas | string | Optional | Manual gas limit (decimal) |
POST /v1/client/send-tx
{
"chain": "BASE",
"to": "0xUniswapV3Router...",
"value": "0x0",
"data": "0xa9059cbb000000000000000000000000..."
}
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
| Field | Type | Required | Description |
|---|---|---|---|
| chain | string | Required | EVM chain (used only for routing — signature is chain-agnostic) |
| from | string | Optional | Source address. Defaults to hot address. |
| message | string | Required | UTF-8 text to sign |
{
"op_no": "op_871391ae...",
"op_type": "sign-message",
"status": 3,
"signature": "0xb00cbb8048b8a9efd7f7e5d297d887dab4c0bf66c69dbef9aa1fbd814f..."
}
Sign structured data (EIP-712). Used for ERC20 Permit, OpenSea listings, Uniswap limit orders, etc.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| chain | string | Required | EVM chain |
| from | string | Optional | Source address. Defaults to hot address. |
| domain | object | Required | EIP-712 domain (name, version, chainId, verifyingContract) |
| types | object | Required | Type definitions |
| primaryType | string | Required | The primary type name |
| message | object | Required | The message payload |
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"
}
}
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| page | number | Page number (default 1) |
| size | number | Page size (default 20, max 100) |
{ "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
}
] } }
Returns operations with status=0 that have not yet expired. Approval is performed in the admin console.
Session Key Endpoints
sk_id secret. Anyone who has it can sign within the scope you configured. Treat it like a password.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Optional | Display label (max 64) |
| scope | object | Optional | See Scope fields. Omit to allow any chain / any contract / any selector. Default deny_native is now 0 — native-coin transfers allowed. |
| max_per_tx_usd | number | Optional | Per-transaction USD cap. Default 1e12 (effectively unlimited). |
| daily_cap_usd | number | Optional | Daily USD cap. Default 1e12 (effectively unlimited). |
| ttl_days | number | Optional | Lifetime in days. Default 365, max 3650. |
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.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
}
{ "code": 0, "data": {
"sk_id": "sk_033a4e2f1919aef3425a6d2d36234d3a3f9b",
"expires_at": "2026-06-25T12:24:16.325Z"
} }
Includes active, revoked, and expired keys. Use used_today_usd to monitor consumption against the daily cap.
Once revoked the key cannot be re-enabled. Subsequent requests carrying that sk_id fall back to pending.
{ "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.
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.
POST /v1/energy/subscribe
{
"low_balance_threshold": "50", // optional, TRX, default 50
"notify_webhook_url": "https://..." // optional, for future low-balance alerts
}
{
"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.
Full subscription state including current balance, totals, and deposit address. Returns null if not subscribed.
{
"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"
}
}
Fast balance-only check. Use this for pre-flight before triggering large batches.
{
"code": 0,
"data": {
"balance_trx": "85.342500",
"low_balance": false,
"enabled": true
}
}
History of TRX top-ups credited to your account. status: 0 = received, 1 = settled to GasStation.
| Query Param | Type | Notes |
|---|---|---|
| page | int | default 1 |
| limit | int | default 20, max 100 |
{
"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
}
}
Energy rental orders placed on your behalf. Each successful TRC20 outbound consumes one order.
| Query Param | Type | Notes |
|---|---|---|
| business_type | string | filter: sweep / withdraw / client_tx |
| page | int | default 1 |
| limit | int | default 20, max 100 |
Status codes: 0 = ordered, 1 = delegated, 2 = failed, 3 = partial, 10 = recovered.
{
"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
}
}
List TRX sweep configs for your project. One row per wallet.
{
"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"
}
]
}
Create or update (upsert by wallet_id) the TRX sweep config. Required scope: manage.
| Field | Type | Required | Notes |
|---|---|---|---|
| wallet_id | int | Yes | unique key |
| dest_address | string | Yes | TRX base58 (T...) |
| dest_sub_account_id | int | No | display only |
| min_usd | number | No | default 5.00 |
| duration_code | enum | No | 10010 (10min, cheapest) / 20001 (1h) / 30001 (1d) |
| fallback_burn_enabled | boolean | No | burn TRX if energy rental fails — requires explicit opt-in |
| fallback_burn_trx | string | No | TRX to burn when fallback triggered, default 30 |
| enabled | boolean | No | default true |
{ "code": 0, "data": { "ok": true } }
How Energy Costs Are Calculated
- Recipient already holds the TRC20 token (warm): 64,400 energy ≈ 1.75 TRX (10-min rental)
- Recipient never held it (cold): 130,400 energy ≈ 3.50 TRX (10-min rental)
- Sender already has staked energy: 0 cost — no rental placed
- Same sender within 10 minutes of a successful rental: 0 cost — energy is still delegated
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.