Cartão: cofre e cifra

Os dados do cartão (número, validade e código de segurança) nunca passam pela API de pagamentos da SwitchPay. Eles vão, já cifrados, direto pro cofre: um serviço separado, que troca o cartão por um código de uso único, o token. É esse token que você manda pra confirmar a cobrança. O cofre não guarda o cartão.

Por que é assim

Quem recebe, guarda ou repassa número de cartão precisa seguir as regras de segurança do setor de cartões (PCI DSS). Quanto mais lugares o número toca, maior a obrigação.

Com o cofre, o número toca o menor número possível de lugares. No jeito recomendado (Hosted Fields), nem a sua página nem o seu servidor veem o cartão: a sua parte da obrigação fica no nível mais leve (SAQ A).

O caminho de uma cobrança no cartão

  1. Você cria a intenção (POST /v2/payment-intents com paymentMethod: CREDIT_CARD). Ela volta com status REQUIRES_TOKEN e o bloco cardTokenization.
  2. O cartão vira token no cofre, por um dos dois jeitos abaixo.
  3. Você confirma (POST /v2/payment-intents/{id}/confirm) mandando { "token": "vtk_..." }.
  4. A resposta já traz o resultado: transação PAID (aprovada) ou FAILED (recusada).

O que vem em cardTokenization:

CampoPra que serve
vaultUrlEndereço do cofre. Use sempre o que vier aqui.
ticketAutorização pra tokenizar o cartão desta intenção. Vale 5 minutos e uma vez só.
encryptionKeyChave pública pra cifrar os dados do cartão antes de enviar.

Dois jeitos de tokenizar

JeitoQuem vê o cartãoComo
Hosted Fields (recomendado)só o cofreSua página inclui {vaultUrl}/sdk.js e monta o campo do cartão com ticket + encryptionKey. O campo é um iframe do cofre: o cliente digita lá dentro, o cofre cifra e tokeniza, e sua página recebe só o token. Seu JavaScript e seu servidor nunca veem o número.
Server-sideseu servidor e o cofreSeu servidor já tem o cartão, cifra com encryptionKey (biblioteca pronta ou os passos da rota /tokenize) e chama POST {vaultUrl}/tokenize. O cartão passa pelo seu ambiente: a conformidade PCI desse trecho é sua.

Qual escolher: se o cliente digita o cartão numa página sua, use Hosted Fields. Use server-side só se o cartão já chega ao seu servidor por outro caminho e você já cuida da conformidade.

Hosted Fields em 3 passos

  1. Inclua a biblioteca do cofre na página: <script src="{vaultUrl}/sdk.js"></script>. É o único arquivo que você inclui. O campo do cartão, a cifra e a conferência do que o cliente digita vêm de dentro do cofre, carregados por essa biblioteca.
  2. Monte o campo num elemento da página, passando vaultUrl, ticket e encryptionKey da intenção.
  3. Quando o cliente enviar, a função onToken recebe o token. Mande pro seu servidor, que chama o confirm.

O campo confere o cartão enquanto o cliente digita e aceita ajuste de aparência (fonte, cor, textos de exemplo, idioma). Todas as opções estão na rota Biblioteca do Hosted Fields, na tag Cofre. O exemplo de ponta a ponta vem logo abaixo.

Se a sua página usa política de conteúdo (CSP), libere o endereço do cofre em frame-src e em script-src.

Exemplo completo: do clique ao resultado

Uma cobrança no cartão tem três pedaços de código: o seu servidor cria a intenção, a sua página monta o campo e recebe o token, e o seu servidor confirma. O exemplo usa Node.js no servidor e JavaScript puro na página.

Antes de começar, crie do seu lado um registro pra cada tentativa de pagamento (aqui, attempt), com um id próprio. Esse id serve de referenceId e de base pras duas chaves de idempotência. Tentativa nova (cartão recusado, prazo vencido) = registro novo.

1. O seu servidor cria a intenção e devolve à página só o que ela precisa.

// Seu servidor
app.post('/checkout/card', async (req, res) => {
  // O valor e o comprador saem do SEU pedido, nunca do que a página mandou.
  const attempt = await createAttempt(req.body.orderId);

  const response = await fetch('https://api.sandbox.switchpay.app.br/v2/payment-intents', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SWP_KEY}`,
      'Idempotency-Key': `${attempt.id}-criar`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amountCents: attempt.amountCents,
      paymentMethod: 'CREDIT_CARD',
      installments: 1,
      referenceId: attempt.id,
      buyer: attempt.buyer,
    }),
  });
  const intent = await response.json();

  if (!response.ok) {
    return res.status(response.status).json({ code: intent.code, details: intent.details });
  }

  await saveIntentId(attempt.id, intent.id); // a intenção fica guardada no servidor

  res.status(201).json({ attemptId: attempt.id, cardTokenization: intent.cardTokenization });
});

A intenção volta com status REQUIRES_TOKEN. Não mande confirm: true aqui: com cartão, isso dá 400 VALIDATION_ERROR.

2. A sua página monta o campo do cartão e manda o token pro seu servidor.

<div id="card"></div>
<button id="pay" disabled>Pagar</button>

<!-- No sandbox. Em produção, use o endereço que vier em cardTokenization.vaultUrl. -->
<script src="https://vault.sandbox.switchpay.app.br/sdk.js"></script>
<script>
  const payButton = document.getElementById('pay');
  let field;

  async function start() {
    if (field) field.unmount(); // tentativa nova: tira o campo antigo

    // postJson = a sua função que chama o seu servidor e devolve o JSON
    const { attemptId, cardTokenization } = await postJson('/checkout/card', { orderId });

    field = SwpVault.mount(document.getElementById('card'), {
      vaultUrl: cardTokenization.vaultUrl,
      ticket: cardTokenization.ticket,
      encryptionKey: cardTokenization.encryptionKey,
      hideSubmitButton: true, // o botão de pagar é o seu

      // Libera o seu botão só quando os 4 campos estão prontos.
      onChange: ({ formComplete }) => { payButton.disabled = !formComplete; },

      // O cofre devolveu o token: mande pro seu servidor confirmar.
      onToken: async ({ token }) => {
        const result = await postJson('/checkout/card/confirm', { attemptId, token });

        if (result.status === 'PAID') showApproved();
        else if (result.status === 'FAILED') showDeclined(); // pra tentar de novo: start()
        else showProcessing(); // ainda sem resultado final: espere o aviso (webhook)
      },

      // TICKET_EXPIRED (passaram 5 minutos): chame start() pra começar de novo.
      onError: (error) => showMessage(error.code),
    });
  }

  payButton.onclick = () => field.tokenize();
  start();
</script>

O número do cartão é digitado dentro do campo do cofre. O seu JavaScript recebe só o token.

3. O seu servidor confirma e lê o resultado na própria resposta.

// Seu servidor
app.post('/checkout/card/confirm', async (req, res) => {
  const attempt = await findAttempt(req.body.attemptId);

  const response = await fetch(
    `https://api.sandbox.switchpay.app.br/v2/payment-intents/${attempt.intentId}/confirm`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.SWP_KEY}`,
        'Idempotency-Key': `${attempt.id}-confirmar`, // outra chave: é outra operação
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ token: req.body.token }),
    },
  );
  const transaction = await response.json(); // aqui a resposta já é a transação

  if (!response.ok) {
    return res.status(response.status).json({ code: transaction.code });
  }

  await saveTransaction(attempt.id, transaction.id, transaction.status);

  res.json({ status: transaction.status }); // PAID ou FAILED
});

Repare na diferença entre as duas respostas: criar devolve a intenção (pi_…); confirmar devolve a transação (trx_…), com o resultado em status.

O que fazer com cada resultado do confirm:

O que voltouO que quer dizerO que fazer
201, transação PAIDCartão aprovadoLibere o pedido.
201, transação FAILEDCartão recusado (não é erro HTTP)Mostre a recusa. Pra tentar de novo, crie outra intenção.
410 INTENT_EXPIREDPassaram 5 minutosCrie outra intenção e monte o campo de novo.
422 TOKEN_INVALIDToken inválido, vencido ou de outra intençãoCrie outra intenção e monte o campo de novo.
502 PROVIDER_ERRORA adquirente falhou; a intenção ficou FAILEDCrie outra intenção.
500, 503 ou sem respostaNão dá pra saber se cobrouRepita o confirm com a mesma Idempotency-Key.

Só libere o pedido com base no que o seu servidor gravou. O que a página mostra é só aviso pro cliente.

O que o cofre devolve

{ "token": "vtk_...", "brand": "VISA", "first4": "4111", "last4": "1111", "expiresAt": "2026-10-06T14:05:00.000Z" }
  • token: o código que vai no confirm. Texto longo e opaco: guarde e repasse como veio.
  • brand, first4, last4: servem pra mostrar ao cliente qual cartão foi usado.
  • expiresAt: até quando o token vale.

Prazos e uso único

  • O ticket vale 5 minutos e uma vez. Passou do prazo ou já foi usado: crie outra intenção.
  • O token é de uso único e vale só pra intenção que gerou o ticket.
  • Cartão recusado não é erro HTTP: o confirm responde 201 com a transação FAILED. Pra tentar de novo (com o mesmo cartão ou outro), crie outra intenção.
  • Se o envio ao cofre falhar por dado inválido, o ticket não é gasto: corrija e envie de novo.

Erros mais comuns

CódigoOndeO que quer dizer
TICKET_EXPIRED (401)cofreO ticket passou dos 5 minutos.
TICKET_USED (401)cofreO ticket já foi usado.
TICKET_INVALID (401)cofreO ticket não é válido pra este cofre ou foi alterado.
TOKEN_REQUIRED (422)confirmIntenção de cartão confirmada sem token.
TOKEN_INVALID (422)confirmToken inválido, vencido ou de outra intenção.

No sandbox

O cofre do sandbox abre e confere o cartão cifrado igual ao de produção; só a ida à adquirente é simulada. Cartões de teste:

NúmeroResultado
4111 1111 1111 1111Aprovado (transação PAID)
4000 0000 0000 0002Recusado (transação FAILED)

Veja a tag Cofre pra referência completa das rotas.