Migrate from Cryptomus

Move from Cryptomus to CoinCircuit: swap request signing for an API key, update webhook verification, and map every field.

Open the interactive version or read this guide as markdown.

CoinCircuit and Cryptomus solve the same problem. You create a payment, your customer sends crypto to an address, and your server gets a callback when it settles. The concepts line up almost one to one. The real work is in two places: request authentication and webhook verification. Cryptomus signs every request body with MD5; CoinCircuit uses a static API key header and signs webhooks with HMAC-SHA256. Both changes delete code rather than add it.

Concept mapping

Cryptomus CoinCircuit Notes
Payment Payment (checkout session) One charge, one address, one webhook.
uuid / order_id reference / metadata reference is CoinCircuit's handle; put your own ID in metadata.
Static wallet Deposit account A reusable address that credits your balance.
Recurring payment Invoice + webhooks Rebuild the schedule on your side; bill with invoices.
Callback (url_callback) Webhook (webhookUrl) Same delivery model, different signing.
sign field in the body x-coincircuit-signature header Moves out of the payload and into a header.
Payment API key + Payout API key One API key CoinCircuit does not split keys by operation.
Merchant balance Balance Per-asset balance credited after each payment.
Payout Payout Save a recipient once, then pay out to it.

Step 1: Replace request signing with an API key

This is the biggest change, and it removes code.

Cryptomus (before)

Every request carried a merchant header and a sign header. The signature was an MD5 of the base64-encoded JSON body concatenated with your API key, recomputed per request.

const body = JSON.stringify(payload);
const sign = crypto
  .createHash('md5')
  .update(Buffer.from(body).toString('base64') + API_KEY)
  .digest('hex');

await fetch('https://api.cryptomus.com/v1/payment', {
  method: 'POST',
  headers: {
    merchant: MERCHANT_UUID,
    sign,
    'Content-Type': 'application/json',
  },
  body,
});

CoinCircuit (after)

One static header. Delete the signing helper.

POST https://api.coincircuit.io/api/v1/payments
x-api-key: sk_live_your_key
Content-Type: application/json
- merchant: 8b03432e-385b-4670-8d06-064591096795
- sign: 8b03432e385b46708d06064591096795
+ x-api-key: sk_live_your_coincircuit_key

Change your base URL:

- https://api.cryptomus.com/v1
+ https://api.coincircuit.io/api/v1

Generate your key in the dashboard under Developers > API keys. Keep it server-side.


Step 2: Create payments

Cryptomus (before)

POST /v1/payment
{
  "amount": "100",
  "currency": "USD",
  "order_id": "order_123",
  "url_callback": "https://your-site.com/webhook",
  "url_return": "https://your-site.com/cart",
  "url_success": "https://your-site.com/success",
  "lifetime": 3600
}

CoinCircuit (after)

POST https://api.coincircuit.io/api/v1/payments
x-api-key: sk_live_your_key
Content-Type: application/json
{
  "title": "Order #order_123",
  "description": "T-shirt",
  "amount": "100.00",
  "currency": "USD",
  "customer": { "email": "customer@example.com" },
  "webhookUrl": "https://your-site.com/webhook",
  "successUrl": "https://your-site.com/success",
  "cancelUrl": "https://your-site.com/cart",
  "metadata": { "orderId": "order_123" }
}

The response is wrapped in the standard { success, message, data } envelope. Redirect the customer to data.url for the hosted checkout, and store data.reference against your order.

Field mapping

Cryptomus field CoinCircuit field Notes
amount amount Send as a string, e.g. "100.00".
currency currency NGN or USD.
to_currency asset Optional. Omit to let the customer choose the asset.
network chain Optional. Omit to let the customer choose the network.
order_id metadata Any key-value data; it comes back on every webhook.
url_callback webhookUrl Per-payment webhook URL.
url_success successUrl Redirect after a successful payment.
url_return cancelUrl Redirect when the customer backs out.
is_payment_multiple CoinCircuit accepts additional payments while the session is open.
lifetime Expiry is set by CoinCircuit and returned as data.expiresAt.
subtract feePaidBy "customer" or "merchant"; defaults to your dashboard setting.

Cryptomus returns both address and url. If you rendered your own payment UI from address, you can keep doing that: set asset and chain on the payment and read the deposit address from the response instead of redirecting.


Step 3: Replace recurring payments with invoices

Cryptomus had a dedicated recurring endpoint. CoinCircuit does not bill on a schedule for you: keep the schedule in your system and issue an invoice each cycle.

POST https://api.coincircuit.io/api/v1/invoices
x-api-key: sk_live_your_key
Content-Type: application/json
{
  "description": "Pro plan for July",
  "currency": "USD",
  "customer": { "email": "client@example.com", "firstName": "Ada" },
  "reference": "sub_2026_07",
  "items": [
    { "name": "Pro plan", "quantity": 1, "unitPrice": 49 }
  ]
}

Invoices are line-item based, so each plan or add-on becomes an items entry. Share the hosted invoice link and fulfil on the paid webhook. Your own cron decides when the next invoice goes out.


Step 4: Migrate webhooks

Both platforms POST JSON to your callback URL. The difference is where the signature lives and how it is computed.

Cryptomus signature (before)

The signature arrived inside the payload as a sign field. You removed it, base64-encoded the remaining JSON, and MD5'd it with your API key.

const { sign, ...payload } = req.body;
const expected = crypto
  .createHash('md5')
  .update(Buffer.from(JSON.stringify(payload)).toString('base64') + API_KEY)
  .digest('hex');
const isValid = expected === sign;

CoinCircuit signature (after)

The signature arrives in the x-coincircuit-signature header, formatted v1=<hex>. It is an HMAC-SHA256 over <timestamp>.<raw body>, where the timestamp comes from the x-coincircuit-timestamp header.

const crypto = require('crypto');

function verifyWebhook(rawBody, timestamp, header, secret) {
  // Reject anything older than 5 minutes to block replays.
  const age = Math.floor(Date.now() / 1000) - Number(timestamp);
  if (!timestamp || age < 0 || age > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)   // raw bytes, not parsed JSON
    .digest('hex');

  const received = String(header).replace(/^v1=/, '');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(received, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyWebhook(
    req.body,
    req.headers['x-coincircuit-timestamp'],
    req.headers['x-coincircuit-signature'],
    process.env.WEBHOOK_SECRET,
  );
  if (!ok) return res.status(401).send('Invalid signature');

  const { event, data } = JSON.parse(req.body.toString('utf8'));
  // handle event
  res.sendStatus(200);
});

Status mapping

Cryptomus reported state through payment_status. CoinCircuit sends a named event instead.

Cryptomus status CoinCircuit event Trigger
process No pre-deposit event; the session simply stays open.
check / confirm_check transaction.received Deposit seen on-chain, awaiting confirmations.
transaction.confirmed Transaction confirmed on the blockchain.
paid payment.completed Settled in full. Fulfil the order here.
paid_over payment.completed Overpayment; compare amountReceived against the amount due.
wrong_amount / wrong_amount_waiting payment.partial Underpaid; the session stays open for the balance.
fail / cancel / system_fail payment.expired Session closed without full payment.
refund_paid refund.success Refund confirmed on-chain.
refund_fail refund.failed Refund could not be completed.

Fulfil on payment.completed (was paid). Your order_id lives in metadata and is returned on every event.


Step 5: Migrate payouts

Cryptomus (before)

Payouts used a separate payout API key and took the destination address inline on every call.

POST /v1/payout
{
  "amount": "500",
  "currency": "USDT",
  "network": "tron",
  "order_id": "payout_1",
  "address": "TRecipientAddress",
  "is_subtract": true
}

CoinCircuit (after)

Same API key as everything else. Save the destination once as a recipient, then reference it by ID, so an address is verified and reusable instead of re-sent each time.

POST https://api.coincircuit.io/api/v1/recipients
x-api-key: sk_live_your_key
Content-Type: application/json

{
  "type": "crypto_address",
  "label": "Vendor A",
  "details": { "chain": "tron", "address": "TRecipientAddress" }
}

Check the cost before sending:

GET https://api.coincircuit.io/api/v1/payouts/fees
x-api-key: sk_live_your_key

Then create the payout:

POST https://api.coincircuit.io/api/v1/payouts
x-api-key: sk_live_your_key
Content-Type: application/json

{
  "method": "crypto",
  "recipientId": "recipient-id-from-above",
  "amount": "500.00",
  "currency": "USDT",
  "reference": "payout_1"
}

Listen for payout.success or payout.failed to confirm the result.


Step 6: Check payment status

Cryptomus (before)

POST /v1/payment/info
{ "uuid": "8b03432e-385b-4670-8d06-064591096795" }

CoinCircuit (after)

A plain GET by the reference you stored when creating the payment.

GET https://api.coincircuit.io/api/v1/payments/reference/{reference}
x-api-key: sk_live_your_key

To list payments:

GET https://api.coincircuit.io/api/v1/payments?page=1&size=20

Treat status polling as a fallback. Webhooks are the reliable path; poll only when you have missed a delivery.


Step 7: Test in the sandbox

CoinCircuit has a full sandbox on testnet. Point your integration at the sandbox base URL with a test key before going live.

- https://api.coincircuit.io/api/v1
+ https://sandbox-api.coincircuit.io/api/v1

Use the Simulate payment button on the sandbox checkout page to trigger outcomes (full, partial, overpayment) without moving real funds. See the Sandbox Environment guide for faucet links and testnet details.