Skip to content
Crypto Merchant

Developers

Sticky wallets

Give each of your customers their own deposit addresses that never change, one per network. Every deposit is added to your Crypto Merchant balance, and your server is told which customer sent it, so you can top up the right account.

By invitation

Crypto Merchant switches sticky wallets on for each business. Until then these endpoints answer 403 sticky_wallets_unavailable. They use the same base URL, secret keys and error format as the rest of the API.

Before you start

  1. 1

    Finish setup and choose your coins. Each customer gets an address on the networks of the coins you accept (portal, Settings).

  2. 2

    Create a secret key. In the portal open Developers and create an API key. It is shown once: keep it on your server, for example as CM_SECRET_KEY, never in a web page or an app.

  3. 3

    Add a webhook endpoint. Also under Developers, add your URL with the deposit.pending, deposit.confirmed and deposit.dropped events (or all events). Keep its signing secret, whsec_…, to check each request.

How it works

  1. 1

    A customer opens the deposit page on your site.

  2. 2

    Your server calls POST /sticky-wallets with your own id for that customer. The first call makes the addresses; every later call returns the same ones.

  3. 3

    You show the address, with a QR code, for the coin the customer picks.

  4. 4

    When the coins arrive you get deposit.pending, then deposit.confirmed once the network has confirmed them.

  5. 5

    On deposit.confirmed you credit the customer named in sticky_wallet.reference.

Get a customer's wallet

POST/sticky-wallets

Makes the customer's wallet on the first call and returns the same one on every later call: 201 when it was just made, 200 after that. It is safe to call every time the deposit page opens.

Body parameters

referenceRequiredstring
Your id for this customer: 1 to 120 letters, digits or . _ : @ + -, starting with a letter or digit. One wallet per reference, for good.
labelOptionalstring
A name you'll see in the portal. Sending it again replaces it.
metadataOptionalobject
An object of your own, up to 4 KB. Sending it again replaces it.
curl
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" }
  }'
Node.js
// 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: [...] }, …]
}

The sticky wallet object

idstring
The wallet's id, starting with sw_.
referencestring
Your id for the customer.
addressesarray
One entry per network: network (tron, evm, sol, btc, ltc or doge), the address and the coins it takes. The evm address is the same on Ethereum, BNB Smart Chain, Polygon, Base and Arbitrum.
pending_networksarray
Networks whose address is still being prepared, which is rare. Call again in a few minutes and they appear in addresses.
created_atstring
When the wallet was made.
sticky_wallet
{
  "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"
}

Tell customers what to send

Show each address with the coins listed for it. Coins on another network, or tokens that aren't listed, are not credited automatically: contact support if a customer sends one.

Find wallets

GET/sticky-wallets

GET/sticky-wallets/{id}

Look a wallet up by your reference, fetch one by its id, or page through all of them, newest first: limit from 1 to 100, starting_after the last wallet id you have, and has_more tells you whether there are more.

curl
# 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"
Response
{
  "object": "list",
  "data": [ { "id": "sw_8Kd2pQx7Vb1mNc3T", "object": "sticky_wallet", … } ],
  "has_more": false
}

List deposits

GET/deposits

Every deposit to your customers' wallets, newest first. Filter by sticky_wallet (a wallet id) and status (pending, confirmed or dropped), and page with limit and starting_after (a deposit id). Use it to catch up when your server missed webhooks.

curl
curl "https://cryptomerchant.online/api/v1/deposits?sticky_wallet=sw_8Kd2pQx7Vb1mNc3T&status=confirmed&limit=20" \
  -H "Authorization: Bearer $CM_SECRET_KEY"

The deposit object

idstring
The deposit's id. Use it to credit each deposit only once.
statusstring
pending: seen on the network, waiting for confirmations. confirmed: final, and added to your balance. dropped: it never confirmed.
sticky_walletobject
The wallet it was sent to: id and your reference.
coinobject
The coin and its network.
amountstring
What the customer sent, in the coin.
feestring
The fee, in the coin.
net_amountstring
What was added to your balance (amount minus fee), once confirmed.
amount_usdstring
Its value in USD when it arrived.
tx_hashstring
The transaction hash; explorer_url opens it on a block explorer.
deposit
{
  "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"
}

Webhooks

Deposit events carry the deposit object in data. Check the signature, as shown under Webhooks, before you trust one.

deposit.pending
A deposit to one of your customers' sticky wallets was seen on-chain and is waiting for confirmations.
deposit.confirmed
A sticky wallet deposit confirmed and was added to your balance, minus the fee: credit that customer now.
deposit.dropped
A sticky wallet deposit that was seen never confirmed (failed or removed from the chain).
server.js · credit the customer
import express from "express";
import { verifyWebhook } from "./verify-webhook.js"; // from 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);
    }
    res.sendStatus(200); // reply first, then do the work

    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: skip it if you have already stored deposit.id
      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,
      });
    }
  },
);

Errors

400
invalid_request: a field is missing or wrong; the message says which.
401
unauthorized: the secret key is missing or not valid.
403
sticky_wallets_unavailable: sticky wallets aren't switched on for this business, or its setup isn't finished.
404
not_found: no sticky wallet with that id.

Good practice

  • Credit a customer only on deposit.confirmed, and only once per deposit id: a webhook can arrive more than once.
  • Decide who pays the fee: credit net_amount to pass it on, or amount to cover it yourself.
  • Your reference is the key: calling POST /sticky-wallets again is always safe, so you don't have to store the addresses.
  • Keep the secret key on your server. If it leaks, revoke it under Developers and create a new one.
  • If your endpoint was down, list the confirmed deposits and credit the ones you haven't handled yet.