Pular para o conteúdo

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:

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

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

JavaScript
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

ChamadaO 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.versionO 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çãoValoresO 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"
paymentMethodslista de métodosSó estes métodos aparecem, sempre dentro dos que a fatura aceita. Vazia ou ausente, todos os da fatura
paymentMethodOrderlista de métodosSó 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
titletextoO 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'.

EventoDadosQuando
loaderstart—Logo depois do mount
readystatus, payable, methodsUma vez a cada mount. payable é falso com a fatura paga, cancelada ou estornada, ou sem método disponível
changeempty, 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
copyvalue: { 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
loaderrorerrorO pagamento não carregou, ou o iframe não abriu em 15 s (confira o endereço do site no dashboard)
resizeheightA altura do conteúdo mudou

Os erros

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

typecode
validation_errorincomplete, no_method
card_errorcard_declined
invalid_request_errornot_ready, not_payable, in_progress, not_mounted, destroyed, wrong_mode
rate_limit_errorrate_limited
api_errorapi_error, unexpected
api_connection_errortimeout (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ávelFormatoOnde vale
colorPrimary#rgb ou #rrggbbOs botões (métodos, copiar código)
colorOnPrimary#rgb ou #rrggbbO texto dos botões
colorText#rgb ou #rrggbbO texto e a base da borda dos campos
colorTextSecondary#rgb ou #rrggbbOs textos de apoio
colorTextPlaceholder#rgb ou #rrggbbO texto de exemplo dentro dos campos
colorBackground#rgb ou #rrggbbO fundo do iframe e dos campos
colorBorder#rgb ou #rrggbbA borda dos campos
colorDanger#rgb ou #rrggbbOs erros dos campos
fontFamilynomes de fonte separados por vírgula, até 120 caracteresTodo o texto. Fontes do sistema ou a Inter, que o checkout serve (não é possível carregar fontes de fora)
fontSizeBasenúmero, 12 a 20 (px)O tamanho do texto. Abaixo de 16, o celular aumenta o zoom ao focar um campo
borderRadiusnúmero, 0 a 24 (px)Os cantos dos campos e dos botões
spacingUnitnú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).

ClasseEstadosPseudoclassesOnde
.Tab, .TabIcon, .TabLabel--selected:hover :focus :active :disabledAs abas de método (layout tabs)
.AccordionItem--selected:hover :focus-visibleCada método do layout accordion
.RadioIcon--checked—O rádio de cada método do accordion
.StepOption—:hover :focus-visibleCada método na escolha do layout steps
.StepHint——A frase debaixo de cada método no steps
.StepBack—:hover :focus-visibleO "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 ::selectionOs 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-visibleOs botões de copiar
.Link—:hover :focus-visibleO "Acessar boleto"
PropriedadeFormato
colorUma cor (como abaixo) com alfa de 0,5 para cima
backgroundColor, borderColor#rgb, #rrggbb, #rrggbbaa, rgb()/rgba() ou transparent
borderWidthAté 4 comprimentos de 0 a 4px
borderStylesolid, dashed ou none
borderRadiusAté 4 comprimentos de 0 a 999px
boxShadownone, ou até duas sombras com deslocamentos até 24px, desfoque até 24px e espalhamento até 4px
paddingAté 4 comprimentos de 0 a 32px
fontFamilyComo a variável
fontSizeDe 10 a 24px
fontWeightnormal, bold ou de 300 a 800
letterSpacingDe -2 a 4px
lineHeightDe 1 a 2 sem unidade, ou de 12 a 36px
textTransformnone, uppercase, lowercase ou capitalize
transitionAté 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.