Skip to main content

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

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:


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;