docs

Tokenização

Como zhex.js Elements transforma PAN/CVV em tok_* sem nunca tocar seu servidor — escopo PCI-DSS SAQ-A, single-use, e o caminho para PaymentMethod reutilizável.

Tokenização é o que separa "alguém que aceita cartão" de "negócio que fala com banco". Sem hosted fields, você cai em PCI-DSS SAQ-D (auditoria pesada, ASV scan trimestral, política de segregação). Com hosted fields, você fica em SAQ-A — o escopo mínimo, ~10 perguntas anuais.

A diferença prática: SAQ-A te livra de quase todo overhead de compliance. É a mesma posição que Stripe, Adyen, Braintree dão aos seus clientes.

A regra de ouro

PAN e CVV nunca podem tocar seu servidor. Eles vão direto do navegador do cliente para prometheus.zhex.io, via um iframe servido por js.zhex.io. Você recebe de volta apenas um Token (tok_*) — uma string opaca, single-use, válida por 15 minutos.

Renderizar Elements no front

O SDK é distribuído como pacote npm. Instale, importe e monte o iframe — não há bundle global via <script> hoje (a versão CDN entra numa minor próxima):

npm install @zhexio/zhex-js
import { Zhex } from '@zhexio/zhex-js';

const zhex = Zhex('zk_live_...');           // publishable key
const elements = zhex.elements({
  appearance: {
    theme: 'dark',
    variables: { colorPrimary: '#5C7DFA' },
  },
});

const card = elements.create('card');
card.mount('#card-element');

document
  .getElementById('payment-form')!
  .addEventListener('submit', async (e) => {
    e.preventDefault();

    const result = await zhex.createToken(card);
    if ('error' in result) {
      alert(result.error.message);
      return;
    }

    // tok_* vai pro seu servidor por fetch normal
    const res = await fetch('/api/charge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: result.token.id }),
    });
    const intent = await res.json();
    // O intent retornado pelo seu /api/charge já tem status atualizado.
    // Em cartão sem 3DS: 'succeeded' direto. Com 3DS, o fluxo é orquestrado
    // pelo checkout hosted da Zhex hoje.
  });

A zk_live_* (publishable) pode aparecer no front — ela só consegue criar Token. Não roda charge, não vê dados sensíveis, não lista clientes.

Customizar a aparência

Todo input e label dentro do iframe é controlado por CSS variables que você passa via appearance.variables. Cada chave em camelCase vira uma variável --zhex-<kebab-case> aplicada no <html> do iframe — então o estilo persiste mesmo durante re-renders e nada do seu CSS externo precisa atravessar a fronteira de origem.

Duas camadas:

  1. Tokens semânticos — afetam todo o iframe (texto, borda, foco, fundo).
  2. Overrides por campo — controle cirúrgico nos inputs quando os tokens são amplos demais.
const elements = zhex.elements({
  appearance: {
    theme: 'light',                // ou 'dark'
    variables: {
      // Tokens semânticos
      fontFamily: '"Inter", system-ui, sans-serif',
      fontSize: '15px',
      colorPrimary: '#f97316',     // foco, brand
      colorText: '#0a0a0a',
      colorMuted: '#a3a3a3',       // placeholders, hints
      colorDanger: '#dc2626',      // borda de erro
      colorBorder: '#e5e5e5',
      colorBackground: '#ffffff',
      borderRadius: '12px',
      // Overrides por campo
      inputBackground: '#fafafa',
      inputBorderColor: '#e5e5e5',
      inputBorderColorFocus: '#f97316',
      inputBorderColorError: '#dc2626',
      inputBorderRadius: '16px',
      inputBorderWidth: '1px',
      inputPaddingX: '16px',
      inputPaddingY: '0px',
      inputHeight: '52px',
      inputFontSize: '15px',
      inputFontWeight: '400',
      inputColor: '#0a0a0a',
      inputPlaceholderColor: '#a3a3a3',
      inputBoxShadow: 'none',
      inputBoxShadowFocus: '0 0 0 4px rgba(249,115,22,0.12)',
      fieldGap: '10px',
      brandGlyphSize: '32px',
    },
  },
});

Tokens semânticos

VariávelCSS var geradaDefaultUso
fontFamily--zhex-font-familysystem-ui stackFamília tipográfica do iframe
fontSize--zhex-font-size15pxTamanho base — input herda quando inputFontSize não vem
colorPrimary--zhex-color-primarytom azul ZhexEstado de foco do input + brand
colorText--zhex-color-textpretoTexto digitado, label
colorMuted--zhex-color-mutedcinza médioPlaceholder, hints
colorDanger--zhex-color-dangervermelhoBorda de erro, mensagem de erro
colorBorder--zhex-color-bordercinza claroBorda do input em estado idle
colorBackground--zhex-color-backgroundbrancoFundo do iframe
borderRadius--zhex-border-radius12pxRaio padrão (input herda quando inputBorderRadius não vem)

Overrides por campo

VariávelCSS var geradaAplicado em
inputBackground--zhex-input-backgroundFundo dos 3 inputs
inputBorderColor--zhex-input-border-colorBorda idle
inputBorderColorFocus--zhex-input-border-color-focusBorda em :focus
inputBorderColorError--zhex-input-border-color-errorBorda quando há erro de validação
inputBorderWidth--zhex-input-border-widthEspessura da borda
inputBorderRadius--zhex-input-border-radiusRaio do input (override de borderRadius)
inputPaddingX--zhex-input-padding-xPadding horizontal
inputPaddingY--zhex-input-padding-yPadding vertical
inputHeight--zhex-input-heightAltura fixa do input
inputFontSize--zhex-input-font-sizeTamanho da fonte digitada
inputFontWeight--zhex-input-font-weightPeso da fonte digitada
inputColor--zhex-input-colorCor do texto digitado
inputPlaceholderColor--zhex-input-placeholder-colorCor do placeholder
inputBoxShadow--zhex-input-box-shadowSombra idle
inputBoxShadowFocus--zhex-input-box-shadow-focusSombra em :focus (use pra fazer "ring")
fieldGap--zhex-field-gapGap entre número, expiry e CVC
brandGlyphSize--zhex-brand-glyph-sizeTamanho do logo da bandeira (Visa, Mastercard…)

Atualizar tema em runtime

elements.update() reaplica appearance sem desmontar o iframe — útil pra um toggle de dark mode no checkout:

const card = elements.create('card');
card.mount('#card');

// usuário clica num switch...
elements.update({
  appearance: {
    theme: 'dark',
    variables: { colorBackground: '#0a0a0a', colorText: '#fafafa' },
  },
});

Tudo que não vem fica no default

Você não precisa setar todas as variáveis — qualquer chave omitida cai pro default do iframe (que tenta combinar luz/escuro automaticamente via theme). Comece com 2-3 cores e ajuste o que destoar do seu checkout.

Por que CSS-in-JS externo não funciona

O iframe vive em js.zhex.io, separado do seu domínio por same-origin policy. Tailwind, styled-components, MUI, etc. do seu app não vazam pra dentro. A única ponte é appearance.variables — qualquer estilo que você queira aplicar tem que passar por essas chaves. É exatamente o que mantém o escopo PCI-DSS em SAQ-A: nem CSS nem JS seu toca os campos onde o cartão é digitado.

Allowlist de origens

zk_* no front exige que o domínio da página seja registrado previamente como origem confiável. Sem isso, POST /v1/tokens retorna 403 e o iframe propaga error.code = "origin_not_allowed".

Registre cada domínio que vai hospedar o iframe (incluindo staging.* e localhost em test mode) via dashboard ou pela API:

curl -X POST https://prometheus.zhex.io/merchant-origins \
  -H "Authorization: Bearer ${ZHEX_DASHBOARD_JWT}" \
  -H "Content-Type: application/json" \
  -d '{
    "origin": "https://checkout.suaempresa.com",
    "livemode": true
  }'

Por baixo dos panos: o iframe pina o parentOrigin no primeiro postMessage(zhex:init) e estampa Zhex-Parent-Origin: <origem-pinada> em cada POST /v1/tokens. A API só confia nesse header quando a requisição vem de um iframe da Zhex (js.zhex.io); de qualquer outra origem, ela cai no Origin do navegador. Os dois caminhos batem na mesma allowlist do merchant.

Test mode usa allowlist própria

Origens registradas em livemode: false só liberam tokens em zk_test_*, e vice-versa. Você não vai conseguir testar com localhost em produção mesmo que adicione o domínio — é proposital.

Anatomia de um Token

O que zhex.createToken() resolve no front é um subset seguro do recurso server-side — sem client_ip, sem fingerprint, só o que a UI precisa para mostrar o cartão e o que você precisa para cobrar:

{
  "id": "tok_a3f29e7b1c4d8052f3a0b8e9c2d1f4a6",
  "type": "CARD",
  "livemode": false,
  "card": {
    "brand": "visa",
    "last4": "4242",
    "expMonth": 12,
    "expYear": 2027,
    "holderName": "MARIA SILVA"
  },
  "used": false,
  "expiresAt": "2026-04-26T14:30:00.000Z",
  "createdAt": "2026-04-26T14:15:00.000Z"
}

Use o que está visível:

  • card.last4 + card.brand para mostrar "Visa terminando em 4242" no checkout.
  • card.holderName para confirmar o nome do titular (null quando não fornecido).
  • expiresAt (ISO) para sumir com o token da UI assim que o TTL acabar.
  • livemode como sanity-check defensivo no servidor — token de test em rota live é sintoma de chave trocada.

Não salve tok_* no seu banco. Ele é single-use e expira em 15 minutos. Para reutilizar em cobranças futuras, suba para PaymentMethod (próxima seção).

Token → PaymentMethod (reutilização)

Endpoint disponível

POST /v1/payment_methods está no ar. Aceita só tok_* produzido pelo zhex.js — dados de cartão crus nunca trafegam por essa rota. O token é consumido atomicamente: uma segunda chamada com o mesmo tok_* sempre falha com token_unusable.

PaymentMethod (pm_*) é o que você associa a um Customer para cobrar de novo sem pedir o cartão. Cria-se a partir de um Token:

import { randomUUID } from 'node:crypto';

const pmRes = await fetch('https://prometheus.zhex.io/v1/payment_methods', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ZHEX_SECRET_KEY}`,
    'Idempotency-Key': randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'card',
    token: 'tok_1NJsVxLkdI...',
    customer: 'cus_abc',
  }),
});
const pm = await pmRes.json();
// { id: "pm_1NJtY7LkdI...", card: { brand, last4, ... }, customer: "cus_abc" }

A confirmação na API V1 aceita tanto tok_* fresco (primeira cobrança ou cliente quer re-tokenizar) quanto pm_* salvo (cobrança recorrente, retry de billing, "1 clique" com cartão guardado).

// Primeira compra — cliente preenche cartão no iframe, servidor recebe tok_*
const intent = await zhex.paymentIntents.create({
  amount: 9700,
  currency: 'brl',
  customer: 'cus_abc',
});

await zhex.paymentIntents.confirm(intent.id, {
  payment_method: 'tok_…',  // do zhex.js
});

// Próxima cobrança recorrente — sem nova tokenização
await zhex.paymentIntents.confirm(nextIntent.id, {
  payment_method: 'pm_…',   // pm_* salvo no customer
  off_session: true,         // sinaliza ao emissor que o cliente está ausente
});

off_session: true avisa o emissor que o cardholder não está presente (renovação de assinatura, retentativa em background). 3DS pode ser exigido mesmo assim em cartões high-risk — nesses casos, o intent volta em requires_action e você notifica o cliente para autorizar.

A Zhex valida que o pm_* pertence ao mesmo customer do intent e à mesma partição (livemode); cross-customer ou cross-mode retornam payment_method_not_found (mesma mensagem que id inexistente — sem leak).

Token vs PaymentMethod

Token (tok_*)PaymentMethod (pm_*)
Tempo de vida15 minPermanente (até deletado)
UsoMúltiplos charges
Onde nasceFront, via zhex.jsServidor, a partir de tok_*
Anexado a Customer?NãoSim, sempre
Bom paraCompra one-shotRecurring, salvar cartão

Regra: se o cliente vai cobrar de novo algum dia, suba para pm_*. Se é cobrança única e ele provavelmente nem volta, fica em tok_* — menos modelo no seu DB.

Tokenização em outros métodos

Hoje só card é tokenizado pelo iframe — todos os IDs ficam no formato genérico tok_<32 hex>. Os tipos abaixo são parte do roadmap; quando entrarem, o type no payload distingue o método (não o prefixo do ID):

TipoStatusVida útil prevista
CARDDisponível15 min
bank_account (US ACH)Roadmap15 min
pix_keyRoadmap15 min
pix_automatic_authorizationRoadmapAté revogada

Para PIX one-shot e boleto, não há tokenização — o método é gerado server-side junto com o PaymentIntent.

Apple Pay e Google Pay

Roadmap

Carteiras digitais ainda não estão expostas pelo SDK público. O snippet abaixo é o contrato planejado — acompanhe o changelog do @zhexio/zhex-js para saber quando zhex.paymentRequest() entra em produção.

zhex.js Elements vai aceitar pagamento por carteiras digitais. O fluxo previsto:

const paymentRequest = zhex.paymentRequest({
  country: 'BR',
  currency: 'brl',
  total: { label: 'Curso Node Avançado', amount: 49700 },
});

const elements = zhex.elements();
const prButton = elements.create('paymentRequestButton', {
  paymentRequest,
});

// só monta se o browser/device suportar
const canMakePayment = await paymentRequest.canMakePayment();
if (canMakePayment) {
  prButton.mount('#payment-request-button');
}

paymentRequest.on('token', async ({ token, complete }) => {
  const res = await fetch('/api/charge', {
    method: 'POST',
    body: JSON.stringify({ token: token.id }),
  });
  if (res.ok) complete('success');
  else complete('fail');
});

A carteira digital faz 3DS implícito — biometric do device serve como autenticação. Liability shift automático, fraude próxima de zero.

Erros comuns na tokenização

zhex.createToken() retorna { error: ElementError } quando algo falha. Os code possíveis são fixos e estão listados aqui:

codeCausaComo tratar no front
incompleteAlgum campo (número, validade ou CVV) ainda não foi preenchidoNão submeter — o iframe emite change com complete: true quando está pronto
invalid_card_numberLuhn check falhou no número digitadoMostrar inline ao lado do campo
invalid_expiryValidade fora do range ou no passadoIdem
invalid_cvcCVV com tamanho errado para a bandeira detectadaIdem
unsupported_cardBandeira detectada que a Zhex não tokeniza (ex: pré-pago restrito)Pedir outro cartão
card_declinedBIN bloqueado pelo emissor (raro nesta etapa)Pedir outro cartão
origin_not_allowedDomínio da página não está na allowlist do merchantErro de configuração — registre o domínio (ver Allowlist de origens)
rate_limitedCliente passou do limite de tokens por minutoBackoff — provavelmente bot/scrape, raramente uso legítimo
network_errorIframe não conseguiu falar com prometheus.zhex.ioRetry curto (1-2 tentativas), depois fallback
unknown_errorFalha inesperada do iframe ou da APILogue o erro com contexto e abra ticket

O iframe já valida incomplete, invalid_card_number, invalid_expiry e invalid_cvc localmente antes de bater na API — você só vê esses códigos se forçar submit antes do estado ficar complete.

CSP (Content Security Policy)

Se sua aplicação tem CSP estrito, libere os domínios da Zhex:

Content-Security-Policy:
  script-src 'self' https://js.zhex.io;
  frame-src https://js.zhex.io;
  connect-src 'self' https://prometheus.zhex.io;

Sem frame-src https://js.zhex.io, o navegador bloqueia a carga do iframe e card.mount() falha silenciosamente — o <div> fica vazio e nenhum evento change chega. Em ambientes com Cloudflare/CSP automático, o caminho mais comum é adicionar js.zhex.io à allowlist. As entradas de script-src e connect-src são preventivas para quando o bundle CDN e métodos diretos como zhex.confirmPayment() forem liberados.

Por que isso importa

Sem tokenização hosted, você teria que:

  • Cumprir 80+ controles PCI-DSS SAQ-D (firewall, key rotation, segregação de rede).
  • Contratar ASV (Approved Scanning Vendor) — scan trimestral pago.
  • QSA (Qualified Security Assessor) — auditoria anual cara para volumes acima de 6M tx/ano.
  • Cifrar PAN em repouso (AES-256 minimum) e em trânsito.
  • Logging segregado, retenção de 12 meses, immutable.
  • Treinamento de pessoal anual.

Com zhex.js, você cumpre 9 controles SAQ-A, todos baseados em "não receber PAN/CVV no servidor". É a diferença entre dois dias de papelada/ano e uma equipe de compliance dedicada.

Próximos passos

Esta página foi útil?

Atualizado em

Nesta página