Embedded API Quickstart
Accept card payments inside your existing checkout page.
- Bolt handles secure card fields and payment processing.
- Your app owns the checkout layout, cart totals, and order records.
Your integration steps
This flow: browser JavaScript, Node.js 22 / Express, and a guest payment through the v1 direct-payment API with Bolt Payments. Shopper login and saved cards are follow-on work.
Before you begin
Have these ready before the estimate starts.
| Requirement | What you need |
|---|---|
| Existing app | Your own checkout page, customer sessions, and saved orders. The examples use Node.js 22 and Express. |
| Sandbox account | Ask Bolt to confirm that your division supports v1 direct payments with Bolt Payments. This path requires enablement; do not substitute v3 endpoints. Account setup is separate from this estimate. |
Platform support
This guide works for any custom web checkout. The examples use Node.js and Express, but the server steps are plain HTTPS calls to Bolt that you can write in any language, and the browser steps load Bolt's hosted embed script from any front end. Native mobile apps and Salesforce Commerce Cloud use their own guides instead.
1. Set up
- Open sandbox Payments and confirm Bolt Payments is configured for USD test payments.
- Open Administration > API. Copy the API key, Publishable key, and Merchant Division Public ID from the same division.
- Open Trusted Domains and save your app's HTTPS origin for that division.
- Add these values to your existing server secrets configuration:
BOLT_API_KEY=YOUR_SANDBOX_API_KEY
BOLT_PUBLISHABLE_KEY=YOUR_SANDBOX_PUBLISHABLE_KEY
BOLT_DIVISION_ID=YOUR_MERCHANT_DIVISION_PUBLIC_IDThese names configure this example, not the SDK. Only the publishable key belongs in browser configuration.
Validate setup
Confirm the keys and domain belong to the enabled sandbox division. Restart the backend and check that all three values are available without logging them. Stop if Bolt has not confirmed this payment path for your account.
2. Connect orders
Use your existing order service to save an unpaid checkout snapshot. For this first test, use one $10.00 USD digital item, no tax, and no discounts. Prices below are cents. Replace the order ID with your app's unique, saved order ID:
const order = {
id: 'YOUR_SAVED_ORDER_ID',
cart: {
order_reference: 'YOUR_SAVED_ORDER_ID', display_id: 'YOUR_SAVED_ORDER_ID',
currency: 'USD', total_amount: 1000, tax_amount: 0,
items: [{ reference: 'test-download', name: 'Test download', type: 'digital',
quantity: 1, unit_price: 1000, total_amount: 1000 }],
},
billing: {
first_name: 'Test', last_name: 'Shopper', email: 'shopper@example.com',
phone: '+14155550199', street_address1: '535 Mission St',
locality: 'San Francisco', region: 'CA', country_code: 'US', postal_code: '94105',
},
payment: null,
};The orders adapter below is your application code. Implement these operations before continuing:
| Operation | Required behavior |
|---|---|
findForShopper(id, req) | Return the saved order only for its owning session, otherwise null. Read prices and billing details from server-validated storage. |
claim(id) | Atomically reserve the order's only payment attempt. Return false if already claimed or paid. Keep this reservation across restarts and uncertain network failures. |
saveReference(id, reference) | Persist the returned reference as order.payment.reference (for example, payment: { reference }) so findForShopper returns it. Preserve other payment fields and reject a different existing reference atomically. |
recordPayment(id, payment) | Atomically persist { reference, status }. Never overwrite a completed payment with a different reference or an older state. |
Keep the snapshot unchanged during payment. A new cart needs a new snapshot. These snippets do not implement your database, sessions, fulfillment, or safe resubmission after an uncertain payment.
Add these handlers to your backend:
export function createEmbeddedHandlers(orders) {
for (const key of ['BOLT_API_KEY', 'BOLT_PUBLISHABLE_KEY', 'BOLT_DIVISION_ID']) {
if (!process.env[key]) throw new Error(`Missing server configuration: ${key}`);
}
async function bolt(path, body, id) {
const response = await fetch(`https://api-sandbox.boltapp.com${path}`, {
method: body ? 'POST' : 'GET',
headers: {
'X-API-Key': process.env.BOLT_API_KEY,
'X-Publishable-Key': process.env.BOLT_PUBLISHABLE_KEY,
'Content-Type': 'application/json',
...(id ? { 'Idempotency-Key': `embedded-${id}` } : {}),
},
...(body ? { body: JSON.stringify(body) } : {}),
});
if (!response.ok) throw new Error(`Bolt HTTP ${response.status}`);
return response.json();
}
const handle = fn => async (req, res, next) => {
res.set('Cache-Control', 'no-store');
try { await fn(req, res); } catch (error) { next(error); }
};
return {
pay: handle(async (req, res) => {
const order = await orders.findForShopper(req.params.id, req);
if (!order) return res.status(404).json({ error: 'Order not found' });
const card = req.body;
if (!['token', 'bin', 'last4', 'expiration'].every(k =>
typeof card?.[k] === 'string' && card[k].length > 0)) {
return res.status(400).json({ error: 'Card token required' });
}
if (order.cart.order_reference !== order.id || !await orders.claim(order.id)) {
return res.status(409).json({ error: 'Check the existing payment before trying again' });
}
const address = order.billing;
const result = await bolt('/v1/merchant/transactions/authorize', {
division_id: process.env.BOLT_DIVISION_ID, source: 'direct_payments',
auto_capture: true, create_bolt_account: false,
user_identifier: { email: address.email, phone: address.phone },
user_identity: { first_name: address.first_name, last_name: address.last_name },
cart: { ...order.cart, billing_address: address },
credit_card: { token: card.token, bin: card.bin, last4: card.last4,
expiration: card.expiration, postal_code: address.postal_code,
token_type: 'bolt', billing_address: address, save: false },
}, order.id);
if (!result.transaction?.reference) throw new Error('Missing payment reference');
await orders.saveReference(order.id, result.transaction.reference);
res.status(202).json({ id: order.id });
}),
status: handle(async (req, res) => {
const order = await orders.findForShopper(req.params.id, req);
if (!order) return res.status(404).json({ error: 'Order not found' });
const reference = order.payment?.reference;
if (!reference) return res.json({ id: order.id, status: 'unconfirmed' });
const transaction = await bolt(`/v1/merchant/transactions/${encodeURIComponent(reference)}`);
if (transaction.reference !== reference || transaction.type !== 'cc_payment' ||
transaction.order?.cart?.order_reference !== order.id ||
transaction.amount?.amount !== order.cart.total_amount ||
transaction.amount?.currency !== order.cart.currency) {
throw new Error('Payment does not match the saved order');
}
await orders.recordPayment(order.id, { reference, status: transaction.status });
res.json({ id: order.id, reference, status: transaction.status });
}),
};
}Register the handlers after your existing JSON parser and before your error handler. Connect your session and CSRF middleware; these identifiers are app-owned, not Bolt APIs:
import { createEmbeddedHandlers } from './embedded-payments.mjs';
const embedded = createEmbeddedHandlers(orders);
app.post('/api/embedded/orders/:id/pay', requireShopper, requireCsrf, embedded.pay);
app.get('/api/embedded/orders/:id', requireShopper, embedded.status);Return generic browser errors from your error handler; keep diagnostic details in server logs without card tokens or credentials. A timeout after submission is uncertain, not permission to submit another charge.
Validate orders
- Save the test snapshot under your shopper's session. Expose its ID to that shopper's checkout page.
- Request
GET /api/embedded/orders/{id}through the app. Expectunconfirmed. - Repeat as another shopper. Expect
404, with no order details. - Verify
claimpermits only one concurrent attempt and survives a restart before connecting payment submission.
3. Add fields
Load the sandbox embed script once in your existing page shell. Replace the publishable key:
<script id="bolt-embed" src="https://connect-sandbox.boltapp.com/embed.js"
data-publishable-key="YOUR_SANDBOX_PUBLISHABLE_KEY" defer></script>Add these elements inside your existing payment section. Keep your app's price and billing-address display visible:
<div id="embedded-fields"></div>
<button id="embedded-pay" type="button" disabled>Pay $10.00</button>
<button id="embedded-refresh" type="button">Check payment status</button>
<p id="embedded-status" role="status"></p>Call this function on page mount, with the saved orderId, public key, and your authenticated JSON request helper. The helper must send CSRF protection on POST, reject non-2xx responses, and parse JSON. Call the returned cleanup on unmount; keep one active payment component per page.
export function mountEmbeddedPayment({ orderId, publishableKey, request }) {
const pay = document.querySelector('#embedded-pay');
const refresh = document.querySelector('#embedded-refresh');
const status = document.querySelector('#embedded-status');
const script = document.querySelector('#bolt-embed');
const path = `/api/embedded/orders/${encodeURIComponent(orderId)}`;
let active = true, fields, submitted = false;
const show = text => { if (active && status.textContent !== text) status.textContent = text; };
const check = async () => {
try {
const result = await request(path);
if (result.status === 'completed') {
submitted = true; pay.disabled = true;
show(`Order ${orderId} paid. Payment ${result.reference}.`);
} else show(`Order ${orderId}: ${result.status}. Check again before paying again.`);
} catch { show('Cannot confirm payment. Check this order with support before paying again.'); }
};
async function start() {
if (!active) return;
try {
Bolt.initialize(publishableKey);
fields = Bolt.create('credit_card_input');
await fields.mount('#embedded-fields');
if (!active) { await fields.unmount(); return; }
pay.disabled = submitted;
show('Enter your test card.');
} catch { show('Payment fields could not load. Reload this page to try again.'); }
}
pay.onclick = async () => {
if (submitted || pay.disabled) return;
pay.disabled = true;
try {
const card = await fields.tokenize();
if (!active) return;
if (!card.token) { show('Check your card details.'); pay.disabled = false; return; }
submitted = true;
show('Submitting payment…');
await request(`${path}/pay`, { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(card) });
await check();
} catch {
show(submitted ? 'Payment is unconfirmed. Check its status before trying again.' : 'Check your card details.');
if (active) pay.disabled = submitted;
}
};
refresh.onclick = check;
const loadFailed = () => show('Payment fields could not load. Reload this page to try again.');
if (window.Bolt) start();
else { script.addEventListener('load', start, { once: true }); script.addEventListener('error', loadFailed, { once: true }); }
return () => {
active = false; pay.onclick = null; refresh.onclick = null;
script.removeEventListener('load', start); script.removeEventListener('error', loadFailed);
if (fields) Promise.resolve(fields.unmount()).catch(() => {});
};
}Validate fields
Open checkout and confirm the card fields load inside Bolt-hosted frames. The Pay button enables after mounting. Confirm the page includes only the publishable key, and card numbers do not pass through your app's inputs or backend.
4. Take payment
- Confirm the order is $10.00 USD and the displayed billing ZIP is
94105. - Enter
4111 1111 1111 1111, expiration03/30, and CVV737in the hosted fields. - Select Pay $10.00 once. The browser sends a token to your backend, not the card number.
- Use Check payment status while capture is pending. Automatic capture is asynchronous.
Validate submission
The pay route returns 202 and your saved order has one transaction reference. authorized, pending, and unconfirmed are not completed payments. If submission fails after it starts, check that order in the dashboard before attempting another payment.
5. Verify order
- Select Check payment status until the backend reports
completed. - Reopen the order in your app's storage. Confirm its payment reference and completed status survived a page refresh.
- Open sandbox Transactions. Match the order ID, reference, $10.00 USD, and completed payment.
Validate completion
You are done when your checkout collects card details through Bolt fields and your server verifies and stores the matching payment. The guest is not enrolled in Bolt and no card is saved by this sample.
Test before launch
Run the Embedded API test plan. Before production, add asynchronous payment reconciliation using verified webhooks, safe recovery for uncertain payments, and your order confirmation/fulfillment behavior. Add shopper login and saved-card flows as separate, explicitly enabled work; this guest-payment quickstart does not implement them.