Custom checkout (Node + Express)
The reference implementation underneath every other page on this site: two routes, no platform quirks, meant to sit next to a checkout you've already built.
If you’re not on one of the named platforms — a hand-rolled storefront, an internal tool, or a checkout already built around Stripe or another processor — everything you actually need is two routes and the SDK itself. Every platform-specific page on this site is a variation of this same shape, adapted to wherever that platform puts the checkout button and however it expects an order to be confirmed.
Architecture
Customer picks Boomcoin
A second button next to your existing payment options submits to /checkout/boompay.
Intent created
The route loads the order, calls createIntent(), and 302-redirects straight to the returned link.
Customer pays
The customer approves payment on BoomPay's hosted page, from their Boom wallet.
Signed return
BoomPay redirects to /checkout/boompay/return with paymentIntentId and X-Boom-Signature.
Order fulfilled
boomPay.webhooks() verifies the signature, then the route re-fetches the payment and fulfills the order.
Files
<form action="/checkout/stripe" method="POST">
<button type="submit">Pay with Card</button>
</form>
<form action="/checkout/boompay" method="POST">
<input type="hidden" name="orderId" value="{{orderId}}">
<button type="submit">Pay with Boomcoin</button>
</form>
const express = require('express');
module.exports = function (boomPay, { getOrder, fulfillOrder }) {
const router = express.Router();
router.post('/checkout/boompay', express.urlencoded({ extended: false }), async (req, res) => {
const order = await getOrder(req.body.orderId);
try {
const intent = await boomPay.payments.createIntent({
amount: order.total,
successUrl: `${process.env.APP_URL}/checkout/boompay/return?status=success&orderId=${order.id}`,
failureUrl: `${process.env.APP_URL}/checkout/boompay/return?status=failure&orderId=${order.id}`,
label: `Order #${order.id}`,
metadata: { orderId: order.id },
});
res.redirect(intent.link);
} catch (err) {
// createIntent() validates amount/successUrl/failureUrl/label
// synchronously and throws before any network call -- a catch here
// is mostly for that, plus RestException from the API itself.
res.status(400).send('Could not start BoomPay checkout: ' + err.message);
}
});
router.get('/checkout/boompay/return', boomPay.webhooks(), async (req, res) => {
const { orderId, status } = req.query;
const payment = await boomPay.payments.getPayment(req.query.paymentIntentId);
if (status === 'success' && payment.paidAt) {
await fulfillOrder(orderId, { provider: 'boompay', reference: payment.id });
return res.redirect(`/orders/${orderId}/confirmation`);
}
res.redirect(`/orders/${orderId}/failed`);
});
return router;
};
const express = require('express');
const BoomPay = require('boom-pay-sdk');
const stripeRoutes = require('./routes/stripe-checkout'); // your existing integration
const boompayRoutes = require('./routes/boompay-checkout');
const { getOrder, fulfillOrder } = require('./orders');
const app = express();
const boomPay = new BoomPay({
apiKey: process.env.BOOMPAY_API_KEY,
sandbox: process.env.NODE_ENV !== 'production',
});
app.use(stripeRoutes);
app.use(boompayRoutes(boomPay, { getOrder, fulfillOrder }));
app.listen(process.env.PORT || 3000);
That’s the whole integration. getOrder and fulfillOrder are stand-ins for whatever your own order system already does — the BoomPay-specific part is entirely contained in routes/boompay-checkout.js, and it doesn’t know or care that Stripe (or anything else) is running in the same app.
Error handling
The SDK validates amount, successUrl, failureUrl, and label synchronously and throws before making any network call if one is missing or malformed — that’s what the try/catch above is mostly there for. Once a request does reach the API, failures surface as a RestException with a .status and .message; see the API Reference for the full shape and the SDK’s built-in retry behavior on 429s.
Testing in sandbox
With sandbox: true, submit the Boomcoin form for a real order in your system and confirm you land on a sapi.boom.market page. Approve the test payment and check fulfillOrder actually ran — then deliberately cancel a second test payment and confirm the failure path doesn’t fulfill anything.
Go-live checklist
- Switch
sandboxtofalseand use a live API key once a full test order has gone through. - Make
fulfillOrderidempotent — a customer refreshing the return URL, or BoomPay retrying a slow response, shouldn’t fulfill the same order twice. - Convert to a BMC amount before calling
createIntentif the rest of your checkout prices in another currency — there’s no FX endpoint to lean on.