Appmax JS
O que é o Appmax JS
O appmax.js é uma biblioteca JavaScript desenvolvida pela Appmax para integração segura em páginas de checkout. Ao incluir o script, você não precisa se preocupar com alterações no visual da loja, já que ele atua de forma discreta e eficiente.
Coleta de IP é sempre obrigatória
A coleta de IP via Appmax JS é obrigatória para todas as integrações — inclusive se sua arquitetura estiver em escopo PCI-DSS. Não existe alternativa via API só para esta etapa.
Tokenização server-side exige PCI-DSS
Já a tokenização do cartão pode ser feita de duas formas: pelo próprio Appmax JS (recomendado — o script isola os dados sensíveis do seu servidor) ou diretamente pela sua API, caso sua arquitetura esteja em escopo PCI-DSS. Ao tokenizar pelo backend, o seu servidor toca o número do cartão e o CVV em claro — isso só é permitido dentro do escopo PCI-DSS. Se você não tem certeza, use o caminho via Appmax JS. Veja Tokenização do cartão de crédito.
Como funciona
Devido as diretrizes do PCI DSS (Padrão de Segurança de Dados da Indústria de Cartões de Pagamento), é crucial proteger dados sensíveis. O appmax.js foi projetado para:
- Evitar que dados sensíveis do cartão passem pelos seus servidores.
- Coletar o IP do cliente para segurança do fluxo de pagamento.
Qual é a sua integração
O script atende três cenários, e o que você precisa implementar muda bastante entre eles. Identifique o seu antes de seguir:
| Cenário | O que você usa | Leia |
|---|---|---|
| Checkout com tokenização no browser — o caso mais comum | <form data-appmax-customer> para o IP + <form data-appmax-checkout> para o cartão, com onIp e onTokenize | Coleta do IP e Tokenização |
| Apenas coleta de IP — você tokeniza pela API, em escopo PCI-DSS | <form data-appmax-customer> (pode ser vazio e escondido) + onIp. Não existe form de cartão na página | Coleta do IP |
| Apple Pay e Google Pay — somam-se ao primeiro cenário | O mesmo do checkout, mais onUpdate e onAuthorize e os seletores do botão de cada carteira | Implementando o botão Apple Pay · Implementando o botão Google Pay |
Nos três casos a coleta de IP é obrigatória, e o gatilho é sempre o form[data-appmax-customer].
Como usar
A URL do script muda conforme o ambiente:
<!-- Produção -->
<script src="https://scripts.appmax.com.br/appmax.min.js"></script><!-- Sandbox -->
<script src="https://scripts.sandboxappmax.com.br/appmax.min.js"></script>O script é por ambiente — e não é o mesmo arquivo
Os dois endereços servem builds diferentes: a URL da API fica embutida no bundle em tempo de build. Não existe parâmetro nem configuração para apontar o script de um ambiente para o outro.
Misturar o script de um ambiente com o external_id do outro faz a tokenização acontecer contra a API errada, que não conhece aquele merchant. O resultado é 404 Merchant not found, que chega ao seu onError como TOKENIZATION_FAILED com status: 404 — a message é Failed to process payment: Failed to tokenize card., genérica, e quem diz a causa real é o details. Veja Ambientes para a lista completa de URLs.
Passe um objeto de opções, informando só os callbacks dos eventos que te interessam:
window.AppmaxScripts.init({
externalId: 'SEU_EXTERNAL_ID',
onIp: ({ ip }) => { customer.ip = ip },
onTokenize: ({ token }) => completePayment(token),
onError: (err) => console.error(err.code, err.message, err.stage),
});| Opção | Obrigatório | Descrição |
|---|---|---|
externalId | Sim para tokenização e carteiras | Identificador da instalação do app na loja. Ver external-id. |
onIp | Recomendado | Chamado quando o IP do cliente é coletado. Recebe { ip }. |
onTokenize | Sim para tokenização | Chamado quando o cartão é tokenizado. Recebe { token }. |
onError | Recomendado | Chamado em qualquer falha, com um objeto estruturado — veja O contrato do onError. É opcional: o script sempre registra a falha no console.error, com o prefixo [Appmax]. |
onUpdate | Sim para Apple Pay e Google Pay | Callback chamado quando a PaymentSheet abre e a cada mudança nela (Apple Pay), ou ao carregar o botão e após cada tentativa (Google Pay). Deve retornar o carrinho atual, com valores numéricos em reais — veja o contrato de retorno. O Google Pay utiliza apenas total. |
onAuthorize | Sim para Apple Pay e Google Pay | Callback chamado quando o pagamento é autorizado. O mesmo callback atende às duas carteiras: recebe o Apple Token ou o paymentMethodData do Google Pay. No Apple Pay, rejeite a Promise para sinalizar falha — veja como sinalizar falha; no Google Pay o retorno não é utilizado — veja como o onAuthorize se comporta. |
onSuccess | Não | Callback único, mantido por compatibilidade. Use onIp e onTokenize em código novo — veja onSuccess: o formato antigo. |
onIp e onTokenize substituem o onSuccess no respectivo evento: cada evento chama exatamente um callback, nunca os dois.
Implementando o botão de uma carteira?
onUpdate e onAuthorize são só metade da história — o botão também exige seletores de DOM específicos e uma ordem de carregamento própria. Veja o passo a passo dedicado em Implementando o botão Apple Pay com o Appmax JS e, para o Google Pay, Implementando o botão Google Pay com o Appmax JS.
A forma posicional
A assinatura antiga continua funcionando e não será removida:
window.AppmaxScripts.init(onSuccess, onError, externalId, onUpdate, onAuthorize);Ela não dá acesso aos callbacks por evento — o onSuccess posicional recebe os dois payloads e você precisa discriminar (veja onSuccess: o formato antigo). Para código novo, prefira o objeto.
Nada muda nesta forma. O onSuccess recebe { ip } na coleta e a string do token na tokenização; o onError recebe a mensagem como string, apenas em falha de tokenização. É o mesmo comportamento de sempre, e ele não será alterado — quem já integrou assim não precisa mexer em nada.
Se você só vai usar a coleta de IP, os campos de tokenização e de carteiras (Apple Pay, Google Pay) podem ser omitidos:
window.AppmaxScripts.init({ onIp: ({ ip }) => { customer.ip = ip } });externalId é obrigatório para tokenização pelo Appmax JS
Sem o externalId, o submit de qualquer form com data-appmax-checkout falha com MISSING_EXTERNAL_ID antes de a chamada HTTP ser feita. O valor é o mesmo external_id definido durante a instalação do app na loja — persista no seu banco e renderize no template do checkout. Veja external-id para a referência completa.
O external_id é por ambiente. O id de uma instalação de sandbox não existe no registro de produção: usá-lo lá devolve 404 Merchant not found, que chega no onError como TOKENIZATION_FAILED / status: 404. Ao promover para produção, refaça a instalação e guarde o novo external_id.
Se sua arquitetura está em escopo PCI-DSS e você prefere tokenizar direto pela sua API (sem o script), use o caminho via backend com o access token do merchant (Authorization: Bearer) em vez do externalId — veja Tokenização do cartão de crédito.
init() pode lançar exceção síncrona para carteiras
Se você passar onUpdate e onAuthorize (fluxo Apple Pay ou Google Pay) sem um externalId válido, o init lança Error("External ID is required for Apple Pay use.") de forma síncrona, dentro da própria chamada — não via onError. Em React, isso estoura dentro do useEffect e pode derrubar a árvore inteira se não houver try/catch ao redor da chamada.
O contrato do onError
O que o onError recebe depende da forma de init() que você usou:
Forma de init() | O que chega no onError | Quando é chamado |
|---|---|---|
| Objeto de opções | um objeto estruturado | em qualquer etapa: fingerprint, ip, setup, tokenize |
| Posicional (legada) | a string da mensagem | apenas em falha de tokenização |
A diferença é deliberada: a forma posicional sempre entregou uma string, e mudar isso quebraria integrações em produção. Nas duas formas toda falha continua indo para o console.error com o prefixo [Appmax] — inclusive as etapas que a forma posicional não encaminha.
Campos do objeto estruturado:
| Campo | Conteúdo |
|---|---|
code | MISSING_EXTERNAL_ID, IP_FETCH_FAILED, FINGERPRINT_FAILED, FORM_SETUP_FAILED, TOKENIZATION_FAILED, APPLE_PAY_FAILED ou GOOGLE_PAY_FAILED |
message | Mensagem legível. |
stage | Em que etapa a falha aconteceu: ip, fingerprint, setup, tokenize, apple_pay ou google_pay. |
status | Código HTTP, quando a falha veio da API. |
details | Corpo da resposta, quando disponível — é o que diferencia um 404 de um 422. |
window.AppmaxScripts.init({
externalId: 'SEU_EXTERNAL_ID',
onTokenize: ({ token }) => completePayment(token),
onError: (err) => {
if (err.code === 'TOKENIZATION_FAILED' && err.status === 404) {
// external_id de outro ambiente, ou instalação não concluída
}
console.error(err.code, err.stage, err.status, err.details);
},
});Leia o status antes de abrir o Network tab: 404 é external_id inválido para aquele ambiente, 422 é payload, 5xx é indisponibilidade.
A falha de IP também chega aqui
Na forma de objeto, a falha na coleta de IP chega ao onError como IP_FETCH_FAILED / stage: "ip". Na forma posicional ela vai apenas para o console.error, como sempre foi.
onSuccess: o formato antigo
Se você usa o onSuccess (posicional ou no objeto), o mesmo callback atende os dois eventos, com payloads diferentes:
| Quando | O que chega |
|---|---|
| Coleta de IP | um objeto: { ip: "187.0.0.1" } |
| Tokenização do cartão | a string do token: "tok_..." |
Nesse callback o token não vem embrulhado: data.token é undefined. Para recebê-lo como { token }, use o onTokenize.
function onSuccess(data) {
if (data?.ip) {
customer.ip = data.ip;
return;
}
// aqui data é a string do token
completePayment(data);
}Discriminar por tipo (typeof data === 'string') funciona e continuará funcionando.
Este formato é estável
O onSuccess está depreciado, mas não será removido e o payload dele não muda. O script é servido por uma URL única, sem versionamento: alterar o formato deste callback mudaria o comportamento de todas as integrações ativas ao mesmo tempo. Por isso o caminho novo é aditivo — onIp e onTokenize — em vez de uma mudança no onSuccess.
Contrato de DOM: o init não é reativo
O AppmaxScripts.init(...) faz querySelector uma única vez, no momento em que é chamado, e não observa mudanças no DOM depois disso. Em uma página tradicional (HTML renderizado no servidor, nada muda depois do load) isso é transparente. Em React, Vue ou qualquer SPA, exige atenção:
- O gatilho de IP, o form de cartão e os botões do Apple Pay e do Google Pay precisam existir no DOM antes do
initrodar. Se o elemento aparece depois — atrás de uma rota, um passo do checkout, umv-if/condicional — o SDK nunca o encontra. Não há erro, não há log: o clique ou a coleta simplesmente não acontecem. - Chamar
init()de novo substitui o handler, não empilha. Um submit gera uma tokenização, independentemente de quantas vezes oinitrodou — então o StrictMode do React (que monta efeitos duas vezes em desenvolvimento) não causa cobrança dupla. Ainda assim, chameinituma vez por carregamento de página: cada chamada refaz a coleta de IP e o fingerprint. - O
externalIdé capturado noinit, não lido a cada uso. Trocar o valor depois não muda as requisições seguintes. Se oexternalIdmudar (troca de loja, troca de ambiente), recarregue a página.
Funcionalidades disponíveis
Coleta do IP do cliente
A coleta acontece quando o SDK encontra um <form data-appmax-customer> no DOM — não é preciso dar submit em nada nem esperar interação do usuário. Sem esse form no momento do init, o onIp simplesmente não dispara (ver "Contrato de DOM" acima — é uma falha silenciosa, sem erro nenhum).
Pode ser o formulário de dados do comprador que você já tem:
<form id="customer-form" data-appmax-customer>
<div>
<label for="first-name">Primeiro Nome:</label>
<input type="text" id="first-name" name="first-name" required />
</div>
<div>
<label for="last-name">Ultimo Nome:</label>
<input type="text" id="last-name" name="last-name" required />
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="phone">Telefone:</label>
<input type="text" id="phone" name="phone" required />
</div>
<button type="submit">Enviar</button>
</form>Se a sua página não tem um formulário de comprador — um checkout em etapas, uma SPA que guarda o comprador em estado — use um form vazio e escondido, só como gatilho:
<form data-appmax-customer hidden></form>O SDK injeta um <input hidden> dentro do form[data-appmax-customer]
Ao encontrar esse form, o SDK insere nele um <input type="hidden" id="ip" name="ip" data-appmax-ip> via DOM direto — fora do controle do React/Vue. Em uma página tradicional ele vai junto no submit nativo, e é assim que o IP chega ao seu backend.
O append é idempotente: chamar init() de novo atualiza o valor em vez de anexar um segundo campo.
Nada é injetado no form[data-appmax-checkout] — se você está procurando o campo de IP no formulário de cartão, ele não está lá.
Numa SPA esse nó pode ser descartado no próximo re-render sem aviso nenhum. Não conte com ele: use o IP que chega no onIp, que é a fonte confiável nos dois casos.
Para frameworks como Vue.js, o gatilho precisa estar renderizado antes do init — veja "Contrato de DOM" acima. Recupere o IP no callback:
<template>
<form data-appmax-customer hidden></form>
</template>const customer = ref({
first_name: '',
last_name: '',
email: '',
phone: '',
ip: ''
})
onMounted(() => {
const script = document.createElement('script')
script.src = 'https://scripts.sandboxappmax.com.br/appmax.min.js'
script.onload = () => {
if (window.AppmaxScripts) {
window.AppmaxScripts.init({
onIp: ({ ip }) => { customer.value.ip = ip },
onError: (err) => console.error(err.code, err.message)
})
}
}
document.head.appendChild(script)
})Precisa de um exemplo completo, com tokenização de cartão? Veja Checkout em SPA (React/Next.js).
Tokenização de pagamento
A tokenização é realizada quando um formulário é enviado com o atributo data-appmax-checkout. Os dados sensíveis do cartão são convertidos em um token seguro.
Requer externalId na inicialização
Para tokenizar pelo script, inicialize com o externalId da loja. Sem ele, o submit do form falha com MISSING_EXTERNAL_ID. Veja external-id.
O token chega no callback onTokenize({ token }). Você não precisa tocar em HTTP nem em headers. Se precisar implementar a tokenização manualmente (sem o script), o contrato do endpoint subjacente está em Tokenização do cartão de crédito — inclui o caminho alternativo via backend (Bearer do merchant), permitido apenas em escopo PCI-DSS.
<form id="payment-form" method="POST" data-appmax-checkout>
<div>
<label for="card-number">Numero do Cartao:</label>
<input type="text" id="card-number" name="card-number"
appmax-form-element="number" required />
</div>
<div>
<label for="card-holder-name">Nome do Titular:</label>
<input type="text" id="card-holder-name" name="card-holder-name"
appmax-form-element="holder_name" required />
</div>
<div>
<label for="exp-month">Mes de Expiracao:</label>
<input type="text" id="exp-month" name="exp-month"
appmax-form-element="expiration_month" required />
</div>
<div>
<label for="exp-year">Ano de Expiracao:</label>
<input type="text" id="exp-year" name="exp-year"
appmax-form-element="expiration_year" required />
</div>
<div>
<label for="cvv">CVV:</label>
<input type="text" id="cvv" name="cvv"
appmax-form-element="cvv" required />
</div>
<button type="submit">Pagar</button>
</form>Os name são obrigatórios e de valor fixo — é por eles que o SDK lê o formulário, com new FormData(form). O appmax-form-element é um atributo HTML (não uma classe CSS), mantido por compatibilidade, que não é lido pelo SDK e pode ser omitido.
| Campo | name (obrigatório) | appmax-form-element (opcional) |
|---|---|---|
| Número do cartão de crédito | card-number | number |
| Nome do titular do cartão | card-holder-name | holder_name |
| Mês de expiração | exp-month | expiration_month |
| Ano de expiração | exp-year | expiration_year |
| Código de segurança (CVV) | cvv | cvv |
Os dois valores são diferentes — e isso derruba integrações
Repare que, em cada linha, o name não é igual ao valor do appmax-form-element. Quem assume que são a mesma coisa e escreve name="number", name="holder_name" e assim por diante acerta só o CVV — o único campo em que os dois valores coincidem.
O sintoma é um 422 listando number, holder_name, expiration_month e expiration_year como field is required, com a tela visivelmente preenchida. Trocar ou remover qualquer name faz o campo chegar vazio na tokenização.
Consequência para SPAs: quem lê os campos é o FormData, ou seja, a propriedade value do elemento no DOM — não o estado do framework. Um input controlado funciona, porque o React mantém o DOM sincronizado. O que não funciona é value sem onChange: o React trata o campo como somente leitura, o comprador não consegue digitar e o FormData lê o valor inicial.
Fingerprint do navegador
O getFingerprint() devolve os sinais de dispositivo que o script consegue coletar. É uma chamada direta, independente do init(): devolve Promise, não passa por callback e não exige externalId — basta o script carregado.
const basico = await AppmaxScripts.getFingerprint();
const completo = await AppmaxScripts.getFingerprint({ meta: true });Sem argumento, devolve os sinais básicos: userAgent, language, screenWidth, screenHeight, colorDepth, deviceMemory, hardwareConcurrency, touchPoints, timezone e isPrivate.
Com { meta: true }, o mesmo objeto mais um meta com os sinais de detecção — que vem null se a coleta dos sinais falhar:
Campo de meta | Conteúdo |
|---|---|
bot | { result: "notDetected" | "bad", reasons: [...] } |
incognito | true, false ou null |
developerTools | Heurística de DevTools aberto. |
virtualMachine | Inferido pelo renderer do WebGL. |
privacySettings | Brave, canvas randomizado, resistFingerprinting. |
tampering | Anomalias entre APIs que deveriam concordar. |
webgl | { vendor, renderer } |
extras | math, voices, mediaDevices, network, css |
O retorno fica com você — a Appmax não recebe nada
Esses dados servem à análise de risco da sua operação: o valor fica no browser e só é enviado se você enviar. Não é o token do antifraude da Appmax (payment.Fingerprint.token), que é gerado por outra biblioteca e tem formato e propósito diferentes.
O formato de meta pode mudar — trate os campos de detecção como informativos, não como contrato.
Custo de coleta
Com meta: true a coleta faz WebGL, media devices e vozes, e leva algumas centenas de milissegundos. Dispare no carregamento da página e guarde a Promise; dê await só no submit.
AppCheckout e AppmaxScripts são o mesmo objeto — tanto faz qual você usar.
Página tradicional e SPA: o que muda
As escolhas divergem em pontos concretos:
| MPA (PHP, HTML) | SPA (React/Next, Vue) | |
|---|---|---|
| Gatilho de IP | <form data-appmax-customer> — ver a nota abaixo | <form data-appmax-customer hidden> vazio |
<input hidden name="ip"> injetado | vai no submit nativo, se houver | ignorar — use o IP do onIp |
Quando chamar init() | script no <head>, DOM estável | depois que o externalId chegar do backend, uma vez por carregamento |
| Form de cartão | está na página desde o primeiro paint | sempre montado, escondido por CSS — nunca atrás de um condicional |
preventDefault | o do SDK basta | precisa do seu também |
O gatilho de IP não depende de ser MPA ou SPA
O que decide é como você envia os dados do comprador:
- Submit nativo do formulário (o browser dá POST na sua rota): marque esse formulário com
data-appmax-customer. O SDK injeta<input type="hidden" name="ip">dentro dele, e o IP viaja junto com o POST — você não escreve uma linha de JavaScript para isso. - Envio por JavaScript (
fetch/XHR, compreventDefaultno submit): um<form data-appmax-customer hidden></form>vazio serve só como gatilho, e você lê o IP noonIp. O formulário de verdade fica separado e sem o atributo.
Uma página PHP ou HTML estático que monta o payload em JS usa o formulário vazio, como qualquer SPA. O inverso também vale.
O preventDefault merece destaque próprio: numa SPA existe uma janela real entre a montagem do componente e o fim do init(), que é assíncrono e espera rede. Um clique nessa janela faz o browser dar POST nativo e sair da aplicação — o sintoma clássico de "clicou em pagar e a página virou um 405". Registre o seu próprio preventDefault no form.
Para um exemplo completo em React/Next.js, veja Checkout em SPA.
Diagnóstico: sintoma e causa
| Sintoma | err.code | Causa provável |
|---|---|---|
| O submit não faz nada — sem erro, sem requisição | — | O form não existia no DOM quando o init() rodou (ver "Contrato de DOM"); ou há um elemento .appmax-ip na página, que encerra a inicialização antes de o listener do cartão ser registrado — remova-o e use <form data-appmax-customer> para o IP |
Tokeniza com 201 e o pagamento nunca é disparado | — | Handler lendo data.token num payload que não é objeto. Use onTokenize |
| Todos os campos do cartão chegam vazios | TOKENIZATION_FAILED | Os name do form foram renomeados ou removidos |
422 listando number, holder_name, expiration_month, expiration_year como field is required, com a tela preenchida | TOKENIZATION_FAILED | Os name receberam o valor do appmax-form-element. Passa só o cvv, o único campo em que os dois coincidem |
Failed to process payment: Failed to tokenize card. no onError, com status: 404 | TOKENIZATION_FAILED (status: 404) | Três origens possíveis: external_id de outro ambiente, script de outro ambiente (ver o passo 1), ou um external_id que não é o da instalação — veja external-id |
| O submit falha antes de qualquer requisição | MISSING_EXTERNAL_ID | init() sem externalId |
| O IP nunca chega no callback | IP_FETCH_FAILED | Falha de rede na coleta; ou não há form[data-appmax-customer] no DOM (aí não há erro nenhum) |
O browser navega para fora da página / 405 ao pagar | — | O clique caiu antes do fim do init(), que é assíncrono. Ponha o seu próprio preventDefault no form |
A coluna err.code vale para o init() com objeto de opções. Na forma posicional o onError recebe só a mensagem, e apenas em falha de tokenização — as demais etapas aparecem no console.error.
Qual bundle esta página carregou?
Enquanto o script não expõe um número de versão, dá para conferir no console se a página está com o bundle atual:
document.querySelector('[data-appmax-checkout]').dataset.appmaxBound
// "true" → bundle atual
// undefined → bundle anterior (provavelmente em cache)O atributo só é escrito pelo bundle atual. É a primeira coisa a checar quando o comportamento não bate com esta página.
Versão do script e cache
O script é publicado sempre no mesmo endereço, sem versão no caminho da URL. Duas consequências práticas:
- O cache é de aproximadamente 4 horas. Uma correção publicada pela Appmax pode levar até esse tempo para chegar ao navegador do comprador, e não há como forçar. Em desenvolvimento, recarregue ignorando o cache.
- Não há como fixar uma versão. Se o comportamento observado não corresponde ao que está documentado aqui, confira o marcador
dataset.appmaxBoundacima antes de investigar o seu código.
Mudanças recentes do script, para quem está migrando de um bundle antigo:
| O que mudou | Antes | Agora |
|---|---|---|
| Token no callback | string crua no onSuccess | inalterado no onSuccess; onTokenize({ token }) é o caminho novo |
| Erros | console.error e mensagens genéricas | inalterado na forma posicional; { code, message, stage, status?, details? } na forma de objeto |
| Callbacks | só o onSuccess polimórfico | onIp e onTokenize dedicados, e init() aceita objeto de opções |
| Submit do cartão | dois listeners por init() — um clique gerava duas tokenizações | um clique, uma tokenização |
| Coleta de IP | dois GET /ip, dois callbacks e dois <input name="ip"> injetados | uma vez de cada, com append idempotente |
| Falha de fingerprint | abortava a inicialização, deixando o form sem listener | não bloqueia o bind, e notifica FINGERPRINT_FAILED |
Teste interativo
Use o playground abaixo para testar as funcionalidades da CDN diretamente no browser — sem precisar instalar nada.