@autra-io/card-sdk-web
v0.1.0
Published
Web SDK for Autra's hosted card page: tokenization and subscription card enrollment without the card data ever touching your site.
Readme
@autra-io/card-sdk-web
SDK web da página hospedada de cartão da Autra. Seu site abre a página da Autra (modal com iframe, popup ou redirect), o cliente digita o cartão lá, e o site recebe só o resultado: final do cartão, bandeira, os tokens do cartão salvo e, em assinaturas, a adesão aprovada. Número, validade e CVV nunca passam pelo seu site nem pelo seu backend.
Sumário
- Recursos
- Requisitos
- Instalação
- Como funciona
- Passo 0: cadastre a origem do seu site
- Fluxo completo de assinatura
- Só salvar o cartão (TOKENIZE)
- Modos de abertura
- Referência da API
- Erros
- Segurança e PCI
- Solução de problemas
- Licença
Recursos
- Três modos:
modal(iframe sobre a página),popuperedirect - API híbrida:
Promisepara o resultado final, eventos para os estados intermediários (ready,submitting,enrollment_denied,closed) - Canal
postMessageendurecido:targetOriginexato, validação deevent.origineevent.source, envelope versionado, dedupe e trava depois do resultado final - Erros tipados (
AutraCardError) com 12 códigos, os mesmos da página - ESM + CJS +
.d.ts,sideEffects: false, sem dependências de runtime - Bundle ESM abaixo de 15 KB minificado+gzip (verificado no CI)
Requisitos
- Tenant com a feature
card_sdk_web(esubscriptions, para assinaturas). Peça a habilitação à Autra. - A origem do seu site cadastrada e ativa (veja o passo 0).
- Um backend com as suas credenciais OAuth2 da Autra, com o IP na allow-list do tenant. Sessões de cartão são criadas só pelo backend.
- Um navegador moderno. Node.js >= 20 apenas para construir este pacote.
Instalação
npm install @autra-io/card-sdk-webComo funciona
Seu backend Seu site + SDK Página da Autra
| | |
|-- POST /card-sessions ------->| (Autra devolve hostedCardUrl) |
|--- hostedCardUrl ------------>| |
| |--- card.open() (iframe) ----->|
| |------- host_hello ----------->|
| |<--------- ready --------------|
| |<------- submitting -----------| cliente digita o cartão
| |<-------- result --------------| final, bandeira, tokens
|<== webhook subscription.activated / acquiring.card_enrollment.completed ==|O site nunca vê o cartão. O resultado no navegador serve para a interface;
a confirmação de verdade é o webhook (ou o GET) no seu backend.
Passo 0: cadastre a origem do seu site
A página só aceita ser embutida (e só conversa) com as origens que o tenant cadastrou, por ambiente. O cadastro é feito no painel da Autra, que chama:
POST /v1/tenants/card-sdk/origins
Authorization: Bearer <token do painel>
Content-Type: application/json
{ "origin": "https://loja.exemplo.com.br", "environment": "production" }- Formato exato:
https://<host>[:porta], minúsculas, sem caminho e sem barra final. Fora disso a API recusa (422); ela nunca corrige o valor. GET /v1/tenants/card-sdk/originslista;DELETE .../{id}revoga (a revogação vale na hora, inclusive para sessões já abertas).- Cadastre uma origem por site/subdomínio e por ambiente (
sandboxeproduction).
Fluxo completo de assinatura
Todas as chamadas /v1/acquiring/* são do seu backend
(https://api.autra.io, Authorization: Bearer <access token>, header
X-Merchant-Document com o CPF/CNPJ do estabelecimento).
1. Backend: crie o plano (uma vez)
POST /v1/acquiring/subscription-plans
X-Merchant-Document: 60116920000100
{
"name": "Plano Mensal Ouro",
"amount": 49.9,
"periodicity": "MONTHLY",
"allowedMethods": ["CARD"],
"enrollmentCharge": "FIRST_DUE_DATE",
"firstDue": { "rule": "FIXED_DAY", "billingDay": 10 }
}2. Backend: crie a assinatura do cliente
POST /v1/acquiring/subscriptions
X-Merchant-Document: 60116920000100
{
"planId": "2814b0fd-25b7-476c-affe-34f30fe46d6d",
"externalRef": "cliente-123-plano-ouro",
"customer": { "name": "Maria da Silva", "document": "12345678909", "email": "[email protected]" }
}A assinatura nasce PENDING_ENROLLMENT. Guarde o id.
3. Backend: abra a sessão de cartão de adesão
POST /v1/acquiring/card-sessions
{
"documentId": "60116920000100",
"purpose": "ENROLLMENT",
"channel": "web",
"origin": "https://loja.exemplo.com.br",
"subscriptionId": "0b3c9f7e-2a1d-4c6e-9f10-6a2b3c4d5e6f"
}Resposta (resumida):
{
"hostedCardSessionId": "03294670-67f0-4dca-a121-cf0e0fb2a92f",
"hostedCardUrl": "https://cards.autra.io/tokenize/sessions/03294670-67f0-4dca-a121-cf0e0fb2a92f#token=...",
"expiresAt": "2026-10-01T13:15:00Z",
"purpose": "ENROLLMENT",
"channel": "web",
"embedModes": ["iframe", "popup", "redirect"],
"allowedOrigin": "https://loja.exemplo.com.br",
"subscriptionId": "0b3c9f7e-2a1d-4c6e-9f10-6a2b3c4d5e6f"
}Devolva ao front só hostedCardUrl (e, se quiser, embedModes e
hostedCardSessionId). A sessão vale 15 minutos e é de uso único. Nunca
registre a hostedCardUrl em log: o fragmento #token= é a credencial da
página.
A adesão faz uma validação de valor zero (não cobra) e salva o cartão.
4. Front: abra a página
import { AutraCard, AutraCardError } from '@autra-io/card-sdk-web';
button.addEventListener('click', async () => {
const { hostedCardUrl } = await fetch('/api/assinatura/cartao', { method: 'POST' }).then((r) =>
r.json(),
);
const card = new AutraCard({ hostedCardUrl, mode: 'modal' });
card.on('ready', () => hideSpinner());
card.on('submitting', () => showMessage('Validando o cartão…'));
card.on('enrollment_denied', ({ reasonText }) => {
// Não é o fim: a página mostra o motivo e o cliente pode tentar outro cartão.
track('card_denied', { reasonText });
});
try {
const result = await card.open();
// { purpose: 'ENROLLMENT', card: { last4, brand }, token: { slugStoredCard },
// enrollment: { status: 'APPROVED', subscriptionId, paymentId, brandReferenceId } }
showMessage(`Assinatura ativada no cartão final ${result.card.last4}`);
} catch (error) {
if (error instanceof AutraCardError && error.indeterminate) {
// O cartão já tinha sido enviado: pode ter sido aprovado. Vale o webhook.
return showMessage('Estamos confirmando o seu cartão…');
}
if (error instanceof AutraCardError && error.code === 'canceled') return;
showError(error instanceof AutraCardError ? error.code : 'unknown_error');
}
});Buscar a sessão dentro do clique, como acima, funciona para
modaleredirect. No modopopupowindow.open()precisa acontecer no gesto do usuário semawaitantes: crie a sessão antes do clique (ela vale 15 minutos) e chamecard.open()direto no handler.
5. Backend: confirme pelo webhook
Com a adesão aprovada, a assinatura vira ACTIVE e a Autra emite
subscription.activated (domínio subscriptions). A adesão em si também
gera acquiring.card_enrollment.completed (ou .failed) no domínio
acquiring. Libere o serviço a partir do webhook ou de
GET /v1/acquiring/subscriptions/{id}, não do resultado do navegador.
Daí em diante a Autra cobra sozinha: gera a fatura 3 dias antes de cada
vencimento e cobra o cartão salvo, sem CVV (webhooks invoice.* e
subscription.*).
Só salvar o cartão (TOKENIZE)
Para salvar um cartão sem assinatura (ex.: carteira do cliente, one-click),
crie a sessão com purpose: "TOKENIZE" (o padrão) e channel: "web":
POST /v1/acquiring/card-sessions
{ "documentId": "60116920000100", "purpose": "TOKENIZE", "channel": "web", "origin": "https://loja.exemplo.com.br" }const card = new AutraCard({ hostedCardUrl, mode: 'modal' });
const { card: saved, token } = await card.open();
// saved = { last4: '1111', first4: '4111', brand: 'VISA' }
// token = { slugToken, slugStoredCard, tokenExpirationDate }
await fetch('/api/cartoes', { method: 'POST', body: JSON.stringify(token) });Seu backend usa slugToken/slugStoredCard em tokenData nas rotas de
pagamento. Trate-os como dados sensíveis: só no backend, nunca em log.
Modos de abertura
| mode | Quando usar | embedModes da sessão | Observações |
| ---------- | ---------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------- |
| modal | Padrão. Overlay com iframe sobre o seu site | iframe | Exige a origem cadastrada e ativa; uma modal por vez |
| popup | Quando o site não pode ter iframe | popup | open() dentro do clique, sem await antes; senão popup_blocked |
| redirect | Mobile web/webviews onde popup e iframe falham | redirect | returnUrl obrigatório, na mesma origem cadastrada; a Promise fica pendente (a aba navega) |
Escolha um modo presente em embedModes. A API chama de iframe o que o
SDK chama de modal.
Retorno do modo redirect
A página devolve o cliente para returnUrl com o resultado no fragmento
(#autra_card_status=result&autra_card=...). Na página de retorno:
import { AutraCard } from '@autra-io/card-sdk-web';
const outcome = AutraCard.readRedirectResult(location.hash, { sessionId: sessionIdDoBackend });
if (outcome?.type === 'result') {
showMessage(`Cartão final ${outcome.result.card.last4} cadastrado`);
} else if (outcome?.type === 'error') {
showError(outcome.error.code);
}No modo redirect o resultado não traz token (slugToken/slugStoredCard):
o fragmento fica no histórico do navegador do cliente, então só vão purpose,
card (final e bandeira) e enrollment. Seu backend recebe o cartão salvo e a
assinatura ativada pelos webhooks acquiring.card_enrollment.completed e
subscription.activated.
sessionId é obrigatório: passe o hostedCardSessionId que o seu
backend criou para este cliente (guarde-o na sessão do seu site antes do
redirect). Sem ele a função devolve null e avisa no console fora de
produção; fragmento de outra sessão vira message_invalid. Com
undefined no lugar do hash, lê window.location.hash e limpa o fragmento
da barra de endereço.
O fragmento é forjável. Qualquer pessoa monta um link com
#autra_card_status=result&.... Use o retorno só para a interface; liberar serviço, marcar pagamento ou ativar conta só pelo webhook (subscription.activated,acquiring.card_enrollment.completed) ou peloGETno backend.
Referência da API
new AutraCard(options)
| Opção | Tipo | Obrigatório | Notas |
| --------------- | -------------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
| hostedCardUrl | string | sim | A hostedCardUrl devolvida pelo seu backend, sem alteração. HTTPS |
| mode | "modal" | "popup" | "redirect" | sim | Veja Modos de abertura |
| returnUrl | string | em redirect | HTTPS, na origem cadastrada |
| timeoutMs | number | não | Teto local do fluxo. Padrão 900000 (15 min, o TTL da sessão) |
| allowedHosts | string[] | não | Hosts aceitos na hostedCardUrl. Padrão ['cards.autra.io', 'cards.sandbox.autra.io'] |
Só valida a entrada, de forma síncrona: lança AutraCardError com
configuration_error para entrada inválida (inclusive hostedCardUrl fora
de allowedHosts). Não mexe no DOM nem na rede. http:// só é aceito para
localhost/127.0.0.1 e ainda exige o host em allowedHosts.
card.on(evento, callback) / card.off(evento, callback)
| Evento | Payload | Quando |
| ------------------- | ------------------------------ | --------------------------------------------------------------------- |
| ready | { purpose?, embedModes } | A página carregou e está pronta |
| submitting | undefined | O cliente enviou o cartão |
| enrollment_denied | { reasonCode?, reasonText? } | O emissor recusou a adesão. Não é final: pode vir mais de uma vez |
| closed | undefined | A modal/popup foi fechada (qualquer desfecho) |
on() devolve uma função que cancela a inscrição.
card.open(): Promise<CardResult>
Abre a página e resolve com o resultado:
type CardResult = {
purpose: 'TOKENIZE' | 'ENROLLMENT';
card: { last4: string; first4?: string; brand?: string; nickname?: string };
token?: { slugToken?: string; slugStoredCard?: string; tokenExpirationDate?: string };
enrollment?: {
status: 'APPROVED';
paymentId?: string;
brandReferenceId?: string;
subscriptionId?: string;
};
};Ou rejeita com AutraCardError. Uso único: a segunda chamada rejeita com
configuration_error (crie outra sessão). Em ENROLLMENT, o resultado
sempre traz enrollment.status === 'APPROVED'.
card.close()
Fecha a página (avisa a página com host_cancel). Sem efeito se nada
estiver aberto.
- Antes de
submitting: fecha na hora e oopen()rejeita comcanceled(indeterminate: false). - Depois de
submitting(cartão a caminho do core): esconde a modal e espera até 10 s pelo desfecho da página. Se chegarresultouerror, oopen()entrega esse desfecho; senão rejeita comcanceledeindeterminate: true, porque o core pode ter aprovado (ou cobrado) o cartão. Nesse caso vale o webhook.
AutraCard.readRedirectResult(hash, { sessionId })
Lê o resultado do modo redirect. sessionId é obrigatório. Devolve
{ type: 'result', sessionId, result }, { type: 'error', sessionId?, error }
ou null (carregamento normal, sem resultado, ou sem sessionId). Nunca
lança. Também exportada como readRedirectResult. O resultado é forjável:
só para a interface.
AutraCardError
class AutraCardError extends Error {
readonly code: AutraCardErrorCode;
readonly retryable: boolean; // criar uma nova sessão e tentar de novo faz sentido?
readonly sessionId?: string;
readonly reasonCode?: string; // só em enrollment_denied
readonly reasonText?: string;
readonly indeterminate: boolean; // true: o cartão já tinha sido enviado; confirme pelo webhook
}AUTRA_CARD_SDK_VERSION
Versão publicada do pacote (também enviada à página no host_hello).
Erros
| code | Origem | O que fazer |
| --------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| configuration_error | SDK | Entrada inválida (URL não HTTPS, mode, returnUrl, timeoutMs), open() repetido ou outra modal aberta |
| session_expired | Página (410 HOSTED_CARD_SESSION_EXPIRED) | A sessão passou de 15 min ou já foi usada: crie outra. No modal/popup costuma aparecer como timeout (veja abaixo) |
| origin_not_allowed | Página | Origem não cadastrada/ativa: veja o passo 0. No modal/popup aparece como timeout |
| message_invalid | SDK | Mensagem fora do contrato (versão, formato). Atualize o SDK |
| validation_error | Página | Dados do cartão inválidos e o cliente desistiu |
| tokenization_failed | Página | Falha ao salvar o cartão: crie outra sessão e tente de novo |
| enrollment_denied | Página (422 CARD_ENROLLMENT_DENIED) | O emissor recusou e o cliente desistiu. reasonCode/reasonText trazem o motivo |
| enrollment_pending_review | Página (503 CARD_ENROLLMENT_PENDING_REVIEW) | Resultado incerto: não abra outra adesão; aguarde o webhook |
| provider_unavailable | Página (503 HOSTED_CARD_PROVIDER_UNAVAILABLE) | Instabilidade transitória: tente de novo em instantes |
| canceled | Cliente ou close() | O cliente fechou a página/popup ou o site chamou close(). Com indeterminate: true (depois de submitting), o cartão pode ter sido aprovado: vale o webhook |
| timeout | SDK | A página não respondeu ready em 15 s (sessão expirada/inválida, origem não cadastrada, página fora do ar) ou o fluxo passou de timeoutMs. Com indeterminate: true, vale o webhook |
| popup_blocked | SDK | O navegador bloqueou o popup: chame open() direto no clique, ou use modal |
Segurança e PCI
- O site nunca vê o cartão. Número, validade e CVV são digitados na página da Autra, em outra origem; o SDK só recebe final (4 dígitos), bandeira e tokens. Isso tira o seu site do escopo de dados de cartão do PCI DSS (a Autra continua responsável pela página).
- O SDK só aceita
hostedCardUrlHTTPS, num host deallowedHosts(padrão: só os da Autra), e deriva a origem da página comnew URL(hostedCardUrl).origin. - Nunca repasse uma
hostedCardUrlvinda do cliente (query string,postMessage, corpo de requisição): use sempre a que o seu backend recebeu dePOST /v1/acquiring/card-sessions. Não amplieallowedHostsem produção. canceled/timeoutcomindeterminate: truenão significam falha: o cartão já tinha sido enviado. Confirme pelo webhook antes de pedir outro cartão ao cliente.- O retorno do modo redirect é forjável: use
readRedirectResultsó para a interface. - Todo
postMessageusa essa origem exata comotargetOrigin, nunca'*'. Toda mensagem recebida é validada porevent.origineevent.source(a própria janela do iframe/popup), por versão do envelope e porcorrelationId; depois do resultado final, nada mais é aceito. - Os campos do resultado são copiados de uma lista fechada;
last4/first4só passam com exatamente 4 dígitos. - O page token vai no fragmento da
hostedCardUrl; o SDK não lê, não guarda e não registra. Não logue ahostedCardUrlno seu backend nem no front. - Nenhuma credencial da API da Autra chega ao navegador: sessões são criadas só pelo seu backend.
- Veja
SECURITY.md.
Solução de problemas
| Sintoma | Causa provável |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 403 CARD_SDK_WEB_DISABLED ao criar a sessão | O tenant não tem a feature card_sdk_web |
| 422 CARD_SDK_ORIGIN_NOT_REGISTERED / _INVALID | origin não cadastrada/ativa no ambiente, ou fora do formato https://host[:porta] (sem barra final) |
| 409 SUBSCRIPTION_NOT_AWAITING_ENROLLMENT | A assinatura já está ativa/cancelada ou o plano não aceita CARD |
| Modal em branco e depois timeout | A página se recusou a ser embutida: a origem do site (esquema, host e porta) não bate com a cadastrada na sessão |
| timeout no modal/popup sem motivo aparente | Sessão expirada/inválida ou origem não cadastrada: sem origem confiável, a página não tem canal para avisar o SDK e mostra o erro só na tela. Crie uma sessão nova e confira a origem |
| Popup abre mas o fluxo termina em canceled/timeout | O seu site manda Cross-Origin-Opener-Policy: same-origin, que corta o window.opener. Use same-origin-allow-popups, ou os modos modal/redirect |
| configuration_error: host "…" is not allowed | hostedCardUrl fora de allowedHosts: use a URL devolvida pelo seu backend, sem alteração |
| Testar em http://localhost | A página real exige origens HTTPS (cadastro e frame-ancestors): exponha o seu site local por um túnel HTTPS (ex.: ngrok, Cloudflare Tunnel) e cadastre essa origem no ambiente sandbox |
| popup_blocked | Houve await entre o clique e card.open(); crie a sessão antes do clique |
| open() nunca resolve no modo redirect | Esperado: a aba navega; leia o resultado na returnUrl com readRedirectResult() |
| session_expired logo ao abrir | Sessão reaproveitada ou criada há mais de 15 min: crie uma nova a cada abertura |
| configuration_error no construtor | hostedCardUrl alterada, sem HTTPS ou sem o id da sessão no caminho |
