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
addressandurl. If you rendered your own payment UI fromaddress, you can keep doing that: setassetandchainon 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.