---
agentTools:
  projectIndex: https://docs.ninepay.com.br/llms.txt
  projectSearch: https://docs.ninepay.com.br/agentSearch?query=term1%20term2
---

# Embedded Checkout (ninepay.js)

The embedded checkout puts the payment of a NinePay invoice inside your page. Your page has the title, the amount and **the pay button**; the NinePay iframe has only the payment methods, the card fields and the PIX QR code, styled to match your page.

The card goes from the iframe straight to NinePay: your page never sees it.

## Before you integrate

1. **Register your site address.** In the dashboard (user menu → Integrações → Checkout embutido), register the addresses that will open the checkout: only `https://domain` or `https://domain:port`, without a path, up to five. Without any, the browser refuses the iframe.
2. **Check the plans' methods.** The iframe offers the invoice's `allowed_methods`, in this order: card, PIX Automático, PIX and bank slip. Open Finance does not appear in the embedded checkout, because the bank does not open in an iframe. Subscription invoices inherit the plan's methods.

## On your server

Everything that uses the API token stays on your server. **The token never goes to the browser.**

### A one-off charge

Create the open invoice, without a card, with `open: true` and the methods the iframe may offer:

```bash
curl -X POST https://payments.cefis.com.br/api/v1/payments \
  -H "Authorization: Bearer $NINEPAY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": { "id": 123 },
    "method": "credit",
    "open": true,
    "allowed_methods": ["credit", "pix", "billet"],
    "value": 5000,
    "due_date": "2026-11-10"
  }'
```

`allowed_methods` is required with `open`. Nothing is charged on creation: the invoice waits for payment in the iframe. Send the `payment.uuid` from the response to the page.

### A subscription

Create the subscription with the `method` and **without a card**. It starts as `new`, with the first invoice open; that invoice is the one that goes to the iframe, and paying it activates the subscription.

```bash
curl -X POST https://payments.cefis.com.br/api/v1/subscriptions \
  -H "Authorization: Bearer $NINEPAY_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": { "id": 123 },
    "plan": { "id": 456 },
    "method": "credit",
    "allowed_methods": ["credit"]
  }'
```

- The response contains `customer`, `subscription` and `payment`: send the `payment.uuid` to the page.
- **Without the `method`, the subscription is created without any invoice**, and there is nothing to pay in the iframe.
- `allowed_methods` is optional: a subset of the plan's methods, for the first invoice only (later ones use the plan's). With `["credit"]`, the iframe shows only the card.

### After the payment

- **Grant or suspend access only through the webhook**, confirming with `GET /payments/{id}`. The `paid` event on the page is for updating the screen: a browser event can be forged.
- A `new` subscription that is not paid **is not canceled automatically**. Cancel the subscription with `POST /subscription/cancel` and the body `{ "subscription": { "id": 789 } }`, and the open invoice with `DELETE /payments/{id}`.
- A canceled invoice can no longer be paid. Cancel the invoices you no longer want.

## On the page

The script always has the same address, `https://checkout.ninepay.com.br/js/v1/ninepay.js`. Fixes and new v1 features reach it without you changing anything; a breaking change gets a new address (`/js/v2/`), and v1 stays online. `Ninepay.version` reports the exact build.

The dashboard's Integrations screen generates this code with the look and options you choose.

```html
<div id="pagamento"></div>
<button id="pagar" disabled>Pagar</button>

<script src="https://checkout.ninepay.com.br/js/v1/ninepay.js"></script>
<script>
  const ninepay = Ninepay()
  const elements = ninepay.elements({
    payment: '<uuid da fatura>', // comes from your server
    // Optional: without it, the look saved in the dashboard applies
    appearance: {
      theme: 'default', // or 'night' | 'flat'
      labels: 'floating', // or 'above' | 'auto'
      inputs: 'condensed', // or 'spaced'
      variables: { colorPrimary: '#0057b8', fontFamily: 'Inter', borderRadius: 8 },
      rules: { '.Tab--selected': { borderColor: '#0057b8' } }
    }
  })
  const pagamento = elements.create('payment', {
    layout: 'tabs', // or 'accordion' | 'steps'
    paymentMethodOrder: ['credit', 'pix', 'billet'],
    saveCard: 'ask' // or 'always' | 'never'
  })
  pagamento.mount('#pagamento')

  const pagar = document.getElementById('pagar')

  // The button is only enabled once the fields of the chosen method are complete
  pagamento.on('change', event => {
    pagar.disabled = !event.complete
  })
  pagamento.on('paid', () => {
    // Paid on this screen (including PIX or bank slip paid later).
    // Update the screen; access is granted through the webhook.
  })

  pagar.addEventListener('click', async () => {
    pagar.disabled = true
    const { error: invalido } = await elements.submit() // validates, without sending
    if (invalido) {
      pagar.disabled = false
      return
    }
    const { error, payment } = await ninepay.confirmPayment({ elements })
    if (error) {
      pagar.disabled = false // the decline is already shown in the iframe
      return
    }
    // payment.status: 'succeeded' (card approved) or 'requires_action'
    // (the PIX QR code or the bank slip on screen, waiting for payment)
  })
</script>
```

> 📘 Note
>
> The iframe has no pay button. The button belongs to your page: it calls `elements.submit()` and then `ninepay.confirmPayment({ elements })`.

## Changing the card

The same script, with the subscription uuid and `mode: 'setup'`: only the card fields, and your page's button confirms.

```js
const elements = ninepay.elements({ subscription: '<uuid da assinatura>', mode: 'setup' })
elements.create('payment').mount('#cartao')

botao.onclick = async () => {
  const { error, subscription } = await ninepay.confirmSetup({ elements })
  // subscription.status: 'succeeded' (the new card is now the subscription's card)
}
```

Always use the subscription `uuid`, which comes from your server; never the numeric id.

## Reference

| Call | What it does |
| --- | --- |
| `Ninepay(options?)` | Creates the client. `options.origin` only for another environment (sandbox); without it, the origin the script was loaded from applies |
| `Ninepay.version` | The exact build, `1.<commit>` |
| `ninepay.elements({ payment, appearance? })` | The group for a payment. To change the card: `{ subscription, mode: 'setup', appearance? }` |
| `elements.create('payment', options?)` | The element. Options: `layout`, `paymentMethods`, `paymentMethodOrder`, `texts`, `saveCard`, `title` |
| `elements.update({ appearance })` | Changes the look; the mounted element follows |
| `elements.submit()` | Validates and shows errors in the iframe, without sending: `{ selectedPaymentMethod }` or `{ error }` |
| `element.mount(target)`, `unmount()`, `destroy()` | Adds and removes the iframe; after `destroy`, it can no longer be mounted |
| `element.update(options)` | Changes the element's options (does not change the invoice, the subscription or the mode) |
| `element.focus()`, `blur()` | Focus on the first empty field, or out of the iframe |
| `element.clear()` | Clears the card fields; the chosen method stays |
| `element.collapse()` | Goes back to choosing the method (`accordion` and `steps` layouts) |
| `element.on/once/off(event, fn)` | The events below; an unknown name throws an error |
| `ninepay.confirmPayment({ elements })` | Pays: `{ payment: { id, status, method } }` or `{ error }`. Always resolves |
| `ninepay.confirmSetup({ elements })` | Changes the card: `{ subscription: { id, status } }` or `{ error }` |

### Element options

| Option | Values | What it does |
| --- | --- | --- |
| `layout` | `'tabs'` (default), `'accordion'`, `{ type: 'accordion', defaultCollapsed?, radios?: 'always' \| 'never', spacedAccordionItems? }` or `'steps'` | How the person chooses the method: tabs, a list that expands the chosen method, or a choice screen with "Trocar forma de pagamento" (change payment method) |
| `paymentMethods` | list of methods | Only these methods appear, always within those the invoice accepts. Empty or missing, all of the invoice's methods |
| `paymentMethodOrder` | list of methods | Only reorders the remaining methods |
| `texts` | `'card.holder'`, `'card.number'`, `'card.expiry'`, `'card.cvc'` | Change the card field labels (text, up to 40 characters) |
| `saveCard` | `'ask'` (default), `'always'` or `'never'` | The "Cobrar automaticamente os próximos pagamentos neste cartão" (charge future payments to this card automatically) checkbox, on subscription invoices. `'always'` only applies when the subscription is already on card; the subscription's method never changes without the person agreeing |
| `title` | text | The iframe title, for screen readers |

The methods are `credit`, `pix_automatic`, `pix` and `billet`. For example, on a one-off installment of a subscription with PIX Automático, `paymentMethods: ['credit', 'pix', 'billet']` hides PIX Automático.

### Events

All of them come with `elementType: 'payment'`.

| Event | Data | When |
| --- | --- | --- |
| `loaderstart` | — | Right after `mount` |
| `ready` | `status`, `payable`, `methods` | Once per `mount`. `payable` is false when the invoice is paid, canceled or refunded, or has no available method |
| `change` | `empty`, `complete`, `collapsed`, `value: { type }` | The person chooses or switches the method, or the fields become complete or empty |
| `focus`, `blur` | — | Focus enters or leaves the iframe (also when switching browser tabs) |
| `escape` | — | The person pressed Esc inside the iframe |
| `copy` | `value: { type }` | The person copied the PIX code or the bank slip line (the code is not included). When you listen to `copy`, the "Código copiado!" notice is up to your page |
| `paid` | — | The payment made on this screen, including PIX or bank slip paid later |
| `loaderror` | `error` | The payment did not load, or the iframe did not open within 15 s (check the site address in the dashboard) |
| `resize` | `height` | The content height changed |

### Errors

Errors come as `{ type, code, message }`, with `message` in Portuguese, ready to display:

| `type` | `code` |
| --- | --- |
| `validation_error` | `incomplete`, `no_method` |
| `card_error` | `card_declined` |
| `invalid_request_error` | `not_ready`, `not_payable`, `in_progress`, `not_mounted`, `destroyed`, `wrong_mode` |
| `rate_limit_error` | `rate_limited` |
| `api_error` | `api_error`, `unexpected` |
| `api_connection_error` | `timeout` (no response within 65 s on `confirm` or 15 s on `submit`), `load_timeout` (the iframe did not open) |

## Appearance

`appearance` accepts `{ theme?, labels?, inputs?, variables?, rules? }`. Anything that does not follow the format is ignored, without an error, and the default applies.

- `theme`: `'default'` (neutral), `'night'` or `'flat'`, the starting point for the variables.
- `labels`: `'auto'` (default), `'above'` (the label above the field) or `'floating'` (inside the field, moving up as you type).
- `inputs`: `'spaced'` (default) or `'condensed'` (the card fields in a single block).

**The company's default appearance.** In Integrações → Checkout embutido, in the dashboard, you save a theme and the variables; the iframe uses this appearance when the page does not send `appearance`. What the page sends wins, variable by variable; a `theme` different from the saved one discards the saved variables. A change in the dashboard shows up within a minute.

### Variables

| Variable | Format | Where it applies |
| --- | --- | --- |
| `colorPrimary` | `#rgb` or `#rrggbb` | The buttons (methods, copy code) |
| `colorOnPrimary` | `#rgb` or `#rrggbb` | The button text |
| `colorText` | `#rgb` or `#rrggbb` | The text and the base of the field borders |
| `colorTextSecondary` | `#rgb` or `#rrggbb` | Supporting text |
| `colorTextPlaceholder` | `#rgb` or `#rrggbb` | The placeholder text inside the fields |
| `colorBackground` | `#rgb` or `#rrggbb` | The background of the iframe and the fields |
| `colorBorder` | `#rgb` or `#rrggbb` | The field borders |
| `colorDanger` | `#rgb` or `#rrggbb` | Field errors |
| `fontFamily` | comma-separated font names, up to 120 characters | All text. System fonts or `Inter`, which the checkout serves (external fonts cannot be loaded) |
| `fontSizeBase` | number, 12 to 20 (px) | The text size. Below 16, phones zoom in when a field gets focus |
| `borderRadius` | number, 0 to 24 (px) | The corners of fields and buttons |
| `spacingUnit` | number, 1 to 8 (px) | The padding inside the fields (default 2) |

> 🚧 Contrast
>
> Contrast of the variables is the integrator's responsibility. For the fields to be legible and accessible, use text and background with at least 4.5:1, and border and background with 3:1 (WCAG 1.4.3 and 1.4.11).

### Rules

`rules` applies CSS by public class: `{ ".Tab--selected": { "borderColor": "#47a472" } }`. Each selector is a class, with at most one state and one pseudo-class, in that order (`.Class--state:pseudo`).

| Class | States | Pseudo-classes | Where |
| --- | --- | --- | --- |
| `.Tab`, `.TabIcon`, `.TabLabel` | `--selected` | `:hover` `:focus` `:active` `:disabled` | The method tabs (`tabs` layout) |
| `.AccordionItem` | `--selected` | `:hover` `:focus-visible` | Each method in the `accordion` layout |
| `.RadioIcon` | `--checked` | — | The radio of each `accordion` method |
| `.StepOption` | — | `:hover` `:focus-visible` | Each method in the `steps` layout choice |
| `.StepHint` | — | — | The sentence under each method in `steps` |
| `.StepBack` | — | `:hover` `:focus-visible` | The "Trocar forma de pagamento" (change payment method) link in `steps` |
| `.Label` | `--empty` `--invalid` `--focused` `--floating` `--resting` | — | The label of each card field |
| `.Input` | `--empty` `--invalid` `--complete` | `:hover` `:focus` `:disabled` `:autofill` `::placeholder` `::selection` | The card fields |
| `.Error` | — | — | Each field's error and the card decline |
| `.Block` | — | — | The box with the PIX code and the bank slip line |
| `.Text` | — | — | The PIX, PIX Automático and bank slip texts |
| `.CopyButton` | — | `:hover` `:focus-visible` | The copy buttons |
| `.Link` | — | `:hover` `:focus-visible` | The "Acessar boleto" (open bank slip) link |

| Property | Format |
| --- | --- |
| `color` | A color (as below) with alpha of 0.5 or higher |
| `backgroundColor`, `borderColor` | `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()`/`rgba()` or `transparent` |
| `borderWidth` | Up to 4 lengths from 0 to 4px |
| `borderStyle` | `solid`, `dashed` or `none` |
| `borderRadius` | Up to 4 lengths from 0 to 999px |
| `boxShadow` | `none`, or up to two shadows with offsets up to 24px, blur up to 24px and spread up to 4px |
| `padding` | Up to 4 lengths from 0 to 32px |
| `fontFamily` | Same as the variable |
| `fontSize` | From 10 to 24px |
| `fontWeight` | `normal`, `bold` or from `300` to `800` |
| `letterSpacing` | From -2 to 4px |
| `lineHeight` | From `1` to `2` unitless, or from 12 to 36px |
| `textTransform` | `none`, `uppercase`, `lowercase` or `capitalize` |
| `transition` | Up to 4 of `color`, `background-color`, `border-color` or `box-shadow`, with the duration in ms |

A length is `0` or a number in `px`, `rem` or `em` (`rem` and `em` count as 16px). Not accepted: `url()`, `var()`, `!important`, attribute, descendant or sibling selectors, or properties that change the layout (`position`, `display`, `width`, `height`, `margin`, `transform` and similar). Limits: 40 selectors, 12 properties per selector and 120 characters per value. A rule with `color` or `backgroundColor` contrast below 3:1 is discarded. In older browsers (before Safari 16.4), rules do not apply and only the variables are used.

## Precautions

- Always load the script from `https://checkout.ninepay.com.br`, never from a copy or a CDN: the iframe's origin is the one the script came from. For the sandbox, pass `Ninepay({ origin: 'https://...' })`; only `https` is accepted (or `http://localhost`, for testing).
- The page's CSP must allow the checkout: `script-src` and `frame-src` with `https://checkout.ninepay.com.br`.
- Display any data that comes from the iframe as text, never in `innerHTML`.
- The script does not use SRI, because it receives fixes at the same address. If you need SRI, ask NinePay for a frozen copy.
- Do not mount the checkout inside another iframe. If your page puts `sandbox` on a surrounding iframe, include `allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox` (the bank slip PDF opens in another tab).
- The iframe limits confirmations (one at a time and five per minute), but protection against card testing is NinePay's, on the server.
