Managed Checkout Quickstart
Accept one-time payments for physical products in your existing web app.
- Bolt handles the checkout interface, shipping details, and payment collection.
- Your app owns the cart, pricing, and order records.
Your integration steps
Before you begin
Have these ready before starting the estimated integration time.
| Requirement | What you need |
|---|---|
| Existing app | An online store with a cart, customer sign-in, and saved orders. The examples use Node.js 22 and Express. |
| Sandbox account | Create a merchant account, accept your sandbox invitation, and ask Bolt for a custom-cart division with split shipping/tax callbacks and pre-authorization order creation. |
Platform support
This guide works for any custom web app. The examples use Node.js and Express, but every server step is a plain HTTPS call to Bolt with a standard HMAC check that you can write in any language, and the browser steps load Bolt's hosted scripts from any front end. The Web checkout reference documents that browser client. Hosted commerce platforms and native mobile apps use their own guides instead.
1. Set up
- Open sandbox Payments, select the Bolt Payments processor, and configure Immediate Capture for your USD sandbox division.
- Open Administration > API. Copy the API key and Publishable key for that division, then copy the Signing secret.
- Open Administration > Trusted Domains. Select the same division as your credentials, select + Add Domain, enter your app's public HTTPS address, and select Update.
- Add these values to your app's server environment using its existing secrets configuration:
BOLT_API_KEY=YOUR_SANDBOX_API_KEY
BOLT_PUBLISHABLE_KEY=YOUR_SANDBOX_PUBLISHABLE_KEY
BOLT_SIGNING_SECRET=YOUR_SANDBOX_SIGNING_SECRETThese are names used by this guide's code. Expose only the publishable key through your app's public configuration. Keep the API key and signing secret on the server and out of source control.
Validate setup
- Reopen sandbox Payments and confirm Immediate Capture is saved.
- Confirm all three credentials belong to the same sandbox division and your HTTPS development origin is saved in Trusted Domains.
- Restart your backend with the updated environment. Check that all three values are present without logging their contents. Do not continue if a value is missing.
2. Connect orders
Connect Bolt to your app's existing order lifecycle. Your backend must price the current cart, save an unpaid order, and use that order's identifier as Bolt's order_reference.
For this walkthrough, use one $10.00 USD physical item with free shipping, no tax, and no discounts.
Map your cart and order storage
Provide an orders adapter with these four operations. These are functions you implement using your app's services, not Bolt SDK methods:
| Operation | Required behavior |
|---|---|
prepare(req) | Load the authenticated shopper's cart, calculate its price on the server, and persist an unpaid checkout snapshot. Return that snapshot as { id, cart, payment }, including its current payment if one exists. Reuse the same unpaid snapshot for repeat requests with an unchanged cart. |
find(id) | Return the saved snapshot, or null. Use this for signed callbacks and transaction notifications. |
findForShopper(id, req) | Return a snapshot only if it belongs to the requesting shopper, otherwise null. |
recordPayment(id, payment) | Persist { reference, status } against that order atomically. Repeated deliveries must update the same payment, never create another order. Reject a different payment reference once a successful payment is recorded. This guide does not implement fulfillment. |
Map your cart fields to this shape. Use your own order identifier and product reference; prices are integers in cents. Store this snapshot before returning it from prepare:
const checkoutSnapshot = {
id: 'YOUR_ORDER_ID',
cart: {
order_reference: 'YOUR_ORDER_ID',
display_id: 'YOUR_ORDER_ID',
currency: 'USD',
total_amount: 1000,
tax_amount: 0,
items: [{
reference: 'YOUR_PRODUCT_ID', name: 'Test product', type: 'physical',
unit_price: 1000, total_amount: 1000, quantity: 1,
}],
},
payment: null,
};The adapter is required application work: implement and validate it before proceeding. The snippets do not provide a database, a cross-tab payment lock, receipt delivery, or fulfillment.
Keep the checkout snapshot immutable while payment is in progress. A changed cart needs a new snapshot and token. Do not accept prices or an arbitrary shopper's order identifier from the browser.
Add the Bolt handlers
Add this module to your backend. It creates an order token, answers checkout callbacks, and records payment only after retrieving the transaction from Bolt. The browser success callback never marks an order paid.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function createBoltHandlers(orders) {
for (const name of ['BOLT_API_KEY', 'BOLT_PUBLISHABLE_KEY', 'BOLT_SIGNING_SECRET']) {
if (!process.env[name]) throw new Error(`Set ${name} in your server environment`);
}
async function bolt(path, body) {
const response = await fetch(`https://api-sandbox.boltapp.com${path}`, {
method: body ? 'POST' : 'GET',
headers: { 'X-API-Key': process.env.BOLT_API_KEY, 'Content-Type': 'application/json' },
...(body ? { body: JSON.stringify(body) } : {}),
});
if (!response.ok) throw new Error(`Bolt returned HTTP ${response.status}`);
return response.json();
}
function signedBody(req) {
if (!Buffer.isBuffer(req.body)) throw new Error('Bolt routes need the raw request body');
const expected = createHmac('sha256', process.env.BOLT_SIGNING_SECRET)
.update(req.body).digest();
// During rotation, the incoming secret signs the Pending header.
const valid = ['X-Bolt-Hmac-Sha256', 'X-Bolt-Hmac-Sha256-Pending'].some(name => {
const received = Buffer.from(req.get(name) || '', 'base64');
return expected.length === received.length && timingSafeEqual(expected, received);
});
if (!valid) return null;
return JSON.parse(req.body.toString('utf8'));
}
const handle = fn => async (req, res, next) => {
res.set('Cache-Control', 'no-store');
try { await fn(req, res); } catch (error) { next(error); }
};
return {
createOrder: handle(async (req, res) => {
const order = await orders.prepare(req);
if (!order || order.cart.order_reference !== order.id || order.payment?.status === 'completed') {
return res.status(409).json({ error: 'No unpaid checkout snapshot' });
}
const result = await bolt('/v1/merchant/orders', { cart: order.cart, channel: 'browser' });
if (!result.token) throw new Error('No order token returned');
res.json({ id: order.id, token: result.token });
}),
orderStatus: handle(async (req, res) => {
const order = await orders.findForShopper(req.params.id, req);
if (!order) return res.status(404).json({ error: 'Order not found' });
res.json({ id: order.id, payment: order.payment });
}),
callbacks: handle(async (req, res) => {
const message = signedBody(req);
if (!message) return res.status(401).json({ error: 'Invalid signature' });
const { event, data } = message;
let reply;
if (event === 'order.shipping') {
reply = { shipping_options: [{ service: 'Free sandbox shipping', cost: 0,
reference: 'free', signature: 'free-v1' }] };
} else if (event === 'order.tax') {
reply = { tax_result: { subtotal_amount: 0 }, taxes_included: false };
} else if (event === 'order.create') {
const cart = data?.order?.cart;
const order = cart?.order_reference ? await orders.find(cart.order_reference) : null;
if (!order || cart.total_amount?.amount !== order.cart.total_amount ||
cart.currency?.currency !== order.cart.currency) {
return res.status(422).json({ event, status: 'failure', errors: [
{ code: 2001, data: [{ reason: 'Unknown or changed order' }] },
] });
}
reply = { display_id: order.cart.display_id };
} else {
return res.status(422).json({ event, status: 'failure', error: {
code: 2319, message: 'This walkthrough does not enable additional checkout features.',
} });
}
res.json({ event, status: 'success', data: reply });
}),
webhooks: handle(async (req, res) => {
const message = signedBody(req);
if (!message) return res.status(401).json({ error: 'Invalid signature' });
if (message.object === 'transaction' && message.data?.reference) {
const transaction = await bolt(`/v1/merchant/transactions/${encodeURIComponent(message.data.reference)}`);
// This walkthrough accepts card purchases only. Credits/refunds are separate transactions.
if (transaction.type !== 'cc_payment') return res.json({ status: 'success' });
if (transaction.reference !== message.data.reference) throw new Error('Transaction reference mismatch');
const id = transaction.order?.cart?.order_reference;
const order = id ? await orders.find(id) : null;
if (!order) throw new Error('No matching local order for this transaction');
if (transaction.amount?.amount !== order.cart.total_amount ||
transaction.amount?.currency !== order.cart.currency) {
throw new Error('Transaction does not match the saved order');
}
await orders.recordPayment(id, { reference: transaction.reference, status: transaction.status });
}
res.json({ status: 'success' });
}),
};
}Register these routes in your existing Express app. orders, requireShopper, and requireCsrf below refer to your app's adapter, authentication middleware, and CSRF middleware. Connect those existing functions before using this snippet.
import express from 'express';
import { createBoltHandlers } from './bolt-checkout.mjs';
const bolt = createBoltHandlers(orders);
// Register BEFORE your existing JSON parser and shopper-login middleware.
// Bolt sends signed server requests, without a shopper session or CSRF token.
app.post('/bolt/callbacks', express.raw({ type: 'application/json', limit: '64kb' }), bolt.callbacks);
app.post('/bolt/webhooks', express.raw({ type: 'application/json', limit: '64kb' }), bolt.webhooks);
// Add to your app's authenticated routes after its normal request parsers.
app.post('/api/bolt/order', requireShopper, requireCsrf, bolt.createOrder);
app.get('/api/bolt/orders/:id', requireShopper, bolt.orderStatus);Signature verification needs the original bytes, so Express's raw body parser must run before a JSON parser consumes the body. Keep your existing server error handler after these routes. Log failures on the server, return a generic error body to clients, and use non-2xx responses for failed notification processing so Bolt can retry.
Validate orders
- From your app's authenticated API client, send
POST /api/bolt/orderusing its normal CSRF protection. Expect HTTP200with a nonemptyidandtoken. - Find that
idin your order storage. Confirm the saved cart has a total of1000, currencyUSD, and no completed payment. - Repeat the request with the unchanged cart. Confirm it uses the same local unpaid order rather than creating another one.
- Request
GET /api/bolt/orders/{id}as the same shopper. Expect that order's ID andpayment: null. A different shopper must not receive its details.
Keep this order ID for the checkout test. Stop here if the token request or ownership check fails.
3. Add checkout
Add a checkout container and status message to your existing cart page:
<div id="bolt-checkout"></div>
<p id="bolt-status" role="status"></p>Run this function once when the cart page mounts and call its returned cleanup function on unmount. Use this module as the only loader for the two Bolt scripts; keep one active checkout mount and one sandbox division per page. Pass the publishable key from your app's public configuration and its existing authenticated JSON request helper. The helper must send the app's CSRF token on POST, reject non-2xx responses, and return parsed JSON. It is application code, not a Bolt SDK method.
const boltScripts = new Map();
export function mountBoltCheckout({ publishableKey, request }) {
const status = document.querySelector('#bolt-status');
const container = document.querySelector('#bolt-checkout');
const cdn = 'https://connect-sandbox.boltapp.com';
let active = true;
let timer;
function show(message) {
if (active && status.textContent !== message) status.textContent = message;
}
function loadScript(id, src) {
if (!boltScripts.has(id)) {
const promise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.id = id;
script.src = src;
script.dataset.publishableKey = publishableKey;
script.onload = resolve;
script.onerror = () => {
script.remove();
boltScripts.delete(id);
reject(new Error('Bolt script could not load'));
};
document.head.append(script);
});
boltScripts.set(id, promise);
}
return boltScripts.get(id);
}
async function start() {
show('Preparing checkout…');
try {
const { id, token } = await request('/api/bolt/order', { method: 'POST' });
if (!active) return;
container.dataset.tid = 'instant-bolt-checkout-button';
const button = document.createElement('object');
button.data = `${cdn}/v1/checkout_button?publishable_key=${encodeURIComponent(publishableKey)}`;
button.setAttribute('aria-label', 'Open Bolt Checkout');
container.replaceChildren(button);
await loadScript('bolt-track', `${cdn}/track.js`);
if (!active) return;
await loadScript('bolt-connect', `${cdn}/connect.js`);
if (!active) return;
BoltCheckout.configure({ orderToken: token }, {}, {
success: (_transaction, callback) => {
show('Payment submitted. Waiting for server confirmation. Do not pay again.');
callback();
},
});
show(`Order ${id}: ready for checkout.`);
async function poll() {
try {
const order = await request(`/api/bolt/orders/${encodeURIComponent(id)}`);
if (!active) return;
if (order.payment) {
show(`Order ${id}: ${order.payment.status}. Transaction: ${order.payment.reference}`);
}
if (['completed', 'failed', 'cancelled', 'rejected_irreversible'].includes(order.payment?.status)) return;
timer = setTimeout(poll, 2000);
} catch {
show(`Cannot confirm order ${id} yet. Check this order in your account or contact support before paying again.`);
}
}
poll();
} catch {
if (active) container.replaceChildren();
show('Checkout could not load. Return to your cart and try again.');
}
}
start();
// Returned immediately, so navigation can cancel pending setup as well as polling.
return () => { active = false; clearTimeout(timer); container.replaceChildren(); };
}track.js collects fraud signals; connect.js launches checkout. The server omits order_received_url so the shopper stays in your app and sees the server-verified status. Keep the cart fixed for this first test.
Validate checkout
- Open your cart page through its HTTPS development URL. Confirm the Bolt button appears and the status shows your order ID.
- In browser developer tools, confirm
track.jsandconnect.jsload successfully fromconnect-sandbox.boltapp.com. - Check the page source and network responses: only the publishable key may appear, never the private API key or signing secret.
Do not open checkout or submit payment yet. Register its receiving URLs next.
4. Connect Bolt
Use the same HTTPS development origin that serves your cart and backend. For example, https://your-app.example below represents your own development URL.
- Open Administration > API > Merchant Callbacks. Select Universal API, enter your development origin followed by
/bolt/callbacks, and select Add merchant callback. - Open Administration > Webhooks and register the same origin followed by
/bolt/webhooks.
If Universal API or Webhooks is unavailable, ask Bolt to confirm your custom-cart division and settings permissions. Merchant callbacks and transaction webhooks are separate registrations.
Validate connections
- Reopen the linked settings and confirm the saved domain and both endpoint URLs. Update existing registrations if the development URL changes.
- Send an unsigned request to each public endpoint. Replace the example origin with yours:
curl -i https://your-app.example/bolt/callbacks \
-H 'Content-Type: application/json' -d '{"event":"order.shipping","data":{}}'
curl -i https://your-app.example/bolt/webhooks \
-H 'Content-Type: application/json' -d '{"object":"transaction","data":{}}'Both must return HTTP 401 from your handler, without a login redirect or proxy error.
- Open Bolt checkout from your cart, enter a test email and US shipping address, then choose Free sandbox shipping. Confirm your server receives signed
order.shippingandorder.taxcalls, answers with HTTP200, and checkout displays $10.00 USD, free shipping, and zero tax. Leave checkout open for the final step.
5. Verify payment
Submit the open checkout using the Bolt Payments approved test card and card details.
Validate payment
- Confirm your server receives
order.create, finds the saved order, and answers successfully. Then confirm/bolt/webhooksreceives a signed transaction notification and completes the server-side transaction lookup. - Wait for your app to show
completedwith a transaction reference.pendingandauthorizedare not the completion checkpoint. - Look up the original order ID in your app's storage. Confirm its payment reference matches the page and its recorded status is
completed. - Open sandbox Transactions. Find that reference and confirm the amount is $10.00 USD and the order number matches your app.
- Restart your backend, then reload
GET /api/bolt/orders/{id}using your authenticated API client. Confirm the result persists. Reprocess the same notification in your development test harness and confirm it updates the existing record without creating another order. The quickstart does not create fulfillment actions or receipts.
You are done when: your existing app opens Bolt checkout, completes a sandbox payment, and persists the verified transaction against the correct order.
If a checkpoint fails
- No order token: check the sandbox API key, saved cart totals, and USD payment enablement. Inspect the backend's Bolt HTTP error.
- Checkout does not open: confirm the current host is in Trusted Domains and both sandbox scripts loaded.
- Callbacks fail signature verification: ensure
express.rawruns before your JSON parser and the Signing secret belongs to the configured division. - Payment exists but your app still waits: check the saved Webhooks URL, server errors, and original order ID. Investigate the existing payment before attempting another purchase.
For callback payloads, token errors, and other setup failures, use custom-cart troubleshooting and callback conventions.
After your first payment
Run the Managed Checkout test plan to check payment failures, recovery, and the experience your app controls. Its first five checks map to the checkpoints in this guide. The scope table identifies which later checks verify these snippets, your required adapters, or additional launch behavior.
Replace the fixed free-shipping and zero-tax responses with your app's shipping and tax calculations. Add safe retry controls, cross-tab payment locking, cart changes, confirmation-page recovery, refunds, and later risk-state changes. The card-only handler acknowledges credit/refund transactions without recording them; implement a separate refund lifecycle before offering refunds. Make payment updates transactional and fulfillment safe to repeat. Review your adapter's behavior for concurrent or out-of-order notifications before launch.
Complete Bolt Payments onboarding with your Bolt representative. Submit company, owner, bank, and verification details through Payments and wait for activation before accepting live payments.
Continue with production readiness. Use the transaction lookup reference for additional operations and webhook reference for later state changes.