> For the complete documentation index, see [llms.txt](https://docs.loomgate.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.loomgate.io/en/integrations/browser.md).

# Payment form (loomgate.js)

Add the card form to your checkout page and confirm payments in the browser.

`loomgate.js` shows the card form (with Apple Pay and Google Pay once your domain is activated), confirms the payment, runs the 3-D Secure step and handles situations such as a declined card or a changed amount for you.

## Load loomgate.js

{% tabs %}
{% tab title="Script tag" %}

```html
<script src="https://api.loomgate.io/partner/v1/loomgate.js"></script>
<script>
  window.Loomgate.loadLoomgate('pk_live_…').then((loomgate) => {
    // ...
  });
</script>
```

The script works out the API address from its own URL.
{% endtab %}

{% tab title="npm" %}

```js
import { loadLoomgate } from '@loompay/loomgate-js-sdk';

const loomgate = await loadLoomgate('pk_live_…', {
  apiBaseUrl: 'https://api.loomgate.io', // optional: this is the default, pass it only to call another API
  locale: 'en', // 'en' (default), another language code, or 'auto' to follow the browser
  appearance: { theme: { appearance: 'light', accentColor: 'blue' } },
});
```

{% endtab %}
{% endtabs %}

Call `loadLoomgate()` once per page.

## Mount the card form

```html
<div id="loomgate-payment"></div>
<div id="loomgate-branding"></div>
```

```js
const session = loomgate.payment({ clientSecret: 'lg_cs_…' });
await session.mount({ payment: '#loomgate-payment', branding: '#loomgate-branding' });
```

* `branding` is **required**: it holds the payment partner's branding; without it the form cannot take payments and `mount()` reports `mount_target_missing`.
* `mount()` accepts a CSS selector or a DOM element. Calling it twice still gives a single form.
* The card form does not collect email, name or address: collect them on your checkout page and pass them to `confirm()`.

### Events

`session.on(name, handler)` returns a function that unsubscribes.

| Event    | When                                                                                         | Data                                                                                                                                                      |
| -------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ready`  | The form has rendered for the first time                                                     | —                                                                                                                                                         |
| `change` | The buyer enters card details / changes payment method, or the payment's amount just changed | `{ complete, amountTotal, currency }`. Use `complete` to enable the Pay button; `amountTotal` (smallest unit) is the amount that will be charged, show it |
| `error`  | The form failed to load                                                                      | `{ type, code, message }`                                                                                                                                 |

## Confirm the payment

```js
payButton.addEventListener('click', () => {
  // Call confirm() directly in the click handler, do NOT await anything before it:
  // Apple Pay and Google Pay need the buyer's actual click.
  session
    .confirm({
      billingDetails: {
        email: 'buyer@example.com',
        name: 'Jane Doe',
        address: { line1: '1 Market St', city: 'San Francisco', state: 'CA', postal_code: '94105', country: 'US' },
      },
      returnUrl: 'https://shop.example/checkout/return', // optional, must be https://
    })
    .then((result) => {
      if (result.error) {
        showError(result.error.message); // in English, can be shown to the buyer as is
        return;
      }
      // result = { status: 'succeeded' | 'processing', paymentIntentId }
      location.assign('/orders/1042/thanks');
    });
});
```

* `billingDetails.address` needs `line1`, `postal_code`, `country` (ISO 3166-1 alpha-2); `line2`, `city`, `state` are optional. Maximum lengths: email 254, `postal_code` 20, other strings 200. Missing or invalid → `validation_error` with `issues[]`, before any network call.
* `confirm()` **never throws**: every error is in `result.error`. Clicking Pay twice sends only one request.
* Declined card: `loomgate.js` releases automatically so the buyer can try another card in the same form.
* Amount changed (`payment_intent_updated`): `loomgate.js` reloads the form with the new amount and emits `change`; the buyer reviews it and clicks Pay again.
* `processing` **does not mean the money has been collected**: only fulfill the order when your server receives the `payment_intent.succeeded` webhook.

### Common errors

| `code`                                                         | Meaning                                                               | What to do                                                          |
| -------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `payment_method_declined`                                      | Card declined (may include `declineCode`)                             | Ask the buyer to use another card                                   |
| `payment_failed`                                               | The authentication step (3-D Secure) failed or was canceled           | Invite the buyer to try again                                       |
| `payment_canceled`                                             | The buyer closed the wallet window                                    | Nothing to do                                                       |
| `payment_intent_updated`                                       | The amount changed since the form loaded; nothing was charged         | Show the new amount, let the buyer click Pay again                  |
| `payment_intent_not_pending`                                   | The payment is complete, canceled, or has used all its attempts       | Create a new payment and let the buyer start over                   |
| `payment_details_missing`                                      | The payment is missing the buyer, order reference or shipping address | Update the payment from your server, then let the buyer click again |
| `rate_limited`                                                 | Too many attempts within 10 minutes                                   | Try again in a few minutes                                          |
| `request_timeout`, `payment_processing`, `next_action_timeout` | Outcome unknown                                                       | Check the order status on your server before allowing a retry       |
| `payment_form_unavailable`                                     | The card form could not load (network, CSP)                           | Call `mount()` again                                                |

## Confirm from your server

When your platform requires confirming on the server:

```js
// 1. Browser, in the click handler: only create a token
const { confirmationToken, error } = await session.createConfirmationToken(billingDetails);
// send confirmationToken + billingDetails to your server

// 2. Server: POST /partner/v1/payment_intents/{id}/confirm (secret key)
//    - 402 card_error  → POST /partner/v1/payment_intents/{id}/release, report the decline
//    - 409 payment_intent_updated → report that the total changed, create a new session to show the form again
//    - 200 → return next_action?.client_secret to the browser

// 3. Browser, if there is a next_action:
const next = await session.handleNextAction(nextActionClientSecret); // { status } | { error }
```

`session.handleNextAction()` releases automatically when the next step fails. When you need to release from the browser, call `session.release()`; only invite the buyer to try again when it returns `status: 'pending'`.

## Session lifecycle

* `session.destroy()` removes the form and stops all work in progress. A destroyed session cannot be mounted again: create a new session with `loomgate.payment(...)`.
* Maximum wait times: loading the form 60 seconds, confirming 180 seconds, releasing 10 seconds. Past that, you get `request_timeout`.

## Content-Security-Policy

The card form loads in an iframe from the payment partner's servers. If your page has a CSP, allow:

* `connect-src` and (when you use the script tag) `script-src`: `https://api.loomgate.io`;
* `script-src` and `frame-src`: the card form's domains. Contact Loomgate for the current list.

Without these entries the form doesn't show and `mount()` reports `payment_form_unavailable`.

## Privacy

`loomgate.js` collects the buyer's device data to prevent fraud. Your privacy policy must say so. See [Payment details](/en/concepts/payment-details.md).

## Full examples

The [loomgate-examples repository](https://github.com/loomgroup/loomgate-examples) has a working checkout page for each way to load `loomgate.js`, with the server part that creates the payment:

* [`examples/js`](https://github.com/loomgroup/loomgate-examples/tree/main/examples/js): the JS SDK installed from npm, bundled with Vite.
* [`examples/js-cdn`](https://github.com/loomgroup/loomgate-examples/tree/main/examples/js-cdn): the `https://api.loomgate.io/partner/v1/loomgate.js` script tag, no bundler needed. Recommended when you don't use a bundler.
* [`examples/js-umd`](https://github.com/loomgroup/loomgate-examples/tree/main/examples/js-umd): the UMD build from jsDelivr, pinned to a version and checked with SRI.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.loomgate.io/en/integrations/browser.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
