开发者
固定钱包
为您的每位客户提供专属且永不更改的充值地址,每个网络一个。每笔充值都会计入您的 Crypto Merchant 余额,并通知您的服务器是哪位客户发送的,以便您为正确的账户充值。
如何开通固定钱包
403 sticky_wallets_unavailable。它们与 API 的其他部分使用相同的基础 URL、密钥和错误格式。开始之前
- 1
完成设置并选择币种。每位客户都会在您接受的币种所在的网络上获得一个地址(后台的设置)。
- 2
创建密钥。在后台打开开发者并创建一个 API 密钥。密钥只显示一次:请将其保存在您的服务器上,例如保存为
CM_SECRET_KEY,切勿放入网页或应用中。 - 3
添加 Webhook 端点。同样在开发者中,添加您的 URL,并选择
deposit.pending、deposit.confirmed和deposit.dropped事件(或所有事件)。请保存其签名密钥whsec_…,用于校验每个请求。
工作原理
- 1
客户在您的网站上打开充值页面。
- 2
您的服务器使用您为该客户设定的 ID 调用
POST /sticky-wallets。首次调用会创建地址,之后的每次调用都返回相同的地址。 - 3
您为客户选择的币种显示对应的地址和二维码。
- 4
币到账时,您会收到
deposit.pending,网络确认后再收到deposit.confirmed。 - 5
收到
deposit.confirmed后,为sticky_wallet.reference中指明的客户入账。
获取客户的钱包
POST/sticky-wallets
首次调用时创建客户的钱包,之后每次调用都返回同一个钱包:刚创建时返回 201,之后返回 200。每次打开充值页面时都可以放心调用。
请求体参数
reference必填string- 您为该客户设定的 ID:1 到 120 个字母、数字或
. _ : @ + -,以字母或数字开头。每个标识永久对应一个钱包。 label可选string- 在后台显示的名称。再次发送会替换原有名称。
metadata可选object- 您自定义的对象,最大 4 KB。再次发送会替换原有内容。
curl https://cryptomerchant.online/api/v1/sticky-wallets \
-H "Authorization: Bearer $CM_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference": "user-1042",
"label": "Jane Doe",
"metadata": { "plan": "pro" }
}'// On your server, when a customer opens their deposit page
export async function depositAddresses(userId) {
const res = await fetch("https://cryptomerchant.online/api/v1/sticky-wallets", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CM_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ reference: String(userId) }),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
const wallet = await res.json();
return wallet.addresses; // [{ network, address, coins: [...] }, …]
}固定钱包对象
idstring- 钱包 ID,以
sw_开头。 referencestring- 您为该客户设定的 ID。
addressesarray- 每个网络一项:
network(tron、evm、sol、btc或ltc)、address以及该地址支持的coins。evm地址在 Ethereum、BNB Smart Chain、Polygon、Base 和 Arbitrum 上相同。 pending_networksarray- 地址仍在准备中的网络,这种情况很少见。几分钟后再次调用,它们就会出现在
addresses中。 created_atstring- 钱包的创建时间。
{
"id": "sw_8Kd2pQx7Vb1mNc3T",
"object": "sticky_wallet",
"reference": "user-1042",
"label": "Jane Doe",
"metadata": { "plan": "pro" },
"addresses": [
{
"network": "tron",
"address": "TXbajeGHq4Vb…",
"coins": [
{ "id": "TRX.TRON", "symbol": "TRX", "network": "Tron" },
{ "id": "USDT.TRON", "symbol": "USDT", "network": "Tron" }
]
},
{
"network": "evm",
"address": "0xE550EC6d…",
"coins": [
{ "id": "ETH.ETH", "symbol": "ETH", "network": "Ethereum" },
{ "id": "BNB.BSC", "symbol": "BNB", "network": "BNB Smart Chain" },
{ "id": "USDT.BSC", "symbol": "USDT", "network": "BNB Smart Chain" }
]
}
],
"pending_networks": [],
"created_at": "2026-10-29T18:20:41.000Z"
}告诉客户应发送什么
查找钱包
GET/sticky-wallets
GET/sticky-wallets/{id}
可按您的标识查找钱包、按 ID 获取单个钱包,或按最新在前的顺序分页浏览全部钱包:limit 为 1 到 100,starting_after 为您已有的最后一个钱包 ID,has_more 表示是否还有更多。
# By your reference
curl "https://cryptomerchant.online/api/v1/sticky-wallets?reference=user-1042" \
-H "Authorization: Bearer $CM_SECRET_KEY"
# By id
curl https://cryptomerchant.online/api/v1/sticky-wallets/sw_8Kd2pQx7Vb1mNc3T \
-H "Authorization: Bearer $CM_SECRET_KEY"
# All of them, 50 at a time
curl "https://cryptomerchant.online/api/v1/sticky-wallets?limit=50&starting_after=sw_8Kd2pQx7Vb1mNc3T" \
-H "Authorization: Bearer $CM_SECRET_KEY"{
"object": "list",
"data": [ { "id": "sw_8Kd2pQx7Vb1mNc3T", "object": "sticky_wallet", … } ],
"has_more": false
}列出充值
GET/deposits
您客户钱包收到的所有充值,最新的在前。可按 sticky_wallet(钱包 ID)和 status(pending、confirmed 或 dropped)筛选,并用 limit 和 starting_after(充值 ID)分页。当您的服务器漏收 Webhook 时,可用它补齐。
curl "https://cryptomerchant.online/api/v1/deposits?sticky_wallet=sw_8Kd2pQx7Vb1mNc3T&status=confirmed&limit=20" \
-H "Authorization: Bearer $CM_SECRET_KEY"充值对象
idstring- 充值 ID。用它确保每笔充值只入账一次。
statusstringpending:已在网络上检测到,正在等待确认。confirmed:已最终确认,并已计入您的余额。dropped:始终未确认。sticky_walletobject- 接收该笔充值的钱包:
id和您的reference。 coinobject- 币种及其网络。
amountstring- 客户发送的数额,以该币种计。
feestring- 手续费,以该币种计。
net_amountstring- 确认后计入您余额的数额(金额减去手续费)。
amount_usdstring- 到账时的美元价值。
tx_hashstring- 交易哈希;
explorer_url会在区块浏览器中打开该交易。
{
"id": "cmvu1x9a0000l10u8k2f3h7q",
"object": "deposit",
"status": "confirmed",
"sticky_wallet": { "id": "sw_8Kd2pQx7Vb1mNc3T", "reference": "user-1042" },
"coin": { "id": "USDT.TRON", "symbol": "USDT", "network": "Tron" },
"address": "TXbajeGHq4Vb…",
"amount": "250",
"amount_usd": "250.00",
"fee": "2.5",
"net_amount": "247.5",
"from_address": "TQn9Y2khEsLJ…",
"tx_hash": "36423ec5…f00d4a",
"explorer_url": "https://tronscan.org/#/transaction/36423ec5…f00d4a",
"confirmations": 19,
"detected_at": "2026-10-29T18:31:02.000Z",
"confirmed_at": "2026-10-29T18:32:01.000Z"
}Webhook
充值事件的 data 中包含充值对象。在信任事件之前,请按 Webhook 中的说明验证签名。
deposit.pending- 已在链上检测到一笔向您某位客户的固定钱包的充值,正在等待确认。
deposit.confirmed- 一笔固定钱包充值已确认,并在扣除手续费后计入您的余额:现在请为该客户入账。
deposit.dropped- 已检测到的一笔固定钱包充值始终未确认(交易失败或已从链上移除)。
import express from "express";
import { verifyWebhook } from "./verify-webhook.js"; // from the Webhooks page
import { db } from "./db.js"; // your database; table webhook_events as on the Webhooks page
const app = express();
app.post(
"/webhooks/crypto-merchant",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.get("CM-Signature"), process.env.CM_WEBHOOK_SECRET);
} catch {
return res.sendStatus(400);
}
try {
// Save first: a repeat delivery has the same event.id and is ignored
await db.query(
"INSERT INTO webhook_events (id, type, payload) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING",
[event.id, event.type, event],
);
} catch {
return res.sendStatus(500); // not saved: we send it again later
}
res.sendStatus(200); // stored, so confirm it
processEvents().catch(console.error); // or run it from a background job
},
);
async function processEvents() {
const { rows } = await db.query(
"SELECT payload FROM webhook_events WHERE processed_at IS NULL ORDER BY received_at",
);
for (const { payload: event } of rows) {
const deposit = event.data;
if (event.type === "deposit.pending") {
// Optional: show "deposit on its way" to deposit.sticky_wallet.reference
}
if (event.type === "deposit.confirmed") {
// Credit once: creditCustomer must skip a deposit.id it has already credited
await creditCustomer({
customerId: deposit.sticky_wallet.reference,
depositId: deposit.id,
coin: deposit.coin.id, // e.g. "USDT.TRON"
amount: deposit.net_amount, // or deposit.amount to cover the fee yourself
txHash: deposit.tx_hash,
});
}
await db.query("UPDATE webhook_events SET processed_at = now() WHERE id = $1", [event.id]);
}
}错误
400invalid_request:某个字段缺失或有误,错误信息会指明是哪个字段。401unauthorized:密钥缺失或无效。403sticky_wallets_unavailable:该商家未开通固定钱包,或尚未完成设置。404not_found:不存在该 ID 的固定钱包。
最佳实践
- 只在收到
deposit.confirmed时为客户入账,并且每个充值id只入账一次:同一个 Webhook 可能会多次送达。 - 决定由谁承担手续费:入账
net_amount表示由客户承担,入账amount表示由您自己承担。 - 关键在于您的标识:重复调用
POST /sticky-wallets始终是安全的,因此您无需保存这些地址。 - 请将密钥保存在您的服务器上。如果密钥泄露,请在开发者中将其撤销并创建新的密钥。
- 如果您的端点曾经无法访问,请列出
confirmed状态的充值,并为尚未处理的充值入账。 - 要为客户建立余额?请阅读如何为客户账户添加加密货币充值。