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
- 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://dominioouhttps://dominio:porta, sem caminho, até cinco. Sem nenhum, o navegador recusa o iframe. - Confira os métodos dos planos. O iframe oferece os
allowed_methodsda 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:
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.
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,subscriptionepayment: mande para a página opayment.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 eventopaidna página serve para atualizar a tela: um evento no navegador pode ser forjado. - Uma assinatura em
newque não é paga não é cancelada sozinha. Cancele a assinatura comPOST /subscription/cancele o corpo{ "subscription": { "id": 789 } }, e a fatura aberta comDELETE /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.
<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.
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, passeNinepay({ origin: 'https://...' }); sóhttpsé aceito (ouhttp://localhost, para testar). - O CSP da página precisa liberar o checkout:
script-srceframe-srccomhttps://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
sandboxnum iframe em volta, incluaallow-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.