@blockmoblabs/cryptopay
v0.1.1
Published
CryptoPay crypto payment infrastructure: create payment intents, open the checkout, verify webhooks.
Readme
@blockmoblabs/cryptopay
Take crypto payments in your product. Create a payment against your own customer, order or subscription reference; CryptoPay handles the address, the chain and the confirmations, and posts you a signed webhook carrying that reference back.
Zero dependencies. Node 18+ for the server entry; any modern browser for @blockmoblabs/cryptopay/browser.
npm install @blockmoblabs/cryptopayServer
import { CryptoPay } from '@blockmoblabs/cryptopay';
const cryptopay = new CryptoPay({
apiKey: process.env.CRYPTOPAY_API_KEY!, // cp_test_… or cp_live_…
baseUrl: process.env.CRYPTOPAY_API_URL, // only when self-hosting
});
const intent = await cryptopay.createIntent({
amountUsd: 40,
customerEmail: '[email protected]',
customerRef: 'user_8812',
orderRef: 'ord_5571',
subscriptionRef: 'sub_221',
successUrl: 'https://yourapp.com/billing/done',
metadata: { plan: 'pro' },
idempotencyKey: 'ord_5571', // replaying returns the same intent
});The key decides the merchant and the mode — there is no mode flag to get wrong. cryptopay.mode
reports which one you are on.
| Method | What |
| --- | --- |
| createIntent(input) | Create a payment. Returns checkoutUrl and clientSecret. |
| getIntent(id) | Read one payment, including its deposit sessions. |
| listIntents(query) | Page through payments; search matches any of your references. |
| cancelIntent(id) | Cancel an unpaid payment. Refused once completed. |
| verifyWebhook(opts) | Verify and parse a webhook. Throws on anything suspect. |
Non-2xx responses throw CryptoPayError with status and body.
Browser
import { openCheckout, embedCheckout } from '@blockmoblabs/cryptopay/browser';
openCheckout({
checkoutUrl, // from the intent your server created
onStatus: (status) => console.log(status),
onSuccess: () => location.assign('/billing/done'),
onCancel: () => console.log('payer closed the modal'),
});
// or inline, into an element you already have
embedCheckout(document.getElementById('pay')!, { checkoutUrl, onSuccess: () => location.reload() });Nothing secret lives here: the checkout URL carries the payer's own credential, and the page it opens is served by CryptoPay, so your page never handles an address or an amount. Only messages from the checkout's origin can drive the modal.
Webhooks
Pass the raw body — a re-serialized object will not match the signature.
app.post('/webhooks/cryptopay', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = cryptopay.verifyWebhook({
body: req.body, // Buffer, not parsed JSON
headers: req.headers,
secret: process.env.CRYPTOPAY_WEBHOOK_SECRET!,
});
} catch {
return res.sendStatus(400); // never act on an unverified body
}
if (await alreadyHandled(event.id)) return res.sendStatus(200);
if (event.type === 'payment.completed') {
await credit(event.customer.customerRef!, event.fee!.netUsd, event.customer.orderRef);
}
await markHandled(event.id);
res.sendStatus(200); // 2xx stops the retries
});verifyWebhook checks the HMAC-SHA256 signature over timestamp.eventId.rawBody in constant time
and rejects a timestamp outside toleranceMs (default 5 minutes), so a validly signed replay of an
old event does not get through. Delivery is at-least-once: dedupe on event.id.
Events: payment.created, payment.detected, payment.confirming, payment.underpaid,
payment.completed, payment.expired, payment.failed.
Test mode
A test key runs the whole lifecycle against a simulator — real intents, statuses and webhooks, no
chain and no money. The deposit address is cptest_…, deliberately invalid on every network, and the
payment confirms on a timer: detected within ~15s and confirmed within ~30s, since the watcher that
advances it ticks every 15 seconds. In an automated test, call POST /sessions/:id/recheck (what I
have paid does in the checkout) to confirm immediately instead of sleeping.
Going live is one environment variable: the key changes, the code does not.
Fees
$2 flat per successful payment, charged once on completion. It comes off your side — the payer is
asked for exactly the invoice amount. On a $40 payment, fee is
{ grossUsd: 40, feeUsd: 2, netUsd: 38 }.
