Loading...
Loading...
Docs / Migration guide / CryptoMus
Use this page to run a controlled provider cutover: confirm the fee-model change, map the payment lifecycle to Payvra payments, replace the webhook handler with HMAC-SHA256 verification, and only then move live traffic.
The safe rollout: leave existing CryptoMus payments draining, point new payment creation at Payvra in sandbox, prove the signed webhook path, and then swap only the credential prefix when you promote to live.
What to prove before cutover
Fee transparency
Auto-conversion coverage
Cutover reversibility
Cutover facts
Auth change
merchant + sign header pair with a single Payvra secret key (sk_test_ in sandbox, live secret-key prefix in production).Signing change
Settlement change
Promotion rule
CryptoMus and Payvra both process crypto payments, but the fee economics and developer surface are different. CryptoMus advertises a low headline rate and recovers margin through conversion spreads and withdrawal fees. Payvra is a flat 0.5% with no spread and no platform-side markups.
Treat this migration as both a fee-model and a developer-experience upgrade. You are replacing the request signing scheme, the webhook contract, and the settlement assumptions reconciliation depends on.
Flat 0.5% with no conversion spread
Modern signing and replay protection
Typed SDKs and OpenAPI
On-demand withdrawals
| CryptoMus | Maps to | Payvra | Notes |
|---|---|---|---|
POST /v1/payment | POST /v1/payments | Replace CryptoMus's form-style payment creation with Payvra's typed JSON payload. `amount` and `currency` map directly. | |
POST /v1/payment/info | GET /v1/payments/{id} | CryptoMus uses POST for payment lookup; Payvra uses idiomatic GET with path parameter. | |
POST /v1/payment/list | GET /v1/payments | Payvra returns cursor-paginated results with status, date-range, and chain filters. | |
POST /v1/payout | POST /v1/withdrawals | On-demand payout to any wallet address. Payvra removes the manual-review delay on the first withdrawal once KYB is on file. | |
POST /v1/exchange/rate | POST /v1/conversions | Auto-conversion is built into the payment flow on Payvra. Use the conversions endpoint only for explicit ad-hoc conversions. | |
POST /v1/balance | GET /v1/balances | Per-asset balance with last-settled timestamps. |
Create a payment
CryptoMus
// CryptoMusconst body = { amount: "49.99", currency: "USD", order_id: "order_123", url_callback: "https://yoursite.com/webhooks/cryptomus",};const sign = crypto .createHash("md5") .update(Buffer.from(JSON.stringify(body)).toString("base64") + API_KEY) .digest("hex");const response = await fetch("https://api.cryptomus.com/v1/payment", { method: "POST", headers: { merchant: MERCHANT_ID, sign, "Content-Type": "application/json", }, body: JSON.stringify(body),});const { result } = await response.json();// Redirect to: result.urlPayvra
// Payvraimport Payvra from "payvra";const payvraSecretKey = process.env.PAYVRA_SECRET_KEY;if (!payvraSecretKey) { throw new Error("Set PAYVRA_SECRET_KEY before running this example.");}const payvra = new Payvra(payvraSecretKey);const payment = await payvra.payments.create({ amount: "49.99", currency: "USDC", metadata: { orderId: "order_123" },});// Redirect to: payment.checkoutUrlVerify webhooks
CryptoMus webhook
// CryptoMus webhook handlerapp.post("/webhooks/cryptomus", (req, res) => { const { sign, ...payload } = req.body; const expected = crypto .createHash("md5") .update(Buffer.from(JSON.stringify(payload)).toString("base64") + API_KEY) .digest("hex"); if (sign !== expected) { return res.status(401).send("Invalid"); } if (payload.status === "paid") { // Mark order paid, payload.order_id } res.sendStatus(200);});Payvra webhook
// Payvra webhook handlerapp.post("/webhooks", (req, res) => { const signature = req.headers["webhook-signature"]; const timestamp = req.headers["webhook-timestamp"]; const webhookId = req.headers["webhook-id"]; const signedContent = `${webhookId}.${timestamp}.${req.rawBody}`; const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(signedContent) .digest("base64"); if (signature !== `v1,${expected}`) { return res.status(401).send("Invalid"); } const { type, data } = req.body; if (type === "payment.completed") { // Mark order paid, data.metadata.orderId } res.sendStatus(200);});payment. events so your handler can branch on the event name directly rather than parsing a status string.| CryptoMus event | Maps to | Payvra | Notes |
|---|---|---|---|
Webhook payment.process | Webhook payment.created | Payment created and awaiting deposit. | |
Webhook payment.confirming | Webhook payment.confirming | Deposit detected, waiting for confirmations. | |
Webhook payment.confirmed | Webhook payment.confirmed | Confirmed on-chain. | |
Webhook payment.paid | Webhook payment.completed | Final settled state, order should advance to paid here. | |
Webhook payment.cancel | Webhook payment.failed | Cancellation maps to a terminal failed state on Payvra. | |
Webhook payment.fail | Webhook payment.failed | Same terminal failed state. |
Create sandbox credentials and keep live traffic where it is
sk_test_... and leave existing CryptoMus payments running to completion. The first milestone is proving the new Payvra path, not forcing an all-at-once cutover.Install the Payvra SDK and replace payment creation
order_id field maps cleanly to Payvra metadata.orderId.npm install payvraReplace the webhook handler before trusting paid-state updates
Run one full sandbox order to completion
Promote to live by swapping only the credential prefix
Can I run both providers in parallel?
What happens to my volume-tier discount?
Do I need to change my webhook URL?
What about unsupported coins?
Start in sandbox, prove the signed webhook path, and only then shift live traffic. The migration stays reversible if you treat it as a controlled cutover instead of a same-day rewrite.