Cartões - Tokenização no navegador
Visão Geral
Duas formas de aceitar cartão na sua própria página sem que o número passe pelo seu servidor:
| Modelo | Por onde passa o número do cartão |
|---|---|
| Checkout hospedado (Sessions) | só a página da Selectwin |
| Selectwin Elements (esta página, §A) — os campos do cartão num iframe da Selectwin dentro da sua página | só o iframe da Selectwin |
| Integração direta (§B) — o seu JavaScript envia o cartão à Selectwin | o navegador (o seu JS) |
Cartão completo pela API (Criar Cartão com numbering) | o seu servidor |
Nos dois modelos desta página a Selectwin tokeniza o cartão, e o que chega ao seu servidor, e depois à nossa API, é um token de uso único (uma hora).
A. Selectwin Elements — recomendado
É o modelo do Stripe Elements: você carrega um script da Selectwin, monta um elemento de cartão num div
seu, e os campos (número, nome, validade, CVV) aparecem dentro de um iframe servido pela Selectwin. A sua
página não tem o cartão no DOM; o createToken devolve o token e os dados mascarados.
<div id="card-element"></div>
<button id="pay">Pagar</button>
<script src="https://pay.selectwin.io/elements/v1/elements.js"></script>
<script>
var selectwin = Selectwin('pk_test_…', { locale: 'pt-BR' });
var card = selectwin.elements().create('card', {
style: { base: { color: '#1f2933', fontSize: '16px', fontFamily: 'Inter, sans-serif' }, invalid: { color: '#dc2626' } }
});
card.mount('#card-element');
var complete = false;
card.on('change', function (ev) { complete = ev.complete; /* ev.brand, ev.error, ev.empty */ });
document.getElementById('pay').addEventListener('click', function () {
if (!complete) return;
selectwin.createToken(card).then(function (r) {
if (r.error) { alert(r.error.message); return; } // já em pt-BR
// r.token → envie ao seu servidor; r.card = { brand, brandType, firstDigits, lastDigits, expirationMonth, expirationYear, holderName }
});
});
</script>| Chamada | O que faz |
|---|---|
Selectwin(pk, { locale }) | Cria a instância com a sua chave publicável. locale: pt-BR (padrão), en, es |
elements().create('card', { style, placeholders, title }) | O elemento de cartão. style.base aceita color, fontFamily, fontSize, fontWeight, backgroundColor, borderColor, borderRadius, focusBorderColor, '::placeholder': { color }; style.invalid.color |
card.mount(seletor | elemento) · unmount() · destroy() | Coloca/retira o iframe. O iframe ajusta a própria altura |
card.on('ready' | 'change' | 'focus' | 'blur', fn) | change traz { complete, empty, brand, error: { code, message } } |
card.focus() · card.clear() · card.update({ style }) | |
selectwin.createToken(card) | Promessa que nunca rejeita: { token, card } ou { error: { code, message } } |
Se a sua página tiver CSP, permita frame-src https://pay.selectwin.io; script-src https://pay.selectwin.io.
Nada mais: é o iframe que fala com a API.
O token entra no seu servidor como qualquer campo do formulário e vai à API em payment.card.token
(Criar Transação), em Criar Assinatura ou em
POST /v1/cards { customerId, token } (Criar Cartão). A Selectwin associa o token ao
cliente na hora: o cartão já pode ser cobrado. Como foi o comprador quem digitou o cartão, a API
devolve associated: null até a 1ª cobrança aprovada dele (o mesmo que acontece com o cartão do checkout hospedado).
B. Integração direta
Para quem não pode usar um iframe (apps nativos, WebViews restritas): o seu próprio código envia o cartão à Selectwin, que o tokeniza. O número passa pelo seu JavaScript, mas nunca pelo seu servidor.
navegador ──(1) GET /v1/tokenization/config?pk=pk_…──▶ Selectwin
navegador ◀── onde enviar o cartão + nomes dos campos ───┘
navegador ──(2) POST /v1/tokenization/tokens?pk=pk_… {holderName, numbering, …}──▶ Selectwin
navegador ◀── { token: "…" } ──────────────────────────────────────────────────────┘
navegador ──(3) token ──▶ seu servidor ──(4) POST /v1/transactions { payment.card.token } ──▶ SelectwinB.1 Configuração de tokenização
GET /v1/tokenization/config?pk=pk_test_…Sem autenticação além da chave publicável (ou, do servidor, a SelectKey sem pk). A resposta pode ser lida
de qualquer origem (Access-Control-Allow-Origin: *) e não deve ser guardada em cache.
{
"environment": "sandbox",
"publicKey": "pk_test_…",
"tokenization": {
"method": "POST",
"url": "https://api.selectwin.io/v1/tokenization/tokens?pk=pk_test_…",
"headers": { "Content-Type": "application/json", "Accept": "application/json" },
"fields": {
"holderName": "holderName",
"number": "numbering",
"expirationMonth": "expirationMonth",
"expirationYear": "expirationYear",
"securityCode": "securityCode"
},
"tokenField": "token",
"ttlSeconds": 3600
},
"acceptedBrands": ["visa", "mastercard", "elo", "amex", "hipercard"],
"elements": {
"scriptUrl": "https://pay.selectwin.io/elements/v1/elements.js",
"fieldsUrl": "https://pay.selectwin.io/elements/v1/fields?pk=pk_test_…"
}
}| Campo | Descrição |
|---|---|
environment | O namespace da chave: sandbox gera tokens de teste; live, de produção |
tokenization.url / headers | Para onde e com que cabeçalhos o navegador envia o cartão. Envie exatamente como veio |
elements.scriptUrl / elements.fieldsUrl | O SDK e o iframe do Selectwin Elements (§A) |
tokenization.fields | Nome lógico → nome do campo no corpo. Mês com 2 dígitos, ano com 4 |
tokenization.tokenField | Onde está o token na resposta (token) |
tokenization.ttlSeconds | Validade do token (3600 s) |
acceptedBrands | Bandeiras que a sua conta aceita — recuse na página antes de enviar |
Erros
| HTTP | code | Quando |
|---|---|---|
| 401 | publicKeyInvalid | pk ausente, desconhecida, revogada ou expirada (a chave de API revogada leva a publicável junto) |
| 422 | publicKeyMissing | A SelectKey usada é antiga e não tem chave publicável — crie uma chave de API nova |
| 503 | tokenizationNotConfigured | A tokenização no navegador não está disponível no ambiente |
B.2 No navegador
<script>
async function tokenizeCard(pk, card) {
const cfg = await fetch(`https://api.selectwin.io/v1/tokenization/config?pk=${encodeURIComponent(pk)}`)
.then((r) => r.json());
const f = cfg.tokenization.fields;
const body = {
[f.holderName]: card.holderName,
[f.number]: card.number.replace(/\D/g, ''),
[f.expirationMonth]: String(card.expirationMonth).padStart(2, '0'),
[f.expirationYear]: String(card.expirationYear).length === 2 ? `20${card.expirationYear}` : String(card.expirationYear),
[f.securityCode]: card.securityCode,
};
const res = await fetch(cfg.tokenization.url, {
method: cfg.tokenization.method,
headers: cfg.tokenization.headers,
body: JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json?.error?.details || json?.error?.message || 'card refused');
return json[cfg.tokenization.tokenField]; // o token — envie ao seu servidor
}
</script>POST /v1/tokenization/tokens?pk=pk_test_…| Campo | Tipo | Descrição |
|---|---|---|
holderName | string | Nome impresso no cartão (3 a 255) |
numbering | string | Número do cartão (validado por Luhn) |
expirationMonth | integer | 1 a 12 |
expirationYear | integer | 4 dígitos (ou 2 → 20xx). Não pode estar no passado |
securityCode | string | CVV (3 ou 4 dígitos) |
Resposta 201:
{
"token": "a1b2c3d4…",
"ttlSeconds": 3600,
"card": { "brand": "Visa", "brandType": "visa", "firstDigits": "4111", "lastDigits": "1111",
"expirationMonth": "12", "expirationYear": "2030", "holderName": "Ana Lima" }
}| HTTP | code | Quando |
|---|---|---|
| 400 | (validação) · cardBrandNotSupported | Campo inválido, Luhn, cartão vencido; bandeira que a sua conta não aceita |
| 401 | publicKeyInvalid | pk desconhecida, revogada ou expirada |
| 422 | invalidCardNumber · invalidSecurityCode · invalidExpiryMonth · invalidExpiryYear · expiredCardError · invalidPaymentInformation | O cartão foi recusado na tokenização |
| 429 | tokenizationRateLimited | Teto por minuto: 15 por IP do comprador (na borda) e 120 por chave publicável |
| 503 | cardTokenizationUnavailable | A tokenização não respondeu a tempo. Tente de novo |
Regras que valem a pena seguir na página:
- Nunca envie
numbering/securityCodeao seu servidor — só o token. - Valide o Luhn e a bandeira (
acceptedBrands) antes de enviar: uma recusa local é mais rápida. - Guarde a configuração só pelo tempo da página; peça de novo a cada carregamento (
Cache-Control: no-store). - Com formulário público, considere um desafio anti-bot (o plugin do WooCommerce integra o Cloudflare Turnstile) para que a sua página não vire ferramenta de teste de cartões — além do teto por chave que a API aplica.
No seu servidor (A e B)
O token entra onde o cartão completo entraria:
POST /v1/transactions
{
"amount": 15990,
"payment": {
"method": "credit",
"installments": 3,
"card": { "token": "a1b2c3…" }
},
"customer": { "id": "cus_…" }
}POST /v1/cards
{ "customerId": "cus_…", "token": "a1b2c3…", "primary": true }A resposta é o cartão de sempre (Criar Cartão), com firstDigits de 4 dígitos e associated: null até a 1ª cobrança aprovada (o cartão foi
digitado pelo comprador; ele já pode ser cobrado).
Erros do token (além dos erros de cartão de Criar Cartão):
| HTTP | code | Quando |
|---|---|---|
| 422 | cardTokenInvalid | Token desconhecido, expirado (1 h) ou já usado. Gere outro no navegador |
| 422 | cardDeclined | O cartão foi recusado na validação (zero dollar) |
| 422 | cardTokenCustomerUnavailable | O cliente não pôde ser cadastrado para receber o cartão (dados incompletos/recusados) |
| 503 | cardTokenAssociationFailed | A validação do cartão não respondeu a tempo. Nada foi cobrado; tente de novo com um token novo |
| 503 | tokenizationNotConfigured | Ambiente sem tokenização no navegador |
O cadastro por token não responde 429: o token foi cunhado no navegador de um comprador, então conta na janela de
cada comprador (o cliente e o endereço de onde o token foi gerado) e num teto da loja para os cartões dos compradores —
nunca no teto da conta. Acima desse teto, ou com a janela do endereço fechada por outros compradores da mesma rede, o
cartão é criado do mesmo jeito (201, associated: null) e validado logo em seguida, antes da 1ª cobrança. Só fica sem
validar o cartão cujo próprio cliente já esgotou a janela dele (cartões demais recusados na hora) ou cujo endereço também
esgotou as validações da hora: o token expira em 1 h e a 1ª cobrança com ele é recusada — peça o cartão de novo.
Sandbox
Com uma pk_test_… a tokenização acontece no ambiente de testes, e os
cartões de teste valem como sempre. Um token de teste nunca cobra em
produção: a chave de API sk_test_… só enxerga o namespace sandbox.
Atualizado em 17 de outubro de 2026