Minstrel

Pagamentos

Checkout transparente

Receba o cartao na pagina do lojista sem o numero passar pelo Minstrel - o servidor cria o pagamento, o SDK @minstrel/js tokeniza no navegador direto no adquirente e confirma, com cartao salvo mediante consentimento, sandbox, erros e responsabilidades de PCI.

No checkout transparente o portador digita o cartao na sua pagina, sem redirecionamento nem iframe do Minstrel. O que muda em relacao a um formulario comum e o caminho do numero: ele vai do navegador direto para o adquirente, que devolve um token de uso unico. O Minstrel so recebe esse token (e, no maximo, bandeira, BIN e ultimos 4 digitos) - nunca o numero nem o CVV.

Quem integra faz tres coisas:

  1. o servidor cria o pagamento sem cartao e entrega paymentId e clientSecret a pagina;
  2. a pagina usa o SDK @minstrel/js para marcar os campos do cartao e chamar confirmCardPayment;
  3. o servidor acompanha o resultado pelo webhook e libera o pedido.

Como funciona

Servidor do lojista     Navegador (pagina do lojista)     Minstrel API          Adquirente
        |                           |                          |                    |
 1. POST /v1/payments (sem cartao, chave de API) ------------->|                    |
        |<------ paymentId + clientSecret (requires_payment_method) ----------------|
        |                           |                          |                    |
 2. entrega os dois a pagina ------>|                          |                    |
        |                           | 3. sessao de tokenizacao |                    |
        |                           |------------------------->|--- AccessToken --->|
        |                           |<-- credencial + script --|                    |
        |                           |                                               |
        |                           | 4. o script le os campos; o cartao vai DIRETO |
        |                           |---------------------------------------------->|
        |                           |<--------------- token de uso unico -----------|
        |                           |                          |                    |
        |                           | 5. confirma com o token  |                    |
        |                           |------------------------->|--- autoriza ------>|
        |                           |<---- pagamento (captured, authorized ou failed)
        |                           |                          |                    |
 6. webhook payment.captured (ou payment.failed) <-------------|                    |
  • Entre os passos 1 e 5 o pagamento fica em requires_payment_method ("Aguardando cartao" no painel). O prazo e de 24 horas, em actionExpiresAt.
  • O passo 4 e o unico em que o numero do cartao existe, e acontece entre o navegador e o adquirente. O Minstrel nao esta no caminho.
  • Confirmado, o pagamento segue o caminho normal de um cartao: roteamento, fluxo, autorizacao, captura e eventos.

1. Criar o pagamento no servidor

POST /v1/payments sem card e sem paymentMethodId. Dois campos opcionais entram nesse modo:

CampoPara que serve
customerIdO cliente do pedido. Obrigatorio para o portador poder salvar o cartao (veja Salvar o cartao).
tokenizationAccountIdA conta de conector (ca_01HQ...) que tokeniza e cobra o cartao. Omitido, vale a unica conta habilitada que tokeniza no navegador; havendo duas ou mais, informe a conta (sem isso a API responde 422 tokenization_account_ambiguous).
curl https://api.minstrel.com.br/v1/payments \
  -H "Authorization: Bearer $MINSTREL_API_KEY" \
  -H "Idempotency-Key: pedido-1234" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 14990,
    "currency": "BRL",
    "customerId": "cus_01M5...",
    "description": "Pedido 1234",
    "externalId": "pedido-1234"
  }'

O mesmo pedido em Node, no seu servidor:

const response = await fetch('https://api.minstrel.com.br/v1/payments', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MINSTREL_API_KEY}`,
    'Idempotency-Key': `pedido-${order.id}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: order.totalInCents,
    currency: 'BRL',
    customerId: order.customer.minstrelId,
    description: `Pedido ${order.id}`,
    externalId: String(order.id),
  }),
});
if (!response.ok) throw new Error(`Minstrel respondeu ${response.status}`);
const payment = await response.json();

// Entregue so estes dois campos a pagina do portador.
return { paymentId: payment.id, clientSecret: payment.clientSecret };

Resposta (resumida):

{
  "object": "payment",
  "id": "pay_01M5...",
  "status": "requires_payment_method",
  "amount": 14990,
  "methodType": "card",
  "clientSecret": "pay_01M5..._secret_Zk3pQ8...",
  "actionExpiresAt": "2026-10-18T14:02:11.000Z"
}
  • clientSecret (<paymentId>_secret_...) e a credencial que deixa o navegador do portador operar esse pagamento e so ele: ler a situacao, pedir a sessao de tokenizacao e confirmar. Entregue-o a pagina do pedido e a mais ninguem: nao o grave em log, nao o envie por e-mail nem o guarde no seu banco. A chave de API nunca vai para o navegador.
  • O segredo e derivado do pagamento: vem igual em toda resposta (criacao, GET /v1/payments/:id, repeticao da mesma requisicao idempotente) enquanto o portador ainda tem o que fazer com ele, isto e, informar o cartao ou concluir uma acao. Depois disso o segredo deixa de vir na resposta, e quem o tiver so consegue ler a situacao publica do pagamento.
  • O valor e a moeda ficam fixados no servidor. A pagina nao consegue mudar o amount - e por isso que o pagamento nasce no seu back-end.
  • Idempotency-Key vale como em qualquer criacao: repetir a chamada devolve o mesmo pagamento (e o mesmo clientSecret).
  • O painel mostra o pagamento como Aguardando cartao, com o prazo, mas nunca exibe o clientSecret.

2. A pagina do portador

Instale o SDK no projeto da sua pagina. Ele e um pacote npm que voce inclui no seu bundle; nao ha script do Minstrel para carregar de um CDN:

npm install @minstrel/js

Os campos do cartao sao seus: voce controla o visual, a mascara e a validacao de interface. Duas regras de HTML:

<form id="checkout">
  <input id="holder" type="text" autocomplete="cc-name" placeholder="Nome como esta no cartao" />
  <input
    id="number"
    type="text"
    inputmode="numeric"
    autocomplete="cc-number"
    placeholder="Numero do cartao"
  />
  <input id="expiry" type="text" inputmode="numeric" autocomplete="cc-exp" placeholder="MM/AA" />
  <input id="cvv" type="text" inputmode="numeric" autocomplete="cc-csc" placeholder="CVV" />

  <label id="save-option" hidden>
    <input id="save" type="checkbox" />
    Salvar este cartao para as proximas compras
  </label>

  <button type="submit">Pagar R$ 149,90</button>
  <p id="error" role="alert"></p>
</form>
  • Sem o atributo name nos campos do cartao. Um campo com name entra no envio do formulario: se o formulario for enviado ao seu servidor (um submit que escapou do preventDefault, um plugin de analytics que serializa formularios), o numero do cartao chega ao seu back-end e aos seus logs, e o seu escopo de PCI cresce. Sem name, o navegador nao o inclui no envio. O SDK nao deixa passar: cardFields e confirmCardPayment recusam campos com name (MinstrelError do tipo validation, codigo card_field_has_name) - cuidado com bibliotecas de formulario e de componentes que acrescentam name sozinhas.
  • Nao copie o valor dos campos para a sua aplicacao. Em React, use campos nao controlados (nada de value e onChange guardando o numero em estado); em qualquer biblioteca de formularios, deixe os campos do cartao de fora do estado e da validacao que serializa o valor. O SDK nunca le o valor dos campos: quem le e o script do adquirente.

O JavaScript da pagina:

import { loadMinstrel, MinstrelError } from '@minstrel/js';

const minstrel = loadMinstrel({ apiBaseUrl: 'https://api.minstrel.com.br' });

// paymentId e clientSecret vieram do seu servidor (passo 1).
const { paymentId, clientSecret } = await fetch('/api/checkout/pedido-1234').then((r) => r.json());

// Marca os seus campos para o script do adquirente. Nao le nem guarda valores.
const card = minstrel.cardFields({
  holderName: document.getElementById('holder'),
  number: document.getElementById('number'),
  expiry: document.getElementById('expiry'),
  cvv: document.getElementById('cvv'),
});

// So ofereca "salvar cartao" se o pagamento aceitar (precisa de customerId).
const info = await minstrel.retrievePayment({ paymentId, clientSecret });
document.getElementById('save-option').hidden = !info.saveCardAllowed;

document.getElementById('checkout').addEventListener('submit', async (event) => {
  event.preventDefault();
  const error = document.getElementById('error');
  error.textContent = '';

  try {
    const payment = await minstrel.confirmCardPayment({
      paymentId,
      clientSecret,
      card,
      saveCard: document.getElementById('save').checked,
    });

    if (payment.status === 'failed') {
      // Recusa do emissor: este pagamento terminou. Veja "Recusa e nova tentativa".
      error.textContent = payment.decline?.consumerMessage ?? 'O cartao foi recusado.';
      return;
    }
    if (payment.status === 'requires_action') {
      // Desafio do portador (3DS): o SDK ainda nao o conduz nesta versao.
      error.textContent = 'Nao foi possivel concluir a autenticacao do cartao.';
      return;
    }
    // captured ou authorized: mostre "recebemos o seu pedido". Quem libera o
    // pedido e o seu servidor, pelo webhook (passo 3).
    window.location.assign('/pedido/obrigado');
  } catch (caught) {
    if (!(caught instanceof MinstrelError)) throw caught;
    // A mensagem do erro e para voce, nao para o portador. Mostre um texto seu.
    if (caught.type === 'validation' && caught.fields?.length) {
      error.textContent = caught.fields.map((issue) => issue.message).join(' ');
    } else {
      error.textContent =
        'Nao foi possivel concluir o pagamento. Confira os dados e tente de novo.';
    }
    console.error(caught.type, caught.code, caught.requestId); // nunca ha dado de cartao aqui
  }
});

// Ao desmontar o formulario (troca de tela, componente removido):
// card.destroy();

O que confirmCardPayment faz, em ordem:

  1. confere a pagina: os quatro campos existem, estao no documento e nao tem name (se algo estiver errado, falha aqui, antes de gastar uma sessao);
  2. pede ao Minstrel uma sessao de tokenizacao para o pagamento (uma credencial de uso unico do adquirente, gerada a cada tentativa);
  3. carrega o script do adquirente (na Cielo, o Silent Order Post), que le os campos marcados por cardFields e envia o cartao direto ao adquirente;
  4. recebe o token de uso unico (na Cielo, o PaymentToken, que vale 20 minutos);
  5. confirma o pagamento no Minstrel com esse token, mais bandeira, BIN e ultimos 4 digitos quando o adquirente os devolve, e os dados do navegador (idioma, tela e fuso, para antifraude e autenticacao), e devolve o pagamento.

Detalhes que poupam suporte:

  • Duplo clique em "Pagar" e seguro. Enquanto uma confirmacao do mesmo pagamento esta em andamento, uma segunda chamada devolve a mesma promessa em vez de abrir outra tokenizacao.
  • retrievePayment({ paymentId, clientSecret }) devolve a visao publica do pagamento (status, amount, merchant.name, saveCardAllowed, expiresAt e, numa recusa, decline). Use depois de uma queda de rede para saber como a confirmacao terminou (veja Erros).
  • loadMinstrel aceita requestTimeoutMs (30 s por chamada ao Minstrel), scriptTimeoutMs (15 s para carregar o script do adquirente) e tokenizationTimeoutMs (60 s para o adquirente devolver o token).

Salvar o cartao, com consentimento

saveCard: true e o consentimento do portador para guardar o cartao e cobra-lo no futuro, sem ele presente (recorrencia, parcelas, cobranca posterior). Por isso:

  • ofereca a opcao desmarcada por padrao, com texto claro sobre o uso ("Salvar este cartao para as proximas compras"). Nunca envie saveCard: true sem o portador ter marcado. Mostre a opcao so quando saveCardAllowed for true no retrievePayment;
  • o pagamento precisa ter customerId: o cartao salvo fica na carteira do cliente, e saveCard: true num pagamento sem cliente e recusado;
  • com saveCard: true, a autorizacao pede ao adquirente que guarde o cartao, e o token de uso unico e trocado pelo token permanente do cofre. O cartao aparece em GET /v1/customers/:id/payment-methods e pode ser usado em assinaturas e cobrancas do estabelecimento (MIT);
  • sem saveCard, o token e de uso unico e nunca mais pode ser cobrado. O pagamento fica no historico com bandeira e ultimos 4 digitos, nada alem.

Se a autorizacao for recusada, o cartao nao e salvo.

Confirmar pelo servidor

Quem nao usa o SDK (aplicativo nativo, por exemplo, que tokeniza com o SDK do proprio adquirente) confirma pelo servidor, com a chave de API:

curl -X POST https://api.minstrel.com.br/v1/payments/pay_01M5.../confirm \
  -H "Authorization: Bearer $MINSTREL_API_KEY" \
  -H "Idempotency-Key: confirma-pedido-1234" \
  -H "Content-Type: application/json" \
  -d '{
    "card": { "token": "3f9a...", "brand": "visa", "bin": "411111", "last4": "1111" },
    "saveCard": true
  }'

card.token e o token de uso unico do adquirente; brand, bin e last4 sao opcionais e servem so para exibicao. O campo device (dados do navegador, para antifraude e autenticacao) tambem e opcional. A confirmacao exige o escopo payments:write.

O SDK usa tres rotas publicas, pensadas para o navegador. Elas nao aceitam chave de API: a credencial e o header Minstrel-Client-Secret, valido para um unico pagamento. Tem CORS aberto e limite de requisicoes:

RotaO que faz
GET /v1/public/payments/:idLe a visao publica do pagamento: situacao, valor, nome da loja, saveCardAllowed, expiresAt.
POST /v1/public/payments/:id/tokenization-sessionCria a sessao de tokenizacao: credencial de uso unico e script do adquirente.
POST /v1/public/payments/:id/confirmConfirma com { card: { token, brand?, bin?, last4? }, saveCard?, device? }.

Os limites de hoje: 60 chamadas por minuto por IP em todas as rotas publicas somadas e, por pagamento e por hora, 10 sessoes de tokenizacao e 5 confirmacoes. Passou disso, a API responde 429 com Retry-After. O limite por pagamento so conta depois que o clientSecret confere, e freia quem testa cartoes em sequencia - um portador legitimo raramente chega perto dele.

Cancelar um pagamento que nao vai mais ser pago

POST /v1/payments/:id/void cancela um pagamento ainda em requires_payment_method (pedido cancelado, carrinho abandonado). Depois disso a pagina nao consegue mais confirma-lo. Um pagamento que ninguem confirma ate actionExpiresAt (24 horas) tambem deixa de aceitar o cartao. O status final e canceled: nada chegou ao adquirente, entao nao ha autorizacao a desfazer.

curl -X POST https://api.minstrel.com.br/v1/payments/pay_01M5.../void \
  -H "Authorization: Bearer $MINSTREL_API_KEY" \
  -H "Idempotency-Key: cancela-pedido-1234" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Pedido cancelado antes do pagamento" }'

Erros e como tratar

confirmCardPayment e retrievePayment lancam MinstrelError. O campo type diz onde a falha aconteceu e, com isso, o que fazer. Alem dele o erro traz code, message (para o desenvolvedor: nunca carrega dado de cartao) e, quando cabe, fields, requestId, httpStatus e providerCode:

typeO que aconteceuO que fazer
validationA pagina esta mal montada (campo ausente, fora do documento ou com name) ou o adquirente apontou campos invalidos. Em fields vem cada campo e o motivo. Nada foi confirmado.Mostre o motivo ao lado de cada campo e deixe o portador corrigir. O pagamento segue requires_payment_method.
tokenizationO adquirente nao aceitou os dados, nao respondeu a tempo ou o script nao carregou. providerCode traz o codigo dele.Deixe o portador tentar de novo. Cada tentativa pede uma sessao nova, no mesmo pagamento. Se persistir, informe o code.
apiO Minstrel respondeu com erro. code, httpStatus e requestId dizem qual (tabela abaixo).Trate pelo code. Pagamento vencido, cancelado ou ja confirmado pede um pagamento novo.
networkSem resposta do Minstrel (queda, timeout). Nao se sabe se a confirmacao chegou.Chame retrievePayment antes de pedir ao portador para tentar de novo, para nao cobrar duas vezes.

Codigos api deste fluxo (o catalogo completo esta em Erros):

codeQuando
client_secret_invalidO clientSecret nao confere com o pagamento (ou o pagamento nao existe: a resposta e a mesma).
payment_not_confirmableO pagamento nao esta em requires_payment_method (ja confirmado, recusado ou cancelado).
payment_expiredO prazo de 24 horas terminou.
no_tokenization_accountNenhuma conta habilitada tokeniza no navegador, ou a informada em tokenizationAccountId nao serve.
tokenization_account_ambiguousHa mais de uma conta que tokeniza: informe tokenizationAccountId ao criar o pagamento.

Tres pontos de atencao:

  • Recusa nao e excecao. Quando o emissor recusa, confirmCardPayment devolve o pagamento com status: "failed". O motivo vem em decline, com uma consumerMessage pensada para mostrar ao portador (as regras de recusa valem como nos demais pagamentos). Trate esse caso no fluxo normal, como no exemplo acima.
  • Recusa e nova tentativa. Nesta versao a recusa encerra o pagamento: failed e final. Para o portador tentar outro cartao, o seu servidor cria um pagamento novo (passo 1) e a pagina recebe um novo paymentId e clientSecret. Tentar de novo no mesmo pagamento esta nos planos.
  • O navegador nao e a fonte da verdade. O portador pode fechar a aba antes de ver o resultado. Libere o pedido pelo evento payment.captured (ou pela consulta GET /v1/payments/:id no servidor), nunca pelo que a pagina disse.

Sandbox

No sandbox use a conta do plugin Minstrel Sandbox (provedor minstrel_sandbox). A pagina e o SDK funcionam como em producao, com uma diferenca: nao ha script de adquirente para carregar, a sessao de tokenizacao nao traz credencial e o SDK fabrica o token sem tocar nos campos (que continuam precisando existir e estar marcados por cardFields). Nenhum cartao real e consultado, e o SDK recusa esse provedor num ambiente de producao.

O resultado da cobranca e escolhido por sandboxScenario, um parametro de confirmCardPayment (so tem efeito no sandbox):

const payment = await minstrel.confirmCardPayment({
  paymentId,
  clientSecret,
  card,
  sandboxScenario: 'decline_insufficient_funds',
});
sandboxScenarioResultado
approveAprova sempre, ignorando o comportamento configurado na conta.
requires_actionPede autenticacao do portador (requires_action).
decline_insufficient_fundsRecusa 51, saldo insuficiente (reversivel).
decline_lost_cardRecusa 41, cartao perdido (irreversivel).
decline_not_permittedRecusa 57, transacao nao permitida para o cartao.
decline_issuer_unavailableRecusa 91, emissor fora do ar (reversivel).
decline_recurring_suspendedRecusa R0, suspensao de recorrencia (cancela a assinatura).

Sem sandboxScenario vale o comportamento da conta Minstrel Sandbox e os atalhos por valor de Primeiros passos (R$ 100,51 recusa por saldo insuficiente, por exemplo). Com sandboxCard voce escolhe a bandeira, o BIN e o final exibidos no painel. Assim voce testa o ramo aprovado, cada recusa e o tratamento de erro da sua pagina sem contratar nada. Tire o sandboxScenario do codigo que vai para producao.

PCI: o que fica com cada um

O Minstrel nunca recebe o numero do cartao nesse fluxo. A pagina, porem, e sua, e e nela que o portador digita o numero - por isso o seu escopo de PCI DSS existe, e costuma ser o SAQ A-EP (comercio que serve a pagina de pagamento e a deixa carregar o script do adquirente). Em troca, voce tem controle total do visual e da experiencia. Se prefere escopo menor, use o link de pagamento, em que o cartao e digitado na pagina do parceiro.

ResponsabilidadeDe quem
Servir a pagina de pagamento so por HTTPS e mante-la livre de scripts desnecessariosLojista
Nao colocar name nos campos, nao copiar o valor para estado, log ou analyticsLojista
Mascarar os campos do cartao em ferramentas de gravacao de sessao e de mapa de calorLojista
Politica de seguranca de conteudo (CSP) liberando so o Minstrel e o dominio do script do adquirente (na Cielo, pagador.com.br)Lojista
Guardar clientSecret apenas pelo tempo do checkout e nunca em logLojista
Recusar numero de cartao e CVV na API (422 raw_card_data_rejected) e nunca gravar nem logarMinstrel
Receber e guardar so token, bandeira, BIN e ultimos 4 digitosMinstrel
Guardar o cartao no cofre do adquirente (PCI DSS dele)Adquirente

Se o numero do cartao chegar mesmo assim a API, ela recusa antes de qualquer validacao ou gravacao - veja Primeiros passos.

Requisitos na Cielo

Para tokenizar no navegador com a Cielo, a conta de conector precisa de:

  • Credenciais do Silent Order Post: sopClientId e sopClientSecret, dois campos opcionais da conta Cielo em Plugins. Sao gerados no site da Cielo, em Gestao API E-commerce, Microsservicos, Silent Order Post. Sem elas, a conta continua autorizando normalmente, mas nao tokeniza no navegador;
  • Cartao Protegido habilitado na loja (o cofre da Cielo), pedido ao suporte da Cielo. E o que permite guardar o cartao: sem ele, saveCard nao devolve o token permanente. Veja Conectores;
  • Credito e sem autenticacao nesta versao. Debito, 3DS e Data Only (Cielo) entram na proxima etapa. Enquanto isso, o roteamento e o fluxo seguem como nos demais cartoes.

O AccessToken do Silent Order Post e de uso unico e vale cerca de 20 minutos; o Minstrel gera um novo a cada sessao de tokenizacao. Voce nao lida com ele.

Proximos passos