# Migrate from Cryptomus

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

Section: Migration
Source: https://coincircuit.io/docs/guides/migrate-from-cryptomus/
Interactive version: https://coincircuit.io/api-reference?tab=guides&guide=migrate-from-cryptomus

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.

```js
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.

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

```diff
- merchant: 8b03432e-385b-4670-8d06-064591096795
- sign: 8b03432e385b46708d06064591096795
+ x-api-key: sk_live_your_coincircuit_key
```

Change your base URL:

```diff
- 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)

```http
POST /v1/payment
```

```json
{
  "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)

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

```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.

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

```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.

```js
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.

```js
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.

```http
POST /v1/payout
```

```json
{
  "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.

```http
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:

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

Then create the payout:

```http
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)

```http
POST /v1/payment/info
```

```json
{ "uuid": "8b03432e-385b-4670-8d06-064591096795" }
```

#### CoinCircuit (after)

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

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

To list payments:

```http
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.

```diff
- 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](https://coincircuit.io/guides/sandbox) guide for faucet links and testnet details.
