Node.js
Integrate Coinsnap into any Node.js backend — Express, Fastify, Next.js API routes, or plain HTTP servers.
Install
No SDK needed. Coinsnap uses standard REST. Use fetch (Node 18+) or any HTTP client.
# Optional: lightweight fetch for older Node versions
npm install node-fetch
Environment variables
.env
COINSNAP_API_KEY=cs_live_xxxxxxxxxxxxxxxxxxxx
COINSNAP_STORE_ID=7CVKXVxM7BtbkMEie8yoNeR8EetExpQhJUYEFY3ftfwR
COINSNAP_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxx
Create an invoice
- Express
- Next.js (App Router)
routes/payment.js
import express from 'express';
const router = express.Router();
router.post('/pay', async (req, res) => {
const { amount, currency, orderId, email } = req.body;
const response = await fetch(
`https://app.coinsnap.io/api/v1/stores/${process.env.COINSNAP_STORE_ID}/invoices`,
{
method: 'POST',
headers: {
'x-api-key': process.env.COINSNAP_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
currency,
orderId,
buyerEmail: email,
redirectUrl: `${process.env.APP_URL}/orders/${orderId}`,
}),
}
);
if (!response.ok) {
const err = await response.json();
return res.status(502).json({ error: 'Invoice creation failed', detail: err });
}
const invoice = await response.json();
// Persist invoiceId → orderId mapping
await db.orders.update({ id: orderId }, { invoiceId: invoice.id });
res.json({ checkoutUrl: invoice.checkoutLink });
});
export default router;
Webhook statuses and suggested actions:
app/api/pay/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { amount, currency, orderId, email } = await req.json();
const response = await fetch(
`https://app.coinsnap.io/api/v1/stores/${process.env.COINSNAP_STORE_ID}/invoices`,
{
method: 'POST',
headers: {
'x-api-key': process.env.COINSNAP_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
currency,
orderId,
buyerEmail: email,
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/orders/${orderId}`,
}),
}
);
const invoice = await response.json();
return NextResponse.json({ checkoutUrl: invoice.checkoutLink });
}
Handle webhooks
routes/webhook.js
import express from 'express';
import crypto from 'crypto';
const router = express.Router();
// Must use raw body — do NOT use express.json() here
router.post('/coinsnap', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['x-coinsnap-sig'] ?? '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.COINSNAP_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const sigBuf = Buffer.from(sig.padEnd(expected.length));
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(req.body);
const orderId = event.metadata?.orderId;
switch (event.type) {
case 'New':
// Payment attempt started — no action needed
break;
case 'Processing':
// Full amount received on-chain, waiting for block confirmation
await db.orders.update({ id: orderId }, { status: 'processing' });
break;
case 'Settled': {
const order = await db.orders.findById(orderId);
if (!order || order.status === 'paid') break; // idempotency guard
await db.orders.update({ id: orderId }, { status: 'paid', paidAt: new Date() });
if (event.additionalStatus === 'Overpaid') {
// Customer paid more than the invoice amount — decide on refund policy
await notifyMerchantOfOverpayment(orderId, event);
}
if (event.additionalStatus === 'PaidAfterExpiration') {
// Payment received after the invoice expired — review manually
await flagForManualReview(orderId);
break;
}
await triggerFulfillment(orderId);
break;
}
case 'Expired':
if (event.additionalStatus === 'Underpaid') {
// Invoice expired with partial payment — contact customer
await db.orders.update({ id: orderId }, { status: 'underpaid' });
await notifyCustomerOfUnderpayment(orderId);
} else {
await db.orders.update({ id: orderId }, { status: 'expired' });
}
break;
case 'Invalid':
// Invoice manually marked invalid or otherwise rejected
await db.orders.update({ id: orderId }, { status: 'cancelled' });
break;
}
res.status(200).send('OK');
});
export default router;