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
- Você cria a intenção (
POST /v2/payment-intentscompaymentMethod: CREDIT_CARD). Ela volta com statusREQUIRES_TOKENe o blococardTokenization. - O cartão vira
tokenno cofre, por um dos dois jeitos abaixo. - Você confirma (
POST /v2/payment-intents/{id}/confirm) mandando{ "token": "vtk_..." }. - A resposta já traz o resultado: transação
PAID(aprovada) ouFAILED(recusada).
O que vem em cardTokenization:
| Campo | Pra que serve |
|---|---|
vaultUrl | Endereço do cofre. Use sempre o que vier aqui. |
ticket | Autorização pra tokenizar o cartão desta intenção. Vale 5 minutos e uma vez só. |
encryptionKey | Chave pública pra cifrar os dados do cartão antes de enviar. |
Dois jeitos de tokenizar
| Jeito | Quem vê o cartão | Como |
|---|---|---|
| Hosted Fields (recomendado) | só o cofre | Sua 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-side | seu servidor e o cofre | Seu 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
- 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. - Monte o campo num elemento da página, passando
vaultUrl,ticketeencryptionKeyda intenção. - Quando o cliente enviar, a função
onTokenrecebe otoken. Mande pro seu servidor, que chama oconfirm.
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 voltou | O que quer dizer | O que fazer |
|---|---|---|
201, transação PAID | Cartão aprovado | Libere o pedido. |
201, transação FAILED | Cartão recusado (não é erro HTTP) | Mostre a recusa. Pra tentar de novo, crie outra intenção. |
410 INTENT_EXPIRED | Passaram 5 minutos | Crie outra intenção e monte o campo de novo. |
422 TOKEN_INVALID | Token inválido, vencido ou de outra intenção | Crie outra intenção e monte o campo de novo. |
502 PROVIDER_ERROR | A adquirente falhou; a intenção ficou FAILED | Crie outra intenção. |
500, 503 ou sem resposta | Não dá pra saber se cobrou | Repita 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 noconfirm. 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
ticketvale 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
confirmresponde201com a transaçãoFAILED. 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ódigo | Onde | O que quer dizer |
|---|---|---|
TICKET_EXPIRED (401) | cofre | O ticket passou dos 5 minutos. |
TICKET_USED (401) | cofre | O ticket já foi usado. |
TICKET_INVALID (401) | cofre | O ticket não é válido pra este cofre ou foi alterado. |
TOKEN_REQUIRED (422) | confirm | Intenção de cartão confirmada sem token. |
TOKEN_INVALID (422) | confirm | Token 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úmero | Resultado |
|---|---|
4111 1111 1111 1111 | Aprovado (transação PAID) |
4000 0000 0000 0002 | Recusado (transação FAILED) |
Veja a tag Cofre pra referência completa das rotas.