# 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.

Source: https://help.boltapp.com/products/embeddable-checkout

<aside className="bolt-quickstart-summary not-prose" aria-labelledby="quickstart-outcome">
  <div className="bolt-quickstart-summary__header">
    <h2 id="quickstart-outcome">
      What you'll build
    </h2>

    <span className="bolt-quickstart-summary__time">
      <Icon name="clock" size="14" aria-hidden="true" />

       Est. 45–60 min
    </span>
  </div>

  <ul className="bolt-quickstart-summary__results">
    <li>
      <Icon name="circle-check" aria-hidden="true" />

      <span>Hosted card fields</span>
    </li>

    <li>
      <Icon name="circle-check" aria-hidden="true" />

      <span>Server-side payment</span>
    </li>

    <li>
      <Icon name="circle-check" aria-hidden="true" />

      <span>Verified paid order</span>
    </li>
  </ul>
</aside>

## Your integration steps [#your-integration-steps]

<nav className="bolt-quickstart-steps not-prose" aria-label="Quickstart steps">
  <ol>
    <li>
      <a href="#1-set-up"><span className="bolt-quickstart-steps__number">1</span><span className="bolt-quickstart-steps__label">Set up</span></a>
    </li>

    <li>
      <a href="#2-connect-orders"><span className="bolt-quickstart-steps__number">2</span><span className="bolt-quickstart-steps__label">Connect orders</span></a>
    </li>

    <li>
      <a href="#3-add-fields"><span className="bolt-quickstart-steps__number">3</span><span className="bolt-quickstart-steps__label">Add fields</span></a>
    </li>

    <li>
      <a href="#4-take-payment"><span className="bolt-quickstart-steps__number">4</span><span className="bolt-quickstart-steps__label">Take payment</span></a>
    </li>

    <li>
      <a href="#5-verify-order"><span className="bolt-quickstart-steps__number">5</span><span className="bolt-quickstart-steps__label">Verify order</span></a>
    </li>
  </ol>
</nav>

**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 [#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 <InlineBrand name="nodedotjs">Node.js</InlineBrand> and <InlineBrand name="express">Express</InlineBrand>, 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.

<IntegrationTable>
  <IntegrationRow label="Frontend SDKs">
    <IntegrationLink href="/products/embeddable-checkout" brand="javascript" title="The JavaScript embed this guide uses">
      JS Embed
    </IntegrationLink>

    <IntegrationLink href="/developers/sdks/react-native" brand="react" title="React Native SDK">
      React Native
    </IntegrationLink>
  </IntegrationRow>

  <IntegrationRow label="Backend SDKs">
    <IntegrationLink href="/api-reference/api-quickstart" icon="braces" title="Plain HTTPS calls to the Bolt API, as this guide does">
      REST API
    </IntegrationLink>

    <IntegrationLink href="https://github.com/BoltApp/Bolt-Python-SDK" brand="python" title="Bolt Python SDK: wraps the v1 order token and transaction calls">
      Python
    </IntegrationLink>
  </IntegrationRow>

  <IntegrationRow label="Commerce Platforms">
    <IntegrationLink href="https://help.boltapp.com/platforms/salesforce/sfcc-embedded/" brand="salesforce" title="Salesforce Commerce Cloud">
      Salesforce
    </IntegrationLink>
  </IntegrationRow>
</IntegrationTable>

## 1. Set up [#1-set-up]

1. Open [sandbox Payments](https://merchant-sandbox.boltapp.com/payments) and confirm Bolt Payments is configured for USD test payments.
2. Open [Administration > API](https://merchant-sandbox.boltapp.com/administration/api-keys#apiKeys). Copy the **API key**, **Publishable key**, and **Merchant Division Public ID** from the same division.
3. Open [Trusted Domains](https://merchant-sandbox.boltapp.com/administration/trusted-domains) and save your app's HTTPS origin for that division.
4. Add these values to your existing server secrets configuration:

```bash title="Server environment"
BOLT_API_KEY=YOUR_SANDBOX_API_KEY
BOLT_PUBLISHABLE_KEY=YOUR_SANDBOX_PUBLISHABLE_KEY
BOLT_DIVISION_ID=YOUR_MERCHANT_DIVISION_PUBLIC_ID
```

These names configure this example, not the SDK. Only the publishable key belongs in browser configuration.

### Validate setup [#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 [#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:

```javascript title="Checkout snapshot"
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:

```javascript title="embedded-payments.mjs"
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:

```javascript title="Express routes"
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 [#validate-orders]

1. Save the test snapshot under your shopper's session. Expose its ID to that shopper's checkout page.
2. Request `GET /api/embedded/orders/{id}` through the app. Expect `unconfirmed`.
3. Repeat as another shopper. Expect `404`, with no order details.
4. Verify `claim` permits only one concurrent attempt and survives a restart before connecting payment submission.

## 3. Add fields [#3-add-fields]

Load the sandbox embed script once in your existing page shell. Replace the publishable key:

```html title="Page shell"
<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:

```html title="Payment section"
<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.

```javascript title="embedded-checkout.js"
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 [#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 [#4-take-payment]

1. Confirm the order is **$10.00 USD** and the displayed billing ZIP is `94105`.
2. Enter `4111 1111 1111 1111`, expiration `03/30`, and CVV `737` in the hosted fields.
3. Select **Pay $10.00** once. The browser sends a token to your backend, not the card number.
4. Use **Check payment status** while capture is pending. Automatic capture is asynchronous.

### Validate submission [#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 [#5-verify-order]

1. Select **Check payment status** until the backend reports `completed`.
2. Reopen the order in your app's storage. Confirm its payment reference and completed status survived a page refresh.
3. Open [sandbox Transactions](https://merchant-sandbox.boltapp.com/transactions). Match the order ID, reference, **$10.00 USD**, and completed payment.

### Validate completion [#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 [#test-before-launch]

Run the [Embedded API test plan](/products/embedded-api-test-plan). Before production, add asynchronous payment reconciliation using [verified webhooks](/api-reference/guides/verify-webhooks), safe recovery for uncertain payments, and your order confirmation/fulfillment behavior. Add [shopper login and saved-card flows](/api-reference/guides/embeddable-checkout) as separate, explicitly enabled work; this guest-payment quickstart does not implement them.
