Integrate sha3 into your application to accept cryptocurrency payments. Merchant access is onboarding-controlled: complete account setup and any required review before using issued API credentials for server-to-server calls.
X-API-Key header for approved payment endpointsYour key is only used in-browser and never stored.
All payment API calls require an API Key sent in the X-API-Key header. API keys are activation-gated and should only be treated as usable after the merchant account has completed the required onboarding and review steps.
Include your API key in every request to /v1/pay/ endpoints.
GET /v1/pay/deposit-address?user_id=user123¤cy=USDT_TRC20
Host: api.sha3.net
X-API-Key: your-api-key-here⚠️ Never expose your API Key in client-side code. All payment API calls should be made from your server.
Onboarding is controlled. Creating an account does not automatically mean the merchant is approved for unrestricted production use, payouts, or live credential issuance.
API keys are random alphanumeric strings. They are stored as SHA-256 hashes in the database — the raw key is only shown once at creation time. Keep it secure.
sha3 follows the Google JSON Style Guide. Success and failure are indicated by HTTP status codes — there is no top-level success or ok field.
{
"data": {
"order_id": "abc-123",
"status": "pending"
}
}{
"error": {
"code": "INVALID_AMOUNT",
"message": "Amount must be positive"
}
}• Always check HTTP status code first: 2xx = success, 4xx/5xx = error
• error.code is a machine-readable string constant (e.g. INVALID_CURRENCY) — use it for programmatic error handling
• error.message is human-readable, may change — do not match on it
• Amounts are always decimal strings (e.g. "10.000000"), never floats
| Environment | API (Gateway) |
|---|---|
| Production | https://api.sha3.net |
• API (Gateway) — single public entry point for all /v1/pay/ endpoints. Production sits behind CloudFront for edge caching and DDoS protection.
• Portal API is an internal service (no public ingress). All portal routes go through the Gateway at /v1/portal/.
• Deposit widgets use deposit widget sessions and permanent account-bound addresses. Orders remain a separate API resource.
• Sandbox is not exposed as a separate public environment today. Chain-backed tests require a real observed transfer on the selected production network, so start with the smallest safe stablecoin amount.
These endpoints require the X-API-Key header. All payment endpoints use the /v1/pay/ prefix.
Order amounts are denominated in the declared currency. amount: "10.00" with currency: "ETH" means 10 ETH; currency: "USD" uses the settled deposit gross USD valuation. The current order API does not lock fiat quotes or apply FX/slippage logic. Settlement requires one matching deposit whose gross token or USD value meets or exceeds the order amount; partial deposits are credited independently but do not combine to fulfill an order.
/v1/pay/orders🔐 API KeyCreate a token- or USD-denominated order record. Orders do not create or host the deposit widget.
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | string | Required | Order target as a decimal string in the declared cryptocurrency unit, or in USD when currency is USD. |
currency | string | Required | Supported cryptocurrency code returned by GET /v1/currencies, or USD for gross USD valuation. |
description | string | Optional | Merchant-defined order description |
metadata | object | Optional | Arbitrary key-value metadata (your internal order ID, user info, etc.). Returned by the order API; not included in the current order.paid webhook payload. |
redirect_url | string | Optional | URL to redirect after successful payment |
cancel_url | string | Optional | URL to redirect on cancellation |
curl -X POST https://api.sha3.net/v1/pay/orders \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"amount": "10.00",
"currency": "USDT_TRC20",
"description": "Premium Plan",
"metadata": {
"merchant_order_id": "INV-10001",
"customer_id": "cus_123"
},
"redirect_url": "https://yoursite.com/success",
"cancel_url": "https://yoursite.com/cancel"
}'{
"data": {
"order_id": "ord_550e8400-e29b-41d4-a716-446655440000",
"expires_at": "2026-02-25T12:00:00Z"
}
}/v1/pay/orders/{order_id}🔐 API KeyGet order details by ID.
curl https://api.sha3.net/v1/pay/orders/ord_550e8400-e29b-41d4-a716-446655440000 \
-H "X-API-Key: your-api-key"{
"data": {
"order_id": "ord_550e8400-e29b-41d4-a716-446655440000",
"amount": "10.00",
"currency": "USDT_TRC20",
"status": "pending",
"selected_crypto": null,
"deposit_address": null,
"deposit_id": null,
"description": "Premium Plan",
"metadata": {
"merchant_order_id": "INV-10001",
"customer_id": "cus_123"
},
"redirect_url": "https://yoursite.com/success",
"cancel_url": "https://yoursite.com/cancel",
"expires_at": "2026-02-25T12:00:00Z",
"created_at": "2026-02-25T11:00:00Z",
"updated_at": "2026-02-25T11:00:00Z"
}
}/v1/pay/orders🔐 API KeyList orders for the authenticated merchant.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | Optional | Filter by status: pending, paid, expired |
limit | integer | Optional | Max results (default: 20, max: 100) |
offset | integer | Optional | Pagination offset (default: 0) |
/v1/pay/orders/{order_id}/select🔐 API KeySelect the cryptocurrency and allocate the order deposit address. Orders do not use the deposit widget UI.
| Parameter | Type | Required | Description |
|---|---|---|---|
currency | string | Required | Supported crypto currency code (e.g. "USDT_TRC20", "ETH"). Selection allocates an address; it does not quote fiat or change the stored order amount. |
{
"data": {
"order_id": "ord_550e8400-...",
"status": "pending",
"selected_crypto": "USDT_TRC20",
"deposit_address": "TXqH4v...",
...
}
}/v1/pay/deposit-address🔐 API KeyGet or allocate a permanent deposit address for a user. The same user always gets the same address (idempotent). Addresses are assigned from the address pool or generated on-demand via the wallet service.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Required | Unique user identifier in your system |
currency | string | Required | Currency code (e.g. "USDT_TRC20", "ETH") |
curl "https://api.sha3.net/v1/pay/deposit-address?user_id=user123¤cy=USDT_TRC20" \
-H "X-API-Key: your-api-key"{
"data": {
"address": "TXqH4v...",
"currency": "USDT_TRC20",
"network": "tron",
"chain_id": 728126428,
"user_id": "user123",
"created_at": "2026-02-25T10:30:00Z"
}
}/v1/pay/deposits🔐 API KeyList deposits for the authenticated merchant.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Optional | Filter by user ID |
limit | integer | Optional | Max results (default: 20, max: 100) |
offset | integer | Optional | Pagination offset (default: 0) |
{
"data": {
"deposits": [
{
"deposit_id": "dep_abc123",
"user_id": "user123",
"address": "TXqH4v...",
"currency": "USDT_TRC20",
"chain_id": 728126428,
"tx_hash": "0xabc...",
"amount": "100.000000",
"fee_amount": "1.000000",
"net_amount": "99.000000",
"status": "settled",
"created_at": "2026-02-25T10:00:00Z",
"confirmed_at": "2026-02-25T10:05:00Z",
"settled_at": "2026-02-25T10:05:10Z"
}
],
"total": 1,
"limit": 20,
"offset": 0
}
}/v1/pay/deposit/{deposit_id}🔐 API KeyGet a specific deposit by ID.
/v1/pay/balance🔐 API KeyGet the merchant's balance across all currencies. Available is spendable; frozen is held pending withdrawal completion.
{
"data": {
"balances": [
{ "currency": "USDT_TRC20", "chain_id": 728126428, "available": "1250.50", "frozen": "0.00" },
{ "currency": "ETH", "chain_id": 1, "available": "0.85", "frozen": "0.10" }
]
}
}/v1/pay/withdrawals/quote🔐 API KeyQuote the withdrawal fee for a gross debit amount. The quote is indicative unless you pass the returned fee_amount as expected_fee_amount when creating the withdrawal.
| Parameter | Type | Required | Description |
|---|---|---|---|
chain_id | integer | Required | Target blockchain chain ID (e.g. 728126428 for Tron, 1 for Ethereum) |
token_address | string | Optional | Token contract address. Omit or set null for native coins (ETH, TRX) |
amount | string | Required | Merchant gross debit amount as a plain decimal string in token units. Actual on-chain transfer amount is amount - fee_amount. |
curl -X POST https://api.sha3.net/v1/pay/withdrawals/quote \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"chain_id": 728126428,
"token_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"amount": "100.000000"
}'{
"data": {
"currency": "USDT_TRC20",
"amount": "100.000000",
"fee_amount": "0.100000"
}
}/v1/pay/withdrawals🔐 API KeyCreate a withdrawal request. amount is the merchant gross debit amount and is immediately frozen from available balance; the actual on-chain transfer amount is amount - fee_amount. Withdrawals within configured limits auto-approve; larger amounts stay pending until signer approval, then move through broadcasting → processing → completed.
| Parameter | Type | Required | Description |
|---|---|---|---|
to_address | string | Required | Destination wallet address (EIP-55 checksum for EVM chains) |
chain_id | integer | Required | Target blockchain chain ID (e.g. 728126428 for Tron, 1 for Ethereum) |
token_address | string | Optional | Token contract address. Omit or set null for native coins (ETH, TRX) |
amount | string | Required | Merchant gross debit amount as a plain decimal string in token units. Actual on-chain transfer amount is amount - fee_amount. Scientific notation and wei-style smallest-unit integers are not accepted. |
expected_fee_amount | string | Optional | Optional fee guard from the quote endpoint. If the current fee differs, creation fails with WITHDRAWAL_FEE_CHANGED. |
client_id | string | Optional | Optional merchant idempotency key. Reuse the same client_id when retrying the same withdrawal request. |
curl -X POST https://api.sha3.net/v1/pay/withdrawals \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"to_address": "TXqH4v...",
"chain_id": 728126428,
"token_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"amount": "100.000000",
"expected_fee_amount": "0.100000",
"client_id": "merchant_withdrawal_1001"
}'{
"data": {
"id": 42,
"to_address": "TXqH4v...",
"chain_id": 728126428,
"token_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"currency": "USDT_TRC20",
"client_id": "merchant_withdrawal_1001",
"amount": "100.000000",
"fee_amount": "0.100000",
"status": "pending",
"tx_hash": null,
"confirmations": 0,
"error_msg": null,
"requested_at": "2026-02-25T10:00:00Z",
"approved_at": null,
"completed_at": null,
"created_at": "2026-02-25T10:00:00Z"
}
}⚠️ Withdrawals exceeding your account's single-tx or daily limits stay pending until approval is submitted through POST /v1/withdrawals/{id}/approve with a signer key. Approved withdrawals then progress through broadcasting and processing before completion. If the platform cannot determine the broadcast outcome, the frozen balance remains held while the sha3 operations team verifies the chain result.
/v1/pay/withdrawals🔐 API KeyList withdrawals for the authenticated merchant.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | Optional | Filter by merchant-visible status: pending, approved, broadcasting, processing, completed, failed |
search | string | Optional | Search by withdrawal ID, destination address, tx hash, or client_id prefix |
limit | integer | Optional | Max results (default: 20, max: 100) |
offset | integer | Optional | Pagination offset (default: 0) |
{
"data": {
"withdrawals": [
{
"id": 42,
"to_address": "TXqH4v...",
"chain_id": 728126428,
"token_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"currency": "USDT_TRC20",
"client_id": "merchant_withdrawal_1001",
"amount": "100.000000",
"fee_amount": "0.100000",
"status": "processing",
"tx_hash": "f4b8...",
"confirmations": 12,
"error_msg": null,
"requested_at": "2026-02-25T10:00:00Z",
"approved_at": "2026-02-25T10:00:04Z",
"completed_at": null,
"created_at": "2026-02-25T10:00:00Z"
}
],
"total": 10,
"limit": 20,
"offset": 0
}
}In each withdrawal record, amount is the total balance debit and fee_amount is deducted from that amount before the on-chain transfer.
/v1/pay/withdrawals/{id}🔐 API KeyGet a specific withdrawal by numeric ID. The response uses the same record shape as list withdrawals.
{
"data": {
"id": 42,
"to_address": "TXqH4v...",
"chain_id": 728126428,
"token_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"currency": "USDT_TRC20",
"client_id": "merchant_withdrawal_1001",
"amount": "100.000000",
"fee_amount": "0.100000",
"status": "processing",
"tx_hash": "f4b8...",
"confirmations": 12,
"error_msg": null,
"requested_at": "2026-02-25T10:00:00Z",
"approved_at": "2026-02-25T10:00:04Z",
"completed_at": null,
"created_at": "2026-02-25T10:00:00Z"
}
}/v1/withdrawals/{id}/approve🔐 Signer KeyApprove or reject a pending withdrawal using a signer key. This route is only for configured signers and only works while the withdrawal is still pending.
X-Signer-Key: sk_live_xxx| Parameter | Type | Required | Description |
|---|---|---|---|
action | string | Required | Either approve or reject |
comment | string | Optional | Optional reviewer comment stored with the approval record |
chain_id | integer | Required | Chain id used when resolving the applicable multisig policy |
token_address | string | Optional | Token contract address for token withdrawals; omit or null for native assets |
curl -X POST https://api.sha3.net/v1/withdrawals/42/approve \
-H "X-Signer-Key: your-signer-key" \
-H "Content-Type: application/json" \
-d '{
"action": "approve",
"comment": "Within treasury policy",
"chain_id": 728126428,
"token_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
}'Rejecting a pending withdrawal moves it to failed and refunds the frozen balance back to available.
Create the deposit widget session on your server, return only its ID and client secret to your browser, and mount the widget with window.Sha3Deposit. Each settled transfer is credited independently and delivered through deposit.settled; the optional target is progress guidance, not an order balance.
| Parameter | Type | Required | Description |
|---|---|---|---|
SHA3_API_BASE_URL | string | Optional | Gateway API base URL. Production: https://api.sha3.net |
SHA3_FRONTEND_BASE_URL | string | Optional | Frontend base URL hosting /widget.js and deposit pages. Production: https://www.sha3.net |
SHA3_API_KEY | string | Required | Merchant API key. Keep it server-side only. |
SHA3_IPN_SECRET | string | Required | IPN secret from webhook configuration. Keep it server-side only. |
PUBLIC_BASE_URL | string | Required | Public HTTPS base URL for your app and the widget parent origin. |
npm init -y
npm install express
export SHA3_API_BASE_URL=https://api.sha3.net
export SHA3_FRONTEND_BASE_URL=https://www.sha3.net
export SHA3_API_KEY=your-api-key
export SHA3_IPN_SECRET=your-ipn-secret
export PUBLIC_BASE_URL=https://merchant.example.com
node server.mjsimport crypto from 'node:crypto';
import express from 'express';
const API_BASE_URL = process.env.SHA3_API_BASE_URL || 'https://api.sha3.net';
const FRONTEND_BASE_URL = process.env.SHA3_FRONTEND_BASE_URL || 'https://www.sha3.net';
const PUBLIC_BASE_URL = required('PUBLIC_BASE_URL');
const API_KEY = required('SHA3_API_KEY');
const IPN_SECRET = required('SHA3_IPN_SECRET');
const PORT = Number(process.env.PORT || 3000);
const app = express();
const seenNonces = new Set();
const processedEvents = new Set();
function required(name) {
const value = process.env[name];
if (!value) {
throw new Error(name + ' is required');
}
return value;
}
async function sha3Fetch(path, options = {}) {
const response = await fetch(API_BASE_URL + path, {
...options,
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
...(options.headers || {}),
},
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error('sha3 ' + response.status + ': ' + JSON.stringify(body));
}
return body.data;
}
function verifySignature(rawBody, timestamp, nonce, signature) {
if (!timestamp || !nonce || !signature) return false;
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false;
if (seenNonces.has(nonce)) return false;
const input = timestamp + '.' + nonce + '.' + rawBody;
const expected = crypto
.createHmac('sha512', IPN_SECRET)
.update(input, 'utf8')
.digest('hex');
const actualBuffer = Buffer.from(signature, 'hex');
const expectedBuffer = Buffer.from(expected, 'hex');
if (actualBuffer.length !== expectedBuffer.length) return false;
const valid = crypto.timingSafeEqual(actualBuffer, expectedBuffer);
if (valid) seenNonces.add(nonce);
return valid;
}
function eventKey(event) {
const reference = event.deposit_id || event.withdrawal_id || event.user_id;
return event.event + ':' + reference;
}
function originOf(url) {
return new URL(url).origin;
}
async function recordSettledDeposit(externalUserId, event) {
// Replace this with your durable notification or reconciliation workflow.
console.log('Settled deposit for', externalUserId, event);
}
app.post('/sha3/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const rawBody = req.body.toString('utf8');
const timestamp = req.get('x-timestamp');
const nonce = req.get('x-nonce');
const signature = req.get('x-signature');
const eventHeader = req.get('x-event');
if (!verifySignature(rawBody, timestamp, nonce, signature)) {
return res.status(401).send('invalid signature');
}
let event;
try {
event = JSON.parse(rawBody);
} catch {
return res.status(400).send('invalid json');
}
if (eventHeader !== event.event) {
return res.status(400).send('event mismatch');
}
const key = eventKey(event);
if (processedEvents.has(key)) {
return res.status(200).send('duplicate');
}
if (event.event === 'deposit.settled') {
await recordSettledDeposit(event.user_id, event);
}
processedEvents.add(key);
res.status(200).send('ok');
});
app.use(express.json());
app.post('/deposit-widget-sessions', async (req, res, next) => {
try {
const currencies = req.body.allowed_currencies || ['USDT_TRC20'];
const data = await sha3Fetch('/v1/pay/deposit-widget-sessions', {
method: 'POST',
body: JSON.stringify({
external_user_id: req.body.external_user_id,
mode: 'embedded',
allowed_currencies: currencies,
allowed_origins: [originOf(PUBLIC_BASE_URL)],
target_amount: req.body.target_amount,
target_currency: req.body.target_currency,
target_editable: Boolean(req.body.target_editable),
tolerance_bps: req.body.tolerance_bps || 0,
metadata: req.body.metadata,
}),
});
res.json({
deposit_widget_session_id: data.deposit_widget_session_id,
client_secret: data.client_secret,
hosted_url:
FRONTEND_BASE_URL +
data.hosted_url +
'#client_secret=' +
encodeURIComponent(data.client_secret),
widget_script_url: FRONTEND_BASE_URL + '/widget.js',
expires_at: data.expires_at,
});
} catch (error) {
next(error);
}
});
app.get('/deposits/:depositId', async (req, res, next) => {
try {
const deposit = await sha3Fetch('/v1/pay/deposits/' + encodeURIComponent(req.params.depositId));
res.json(deposit);
} catch (error) {
next(error);
}
});
app.use((error, _req, res, _next) => {
console.error(error);
res.status(500).json({ error: 'internal_error' });
});
app.listen(PORT, () => {
console.log('Merchant integration listening on :' + PORT);
});The in-memory nonce and event sets keep the example compact. In production, store webhook nonces and processed event keys in a durable database or Redis so retries and restarts stay idempotent.
curl -X POST http://localhost:3000/deposit-widget-sessions \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "customer_123",
"allowed_currencies": ["USDT_TRC20", "USDC_ETH"],
"target_amount": "100.00",
"target_currency": "USDT_TRC20",
"target_editable": true,
"tolerance_bps": 50
}'
# Mount the returned deposit_widget_session_id and client_secret with the widget.
# For hosted redirect, open hosted_url in the customer's browser.
# Listen for deposit.settled on your webhook for every settled transfer.<script src="https://www.sha3.net/widget.js"></script>
<div id="sha3-deposit"></div>
<script>
async function startDeposit() {
const response = await fetch('/deposit-widget-sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
external_user_id: 'customer_123',
allowed_currencies: ['USDT_TRC20'],
target_amount: '100.00',
target_currency: 'USDT_TRC20'
})
});
const session = await response.json();
window.Sha3Deposit.mount('#sha3-deposit', {
sessionId: session.deposit_widget_session_id,
clientSecret: session.client_secret,
baseUrl: 'https://www.sha3.net',
onProgress: event => console.log(event.progress),
onSettled: event => console.log(event.deposit),
onTargetReached: event => console.log(event.progress)
});
}
startDeposit();
</script>The API key stays on your server. The browser receives only the short-lived widget client secret. The deposit address remains bound to the external user after the display session expires.
Before sending chain funds, verify that your receiver accepts a correctly signed payload and rejects bad signatures. This smoke test uses a deposit event so it does not trigger order fulfillment.
node - <<'NODE'
const crypto = require('node:crypto');
const secret = process.env.SHA3_IPN_SECRET;
const body = JSON.stringify({
event: 'deposit.settled',
deposit_id: 'dep_test',
merchant_id: 1,
user_id: 'smoke-test-user',
amount: '10.000000',
fee_amount: '0.100000',
net_amount: '9.900000',
status: 'settled',
timestamp: new Date().toISOString(),
});
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const signature = crypto
.createHmac('sha512', secret)
.update(timestamp + '.' + nonce + '.' + body)
.digest('hex');
console.log('curl -X POST http://localhost:3000/sha3/webhook \\');
console.log(' -H "Content-Type: application/json" \\');
console.log(' -H "X-Timestamp: ' + timestamp + '" \\');
console.log(' -H "X-Nonce: ' + nonce + '" \\');
console.log(' -H "X-Signature: ' + signature + '" \\');
console.log(" --data '" + body.replace(/'/g, "'\\''") + "'");
NODEConfigure Deposit Webhook
Enable the deposit webhook endpoint, set it to https://your-public-domain/sha3/webhook, and save the deposit IPN secret. Local servers need an HTTPS tunnel.
Discover Currency
Call GET /v1/currencies and choose a currency supported by your test wallet and target chain.
Create Widget Session
Call your /deposit-widget-sessions endpoint and mount the returned session with window.Sha3Deposit.
Send Chain Deposit
Send any positive amount to the permanent account-bound address on the selected chain. Use the smallest safe amount for production networks.
Wait for Confirmations
The chain syncer must detect the transfer and wait for the required confirmations before settlement.
Receive deposit.settled
Your webhook receives one deposit.settled event for each settled transfer. Use payload.user_id as the external user binding.
Verify Progress
The widget status endpoint reports cumulative settled progress and recent independent deposits for this session.
Use live funds only after the merchant account, deposit webhook endpoint, and event idempotency are verified. There is no mock settlement callback from the production API; real deposit.settled delivery requires an observed on-chain transfer.