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

# Checkout Embutido (ninepay.js)

O checkout embutido coloca o pagamento de uma fatura da NinePay dentro da sua página. A sua página tem o título, o valor e **o botão de pagar**; o iframe da NinePay tem só os métodos de pagamento, os campos do cartão e o QR do PIX, com a aparência da sua página.

O cartão vai do iframe direto para a NinePay: a sua página nunca o vê.

## Antes de integrar

1. **Cadastre o endereço do site.** No dashboard (menu do usuário → Integrações → Checkout embutido), cadastre os endereços que vão abrir o checkout: só `https://dominio` ou `https://dominio:porta`, sem caminho, até cinco. Sem nenhum, o navegador recusa o iframe.
2. **Confira os métodos dos planos.** O iframe oferece os `allowed_methods` da fatura, nesta ordem: cartão, PIX Automático, PIX e boleto. O Open Finance não aparece no embutido, porque o banco não abre num iframe. As faturas de assinatura herdam os métodos do plano.

## No seu servidor

Tudo o que usa o token da API fica no seu servidor. **O token nunca vai para o navegador.**

### Uma cobrança avulsa

Crie a fatura em aberto, sem cartão, com `open: true` e os métodos que o iframe pode oferecer:

```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` é obrigatório com `open`. Nada é cobrado na criação: a fatura espera o pagamento no iframe. Mande para a página o `payment.uuid` da resposta.

### Uma assinatura

Crie a assinatura com o `method` e **sem cartão**. Ela nasce em `new`, com a primeira fatura em aberto; é essa fatura que vai para o iframe, e pagá-la ativa a assinatura.

```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"]
  }'
```

- A resposta traz `customer`, `subscription` e `payment`: mande para a página o `payment.uuid`.
- **Sem o `method`, a assinatura é criada sem nenhuma fatura**, e não há o que pagar no iframe.
- `allowed_methods` é opcional: um subconjunto dos métodos do plano, só para a primeira fatura (as seguintes usam os do plano). Com `["credit"]`, o iframe mostra só o cartão.

### Depois do pagamento

- **Libere ou suspenda o acesso só pelo webhook**, confirmando com `GET /payments/{id}`. O evento `paid` na página serve para atualizar a tela: um evento no navegador pode ser forjado.
- Uma assinatura em `new` que não é paga **não é cancelada sozinha**. Cancele a assinatura com `POST /subscription/cancel` e o corpo `{ "subscription": { "id": 789 } }`, e a fatura aberta com `DELETE /payments/{id}`.
- Fatura cancelada não pode mais ser paga. Cancele as faturas que você não quer mais.

## Na página

O script é sempre o mesmo endereço, `https://checkout.ninepay.com.br/js/v1/ninepay.js`. As correções e os recursos novos da v1 chegam nele sem você trocar nada; uma mudança incompatível vira outro endereço (`/js/v2/`), e a v1 continua no ar. `Ninepay.version` informa o build exato.

A tela Integrações do dashboard gera este código já com a aparência e as opções que você escolher.

```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>', // vem do seu servidor
    // Opcional: sem ela, vale a aparência salva no dashboard
    appearance: {
      theme: 'default', // ou 'night' | 'flat'
      labels: 'floating', // ou 'above' | 'auto'
      inputs: 'condensed', // ou 'spaced'
      variables: { colorPrimary: '#0057b8', fontFamily: 'Inter', borderRadius: 8 },
      rules: { '.Tab--selected': { borderColor: '#0057b8' } }
    }
  })
  const pagamento = elements.create('payment', {
    layout: 'tabs', // ou 'accordion' | 'steps'
    paymentMethodOrder: ['credit', 'pix', 'billet'],
    saveCard: 'ask' // ou 'always' | 'never'
  })
  pagamento.mount('#pagamento')

  const pagar = document.getElementById('pagar')

  // O botão só fica ativo com os campos do método escolhido completos
  pagamento.on('change', event => {
    pagar.disabled = !event.complete
  })
  pagamento.on('paid', () => {
    // Pago nesta tela (inclusive o PIX ou o boleto pagos depois).
    // Atualize a tela; o acesso se libera pelo webhook.
  })

  pagar.addEventListener('click', async () => {
    pagar.disabled = true
    const { error: invalido } = await elements.submit() // valida, sem enviar
    if (invalido) {
      pagar.disabled = false
      return
    }
    const { error, payment } = await ninepay.confirmPayment({ elements })
    if (error) {
      pagar.disabled = false // a recusa já aparece no iframe
      return
    }
    // payment.status: 'succeeded' (cartão aprovado) ou 'requires_action'
    // (o QR do PIX ou o boleto na tela, à espera do pagamento)
  })
</script>
```

> 📘 Nota
>
> O iframe não tem botão de pagar. O botão é da sua página: ele chama `elements.submit()` e depois `ninepay.confirmPayment({ elements })`.

## A troca de cartão

O mesmo script, com o uuid da assinatura e `mode: 'setup'`: só os campos do cartão, e o botão da sua página confirma.

```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' (o cartão novo já é o da assinatura)
}
```

Use sempre o `uuid` da assinatura, que vem do seu servidor; nunca o id numérico.

## Referência

| Chamada | O que faz |
| --- | --- |
| `Ninepay(options?)` | Cria o cliente. `options.origin` só para outro ambiente (sandbox); sem ele, vale a origem de onde o script veio |
| `Ninepay.version` | O build exato, `1.<commit>` |
| `ninepay.elements({ payment, appearance? })` | O grupo de um pagamento. Para a troca de cartão: `{ subscription, mode: 'setup', appearance? }` |
| `elements.create('payment', options?)` | O elemento. Opções: `layout`, `paymentMethods`, `paymentMethodOrder`, `texts`, `saveCard`, `title` |
| `elements.update({ appearance })` | Troca a aparência; o elemento montado acompanha |
| `elements.submit()` | Valida e mostra os erros no iframe, sem enviar: `{ selectedPaymentMethod }` ou `{ error }` |
| `element.mount(alvo)`, `unmount()`, `destroy()` | Põe e tira o iframe; depois do `destroy`, não monta mais |
| `element.update(options)` | Troca as opções do elemento (não troca a fatura, a assinatura nem o modo) |
| `element.focus()`, `blur()` | O foco no primeiro campo vazio, ou fora do iframe |
| `element.clear()` | Esvazia os campos do cartão; o método escolhido fica |
| `element.collapse()` | Volta para a escolha do método (layouts `accordion` e `steps`) |
| `element.on/once/off(evento, fn)` | Os eventos abaixo; um nome desconhecido lança erro |
| `ninepay.confirmPayment({ elements })` | Paga: `{ payment: { id, status, method } }` ou `{ error }`. Sempre resolve |
| `ninepay.confirmSetup({ elements })` | Troca o cartão: `{ subscription: { id, status } }` ou `{ error }` |

### As opções do elemento

| Opção | Valores | O que faz |
| --- | --- | --- |
| `layout` | `'tabs'` (padrão), `'accordion'`, `{ type: 'accordion', defaultCollapsed?, radios?: 'always' \| 'never', spacedAccordionItems? }` ou `'steps'` | Como a pessoa escolhe o método: abas, uma lista que abre o método escolhido, ou uma tela de escolha com "Trocar forma de pagamento" |
| `paymentMethods` | lista de métodos | Só estes métodos aparecem, sempre dentro dos que a fatura aceita. Vazia ou ausente, todos os da fatura |
| `paymentMethodOrder` | lista de métodos | Só reordena os métodos que sobraram |
| `texts` | `'card.holder'`, `'card.number'`, `'card.expiry'`, `'card.cvc'` | Trocam os rótulos dos campos do cartão (texto, até 40 caracteres) |
| `saveCard` | `'ask'` (padrão), `'always'` ou `'never'` | A caixa "Cobrar automaticamente os próximos pagamentos neste cartão", na fatura de assinatura. `'always'` só vale com a assinatura já no cartão; o método da assinatura nunca muda sem a pessoa aceitar |
| `title` | texto | O título do iframe, para leitores de tela |

Os métodos são `credit`, `pix_automatic`, `pix` e `billet`. Por exemplo, numa parcela avulsa de uma assinatura com PIX Automático, `paymentMethods: ['credit', 'pix', 'billet']` esconde o PIX Automático.

### Os eventos

Todos chegam com `elementType: 'payment'`.

| Evento | Dados | Quando |
| --- | --- | --- |
| `loaderstart` | — | Logo depois do `mount` |
| `ready` | `status`, `payable`, `methods` | Uma vez a cada `mount`. `payable` é falso com a fatura paga, cancelada ou estornada, ou sem método disponível |
| `change` | `empty`, `complete`, `collapsed`, `value: { type }` | A pessoa escolhe ou troca o método, ou os campos ficam completos ou vazios |
| `focus`, `blur` | — | O foco entra ou sai do iframe (também ao trocar de aba do navegador) |
| `escape` | — | A pessoa apertou Esc dentro do iframe |
| `copy` | `value: { type }` | A pessoa copiou o código do PIX ou a linha do boleto (o código não vem). Com o `copy` escutado, o aviso "Código copiado!" é da sua página |
| `paid` | — | O pagamento feito nesta tela, inclusive o PIX ou o boleto pagos depois |
| `loaderror` | `error` | O pagamento não carregou, ou o iframe não abriu em 15 s (confira o endereço do site no dashboard) |
| `resize` | `height` | A altura do conteúdo mudou |

### Os erros

Os erros vêm como `{ type, code, message }`, com `message` em português, pronta para mostrar:

| `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` (sem resposta em 65 s no `confirm` ou em 15 s no `submit`), `load_timeout` (o iframe não abriu) |

## A aparência

`appearance` aceita `{ theme?, labels?, inputs?, variables?, rules? }`. O que não segue o formato é ignorado, sem erro, e fica o padrão.

- `theme`: `'default'` (neutro), `'night'` ou `'flat'`, o ponto de partida das variáveis.
- `labels`: `'auto'` (padrão), `'above'` (o rótulo em cima do campo) ou `'floating'` (dentro do campo, subindo ao digitar).
- `inputs`: `'spaced'` (padrão) ou `'condensed'` (os campos do cartão num bloco só).

**A aparência padrão da empresa.** Em Integrações → Checkout embutido, no dashboard, você salva um tema e as variáveis; o iframe usa essa aparência quando a página não manda `appearance`. O que a página manda vence, variável por variável; um `theme` diferente do salvo descarta as variáveis salvas. Uma mudança no dashboard aparece em até um minuto.

### As variáveis

| Variável | Formato | Onde vale |
| --- | --- | --- |
| `colorPrimary` | `#rgb` ou `#rrggbb` | Os botões (métodos, copiar código) |
| `colorOnPrimary` | `#rgb` ou `#rrggbb` | O texto dos botões |
| `colorText` | `#rgb` ou `#rrggbb` | O texto e a base da borda dos campos |
| `colorTextSecondary` | `#rgb` ou `#rrggbb` | Os textos de apoio |
| `colorTextPlaceholder` | `#rgb` ou `#rrggbb` | O texto de exemplo dentro dos campos |
| `colorBackground` | `#rgb` ou `#rrggbb` | O fundo do iframe e dos campos |
| `colorBorder` | `#rgb` ou `#rrggbb` | A borda dos campos |
| `colorDanger` | `#rgb` ou `#rrggbb` | Os erros dos campos |
| `fontFamily` | nomes de fonte separados por vírgula, até 120 caracteres | Todo o texto. Fontes do sistema ou a `Inter`, que o checkout serve (não é possível carregar fontes de fora) |
| `fontSizeBase` | número, 12 a 20 (px) | O tamanho do texto. Abaixo de 16, o celular aumenta o zoom ao focar um campo |
| `borderRadius` | número, 0 a 24 (px) | Os cantos dos campos e dos botões |
| `spacingUnit` | número, 1 a 8 (px) | O espaço por dentro dos campos (padrão 2) |

> 🚧 Contraste
>
> O contraste das variáveis é responsabilidade de quem integra. Para os campos serem legíveis e acessíveis, use texto e fundo com pelo menos 4,5:1 e borda e fundo com 3:1 (WCAG 1.4.3 e 1.4.11).

### As regras

`rules` aplica CSS por classe pública: `{ ".Tab--selected": { "borderColor": "#47a472" } }`. Cada seletor é uma classe, com no máximo um estado e uma pseudoclasse, nessa ordem (`.Classe--estado:pseudo`).

| Classe | Estados | Pseudoclasses | Onde |
| --- | --- | --- | --- |
| `.Tab`, `.TabIcon`, `.TabLabel` | `--selected` | `:hover` `:focus` `:active` `:disabled` | As abas de método (layout `tabs`) |
| `.AccordionItem` | `--selected` | `:hover` `:focus-visible` | Cada método do layout `accordion` |
| `.RadioIcon` | `--checked` | — | O rádio de cada método do `accordion` |
| `.StepOption` | — | `:hover` `:focus-visible` | Cada método na escolha do layout `steps` |
| `.StepHint` | — | — | A frase debaixo de cada método no `steps` |
| `.StepBack` | — | `:hover` `:focus-visible` | O "Trocar forma de pagamento" do `steps` |
| `.Label` | `--empty` `--invalid` `--focused` `--floating` `--resting` | — | O rótulo de cada campo do cartão |
| `.Input` | `--empty` `--invalid` `--complete` | `:hover` `:focus` `:disabled` `:autofill` `::placeholder` `::selection` | Os campos do cartão |
| `.Error` | — | — | O erro de cada campo e a recusa do cartão |
| `.Block` | — | — | A caixa com o código do PIX e a linha do boleto |
| `.Text` | — | — | Os textos do PIX, do PIX Automático e do boleto |
| `.CopyButton` | — | `:hover` `:focus-visible` | Os botões de copiar |
| `.Link` | — | `:hover` `:focus-visible` | O "Acessar boleto" |

| Propriedade | Formato |
| --- | --- |
| `color` | Uma cor (como abaixo) com alfa de 0,5 para cima |
| `backgroundColor`, `borderColor` | `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()`/`rgba()` ou `transparent` |
| `borderWidth` | Até 4 comprimentos de 0 a 4px |
| `borderStyle` | `solid`, `dashed` ou `none` |
| `borderRadius` | Até 4 comprimentos de 0 a 999px |
| `boxShadow` | `none`, ou até duas sombras com deslocamentos até 24px, desfoque até 24px e espalhamento até 4px |
| `padding` | Até 4 comprimentos de 0 a 32px |
| `fontFamily` | Como a variável |
| `fontSize` | De 10 a 24px |
| `fontWeight` | `normal`, `bold` ou de `300` a `800` |
| `letterSpacing` | De -2 a 4px |
| `lineHeight` | De `1` a `2` sem unidade, ou de 12 a 36px |
| `textTransform` | `none`, `uppercase`, `lowercase` ou `capitalize` |
| `transition` | Até 4 de `color`, `background-color`, `border-color` ou `box-shadow`, com a duração em ms |

Comprimento é `0` ou um número em `px`, `rem` ou `em` (`rem` e `em` contam como 16px). Não são aceitos `url()`, `var()`, `!important`, seletores de atributo, descendente ou irmão, nem as propriedades que mudam o layout (`position`, `display`, `width`, `height`, `margin`, `transform` e semelhantes). Limites: 40 seletores, 12 propriedades por seletor e 120 caracteres por valor. Uma regra com `color` ou `backgroundColor` de contraste abaixo de 3:1 é descartada. Em navegadores antigos (antes do Safari 16.4), as regras não se aplicam e valem só as variáveis.

## Cuidados

- Carregue o script sempre de `https://checkout.ninepay.com.br`, nunca de uma cópia ou de um CDN: a origem do iframe é a de onde o script veio. Para o sandbox, passe `Ninepay({ origin: 'https://...' })`; só `https` é aceito (ou `http://localhost`, para testar).
- O CSP da página precisa liberar o checkout: `script-src` e `frame-src` com `https://checkout.ninepay.com.br`.
- Mostre qualquer dado que venha do iframe como texto, nunca em `innerHTML`.
- O script não usa SRI, porque recebe correções no mesmo endereço. Se você precisa de SRI, peça à NinePay uma cópia congelada.
- Não monte o checkout dentro de outro iframe. Se a sua página puser `sandbox` num iframe em volta, inclua `allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox` (o PDF do boleto abre em outra aba).
- O iframe limita as confirmações (uma por vez e cinco por minuto), mas a proteção contra teste de cartão é a da NinePay, no servidor.
