Welcome to AlliancePay
Implementar um checkout próprio com a AlliancePay
Este guia mostra como criar um link e concluir seu pagamento dentro da interface do parceiro, usando cartão ou Pix. Inclui tokenização, autenticação 3DS, adesão de cartão para recorrência, recuperação de tentativas e acompanhamento do resultado.
Os exemplos usam Node.js 22+ no backend e JavaScript com APIs nativas no navegador, em HTTPS. Os IDs, dados cadastrais e respostas ilustrativas devem ser substituídos pelos dados do seu ambiente. Chaves públicas, campos cifrados, creq e Pix copia e cola dos exemplos não representam transações reais.
1. Arquitetura e responsabilidades
O navegador coleta os dados do pagador, cifra os campos do cartão com a chave de tokenização, exibe o desafio 3DS e apresenta o resultado. O backend do parceiro mantém as credenciais OAuth, vincula cada tentativa ao pedido e faz as chamadas à AlliancePay. A AlliancePay valida o escopo do parceiro, integra com os provedores, recebe os callbacks e disponibiliza o estado atual pela API.
O parceiro implementa sua própria sessão de checkout. As rotas do seu backend devem validar o pedido e os recursos associados a ele antes de chamar a AlliancePay. O browser não deve receber o client_secret nem o Bearer M2M. No 3DS, os formulários enviados ao emissor contêm somente os dados da ação retornada.
Ambientes
| Ambiente | Base da API | Token OAuth |
|---|---|---|
| Homologação | https://api-hml.alliancepay.app | POST /oauth/token |
| Produção | https://api.alliancepay.app | POST /oauth/token |
Credenciais, IDs e tokens pertencem ao ambiente. O domínio do checkout hospedado vem em checkout_url; ele é distinto do domínio da API. Todas as rotas /acquiring/... abaixo exigem Bearer do parceiro, inclusive as que contêm /public/.
Identificadores usados no fluxo
| Identificador | Origem | Uso |
|---|---|---|
access_token | POST /oauth/token | Autenticar o parceiro no gateway |
payment_link_id | id da criação/detalhe do link | Gestão do link, pagamentos e histórico |
public_token | Último segmento do pathname de checkout_url | Identificar o link nas rotas de checkout |
tokenization_id | id da tokenização | Confirmar e consultar a tokenização |
payment_method_id | Tokenização com status READY | Referenciar o cartão na criação do pagamento/adesão |
payment_id | id da tentativa de cartão ou Pix | Consultar o pagamento e continuar seu 3DS |
setup_id | id da adesão de cartão | Consultar a adesão e continuar seu 3DS |
Idempotency-Key | Gerada e preservada pelo parceiro | Identificar a mesma tentativa/operação nas repetições |
public_token é aleatório, não é JWT e não substitui OAuth. No gateway, o link precisa pertencer ao parceiro autenticado. A disponibilidade para pagar depende de status, expires_at e das regras do link; renovar OAuth não renova o link. O checkout público hospedado continua acessível pela URL compartilhada, por isso trate URLs de checkout com cuidado em logs e analytics.
2. Autenticação e cliente de backend
Solicite o token com HTTP Basic e grant_type=client_credentials. Também é aceito enviar client_id e client_secret no corpo. O token dura até uma hora; reutilize-o e observe expires_in.
Exemplo em shell compatível com cURL, executado em ambiente de backend com as variáveis já configuradas:
curl --request POST "$ALLIANCEPAY_BASE_URL/oauth/token" \
--user "$ALLIANCEPAY_CLIENT_ID:$ALLIANCEPAY_CLIENT_SECRET" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials'Use Accept: application/json e, em escritas JSON, Content-Type: application/json. O gateway repassa Idempotency-Key e X-Request-Id; Origin e X-Partner-ID não são necessários. O X-Request-Id da resposta pode ser informado ao suporte.
O módulo abaixo mantém o token somente no backend e não repete automaticamente uma escrita após falha. Ao receber 401, ele invalida o cache; a decisão de repetir uma operação depende da recuperação do estado da tentativa.
Arquivo: gateway-client.mjs
// Executar somente no backend. Node.js 22+; sem dependências externas.
const ENVIRONMENTS = new Set([
"https://api-hml.alliancepay.app",
"https://api.alliancepay.app",
]);
export class GatewayError extends Error {
constructor(status, body, requestId) {
super(`AlliancePay respondeu HTTP ${status}`);
this.name = "GatewayError";
this.status = status;
this.body = body;
this.requestId = requestId;
}
}
export function checkoutPath(publicToken) {
if (typeof publicToken !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(publicToken)) {
throw new Error("public_token inválido");
}
return `/acquiring/payment-links/public/${publicToken}`;
}
export function createGatewayClient({
baseUrl,
clientId,
clientSecret,
fetchImpl = globalThis.fetch,
timeoutMs = 25000,
now = Date.now,
}) {
const origin = new URL(baseUrl).origin;
if (!ENVIRONMENTS.has(origin) || !clientId || !clientSecret) {
throw new Error("Configure ambiente, client_id e client_secret no backend");
}
let cachedToken;
let usableUntil = 0;
let tokenInFlight;
async function exchangeToken() {
const response = await fetchImpl(`${origin}/oauth/token`, {
method: "POST",
redirect: "error",
signal: AbortSignal.timeout(timeoutMs),
headers: {
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({ grant_type: "client_credentials" }),
});
const body = await response.json().catch(() => null);
if (!response.ok) {
throw new GatewayError(response.status, body, response.headers.get("x-request-id"));
}
const ttl = Number(body?.expires_in);
if (typeof body?.access_token !== "string" || !Number.isFinite(ttl) || ttl <= 0) {
throw new Error("Resposta de autenticação inválida");
}
cachedToken = body.access_token;
usableUntil = now() + Math.max(0, Math.min(ttl, 3600) - 60) * 1000;
return cachedToken;
}
async function accessToken() {
if (cachedToken && now() < usableUntil) return cachedToken;
tokenInFlight ??= exchangeToken().finally(() => { tokenInFlight = undefined; });
return tokenInFlight;
}
async function request(path, { method = "GET", body, idempotencyKey } = {}) {
const url = new URL(path, origin);
if (url.origin !== origin || !url.pathname.startsWith("/acquiring/") || !["GET", "POST"].includes(method)) {
throw new Error("Destino não permitido neste exemplo");
}
const headers = {
Accept: "application/json",
Authorization: `Bearer ${await accessToken()}`,
"X-Request-Id": crypto.randomUUID(),
};
if (body !== undefined) headers["Content-Type"] = "application/json";
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
// Não repetir escritas automaticamente: o resultado de um timeout pode ser incerto.
const response = await fetchImpl(url, {
method, headers, redirect: "error", signal: AbortSignal.timeout(timeoutMs),
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
});
const data = await response.json().catch(() => null);
if (!response.ok) {
if (response.status === 401) { cachedToken = undefined; usableUntil = 0; }
throw new GatewayError(response.status, data, response.headers.get("x-request-id"));
}
return { status: response.status, data, requestId: response.headers.get("x-request-id") };
}
return { request };
}Inicialização no processo do backend:
import { createGatewayClient, checkoutPath } from "./gateway-client.mjs";
const alliancepay = createGatewayClient({
baseUrl: process.env.ALLIANCEPAY_BASE_URL,
clientId: process.env.ALLIANCEPAY_CLIENT_ID,
clientSecret: process.env.ALLIANCEPAY_CLIENT_SECRET,
});Essas variáveis não devem ser publicadas no bundle do frontend. O módulo request() é uma função interna do backend; não o exponha como proxy de URL livre para o navegador.
3. Criar o link e preparar o checkout
3.1 Consultar opções para o EC
GET /acquiring/payment-links/creation-options/?acquirer_registration_id={ec_id}
Authorization: Bearer {access_token}Verifique os métodos e condições disponíveis para o EC. Na criação e nas opções de links, partner_id pode ser omitido: a autenticação identifica o parceiro.
3.2 Criar um link avulso
POST /acquiring/payment-links/
Authorization: Bearer {access_token}
Content-Type: application/jsonExemplo JSON, conforme o schema CreatePaymentLinkRequest:
{
"acquirer_registration_id": "11111111-1111-4111-8111-111111111111",
"billing_type": "ONE_TIME",
"amount": "149.90",
"description": "Pedido de exemplo",
"external_reference": "pedido-exemplo-001",
"payment_methods": ["CREDIT", "PIX"],
"max_installments": 3
}Persistir no pedido o id do link e a checkout_url recebidos. external_reference ajuda na correlação, mas não é uma chave de idempotência. A criação do link não tem o mesmo contrato idempotente de /payments; uma resposta perdida não autoriza recriar o link sem verificar o resultado.
3.3 Obter o token público
A resposta atual fornece checkout_url, sem um campo independente public_token. Extraia o token do pathname, ignorando query string e barra final:
const url = new URL(link.checkout_url);
const publicToken = url.pathname.split("/").filter(Boolean).at(-1);
const base = checkoutPath(publicToken); // somente no backend
const { data: checkout } = await alliancepay.request(base);A versão com validação está no módulo de navegador adiante. O backend também pode extrair e guardar esse token, evitando que o browser precise montar caminhos da AlliancePay.
3.4 Consultar o contrato específico do link
GET /acquiring/payment-links/public/{public_token}
Authorization: Bearer {access_token}Antes de iniciar uma nova tentativa, ler:
available,statuseexpires_at: disponibilidade do link.payment_methodsemax_installments: opções permitidas.checkout_mode:PAYMENTpara pagamento ouCARD_SETUPpara adesão sem cobrança imediata.checkout_requirementsecheckout_requirements_by_payment_method: campos exigidos do pagador.card_setup,recurrence,recoveryerecurring_acceptance: contexto de adesão/recorrência/recuperação.
O valor cobrado é definido pelo link. A criação do pagamento não recebe um campo amount. Documento, telefone e endereço podem ser exigidos pela configuração; as respostas de checkout são a referência para montar o formulário. O customer da consulta pública pode conter documento e e-mail mascarados: esses valores não substituem os dados completos exigidos no pagamento.
Mesmo que o link fique indisponível após o pagamento, continue consultando a tentativa existente para obter seu resultado.
4. Rotas do backend do parceiro
Neste guia, /api/checkout/... são rotas exemplificativas a implementar na aplicação do parceiro. Elas não existem na API AlliancePay. O backend resolve o public_token a partir do pedido validado, acrescenta o Bearer e devolve o JSON recebido da AlliancePay.
| Rota exemplificativa do parceiro | Operação AlliancePay |
|---|---|
GET /api/checkout/{pedido} | GET /acquiring/payment-links/public/{public_token} |
POST /api/checkout/{pedido}/card-tokenizations | POST .../card-tokenizations |
POST /api/checkout/{pedido}/card-tokenizations/{id}/confirm | POST .../card-tokenizations/{id}/confirm |
GET /api/checkout/{pedido}/card-tokenizations/{id} | GET .../card-tokenizations/{id} |
POST /api/checkout/{pedido}/payments | POST .../payments |
GET /api/checkout/{pedido}/payments/{id} | GET .../payments/{id} |
GET /api/checkout/{pedido}/payments/by-idempotency-key | GET .../payments/by-idempotency-key |
POST /api/checkout/{pedido}/payments/{id}/three-ds/continue | POST .../payments/{id}/three-ds/continue |
POST /api/checkout/{pedido}/pix-payments | POST .../pix-payments |
GET /api/checkout/{pedido}/pix-payments/{id} | GET .../pix-payments/{id} |
GET /api/checkout/{pedido}/pix-payments/by-idempotency-key | GET .../pix-payments/by-idempotency-key |
POST /api/checkout/{pedido}/card-setups | POST .../card-setups |
GET /api/checkout/{pedido}/card-setups/{id} | GET .../card-setups/{id} |
POST /api/checkout/{pedido}/card-setups/{id}/three-ds/continue | POST .../card-setups/{id}/three-ds/continue |
Nos caminhos abreviados, ... significa /acquiring/payment-links/public/{public_token}. Preserve método, corpo, código HTTP e a chave de idempotência nas rotas correspondentes. Não transforme erros em HTTP 200. Verifique que IDs de pagamento, tokenização e adesão estão associados ao pedido atendido. Se a sessão usa cookies, aplique a proteção CSRF do seu framework.
O backend deve obter o IP do pagador a partir da conexão e dos proxies confiáveis da sua infraestrutura. Antes de criar cartão/adesão, preenchê-lo em payload.browser.ip_address. Não use o IP do gateway nem aceite um X-Forwarded-For arbitrário como verdade.
O adaptador de navegador usado nos exemplos é:
Arquivo: checkout-api.mjs
// Adaptador do browser para rotas DO PARCEIRO. Essas URLs NÃO pertencem à AlliancePay.
// Implemente o mapeamento descrito no guia, validando sessão de checkout, pedido e IDs.
export function createCheckoutApi(orderId, { csrfToken, fetchImpl = globalThis.fetch } = {}) {
const base = `/api/checkout/${encodeURIComponent(orderId)}`;
return async function call(suffix, { method = "GET", body, idempotencyKey } = {}) {
const headers = { Accept: "application/json" };
if (body !== undefined) headers["Content-Type"] = "application/json";
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const response = await fetchImpl(`${base}${suffix}`, {
method, headers, credentials: "same-origin", cache: "no-store", redirect: "error",
signal: AbortSignal.timeout(30000),
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
});
const data = await response.json().catch(() => null);
if (!response.ok) {
const error = new Error("Não foi possível concluir esta etapa; consulte a tentativa existente.");
error.status = response.status;
error.body = data;
throw error;
}
return data;
};
}5. Pagamento com cartão
5.1 Iniciar tokenização
No backend, chamar com uma chave exclusiva da tokenização:
POST /acquiring/payment-links/public/{public_token}/card-tokenizations
Authorization: Bearer {access_token}
Idempotency-Key: {chave_da_tokenizacao}
Content-Type: application/json
{}A resposta 201 contém id, status, public_key e expires_at. Guardar o ID e a chave da tentativa. A chave pública é temporária, normalmente limitada a 15 minutos e à validade do link; respeite expires_at e a presença de public_key.
5.2 Cifrar e confirmar
Usar a chave pública PEM/SPKI retornada na tokenização com RSA-OAEP, SHA-1/MGF1-SHA-1, texto UTF-8 e saída Base64. Esse algoritmo corresponde ao contrato atual da API. Cifrar cada campo separadamente: número, mês, ano e CVV. O ano deve ser completo, como 2030; usar mês de 01 a 12. A bandeira não é cifrada.
POST /acquiring/payment-links/public/{public_token}/card-tokenizations/{tokenization_id}/confirm
Authorization: Bearer {access_token}
Content-Type: application/jsonExemplo JSON, conforme o schema ConfirmCardTokenizationRequest:
{
"payment_method": "CREDIT",
"card": {
"brand": "VISA",
"number": "BASE64_DO_NUMERO_CIFRADO",
"expiration_month": "BASE64_DO_MES_CIFRADO",
"expiration_year": "BASE64_DO_ANO_CIFRADO"
}
}Os textos BASE64_... são marcadores, não cifras válidas. O CVV cifrado será enviado na criação do pagamento/adesão, não nessa confirmação. As bandeiras aceitas pelo contrato são VISA, MASTERCARD, ELO, HIPERCARD e AMEX, sujeitas à habilitação do EC.
Aguardar status=READY e payment_method_id. Se a confirmação retornar PROCESSING, consultar a tokenização existente:
GET /acquiring/payment-links/public/{public_token}/card-tokenizations/{tokenization_id}Em FAILED ou EXPIRED, essa tokenização não pode ser usada para iniciar o pagamento. Antes de criar outra, distinguir essa fase de uma tentativa financeira já enviada. Não persistir PAN ou CVV em armazenamento do navegador ou logs; mantenha a cifra do CVV apenas pelo tempo necessário para enviar a tentativa.
5.3 Criar a tentativa de pagamento
POST /acquiring/payment-links/public/{public_token}/payments
Authorization: Bearer {access_token}
Idempotency-Key: {chave_do_pagamento}
Content-Type: application/jsonExemplo JSON, conforme o schema CheckoutCardPaymentRequest:
{
"payment_method_id": "22222222-2222-4222-8222-222222222222",
"payment_method": "CREDIT",
"installments": 1,
"card": { "security_code": "BASE64_DO_CVV_CIFRADO" },
"payer": {
"name": "Pagador de Exemplo",
"email": "[email protected]",
"document": "11144477735"
},
"phone": { "country_code": "55", "area_code": "65", "number": "999990000" },
"billing_address": {
"country": "BR",
"zip_code": "78000000",
"state": "MT",
"city": "Cuiaba",
"street": "Rua de Exemplo",
"district": "Centro",
"street_number": "100"
},
"browser": {
"http_accept_browser_value": "text/html,application/xhtml+xml",
"http_browser_java_script_enabled": true,
"http_browser_java_enabled": false,
"http_browser_language": "pt-BR",
"http_browser_color_depth": 24,
"http_browser_screen_height": 1080,
"http_browser_screen_width": 1920,
"http_browser_time_difference": 240,
"user_agent_browser_value": "Mozilla/5.0",
"ip_address": "192.0.2.10"
}
}Substituir todos os dados fictícios pelos dados reais do pagador e do navegador. Documento deve ser válido; endereço deve ser brasileiro; telefone segue código 55, DDD de dois dígitos e número de nove dígitos iniciado por 9. Os serializers rejeitam campos desconhecidos. Quantidade de parcelas e modalidade seguem o link e sua habilitação.
A chave de pagamento deve ser diferente da chave de tokenização e persistida antes do POST. A resposta 201 indica criação/recuperação da tentativa; não é confirmação de aprovação. Persistir seu id e tratar status e next_action.
5.4 Exemplo de montagem no navegador
Este trecho considera que o parceiro já coletou e validou form, payer, phone e billingAddress, e que checkoutState é o registro da tentativa mantido no seu backend. Os helpers de criptografia e browser aparecem na seção 6.2.
import { createCheckoutApi } from "./checkout-api.mjs";
import { browserPayload, importCheckoutKey, encryptCheckoutField } from "./browser-3ds.mjs";
const call = createCheckoutApi(orderId, { csrfToken });
// Chaves geradas/persistidas uma vez por tentativa; reutilizadas nas retomadas.
const tokenization = await call("/card-tokenizations", {
method: "POST", body: {}, idempotencyKey: checkoutState.tokenizationKey,
});
if (!tokenization.public_key) throw new Error("Tokenização sem chave disponível");
const key = await importCheckoutKey(tokenization.public_key);
const encrypted = {
number: await encryptCheckoutField(key, form.cardNumber.replace(/\D/g, "")),
expiration_month: await encryptCheckoutField(key, form.month.padStart(2, "0")),
expiration_year: await encryptCheckoutField(key, form.fullYear),
};
const tokenized = await call(`/card-tokenizations/${tokenization.id}/confirm`, {
method: "POST", body: { payment_method: "CREDIT", card: { brand: form.brand, ...encrypted } },
});
// Se PROCESSING, consultar a tokenização; não reiniciar o fluxo por timeout.
if (tokenized.status !== "READY" || !tokenized.payment_method_id) {
throw new Error("Acompanhe a tokenização existente antes de prosseguir");
}
const payment = await call("/payments", {
method: "POST", idempotencyKey: checkoutState.paymentKey,
body: {
payment_method_id: tokenized.payment_method_id,
payment_method: "CREDIT", installments: 1,
card: { security_code: await encryptCheckoutField(key, form.cvv) },
payer, phone, billing_address: billingAddress, browser: browserPayload(),
},
});
// O backend acrescenta browser.ip_address e persiste payment.id antes de responder.
// Seguir para trackCardOperation(payment); não mostrar sucesso apenas pelo HTTP 201.Em caso de erro na criação, seguir a recuperação da seção 9. Não executar novamente todo o trecho criando novas chaves. Na recorrência com cobrança inicial por cartão, incluir também recurring_consent=true e a consent_terms_version apresentada e aceita pelo pagador.
6. Autenticação 3DS no checkout próprio
O 3DS autentica o portador do cartão. A API pode solicitar uma coleta invisível de informações do navegador, apresentar um desafio do banco emissor ou aguardar autenticação fora da página. A aprovação financeira é verificada depois pelo estado do pagamento.
6.1 Comportamento por ação
| Ação | Como implementar | Próxima chamada |
|---|---|---|
THREE_DS_METHOD | Formulário POST direcionado a iframe invisível. Campo threeDSMethodData recebe next_action.three_ds_method_data, sem alteração. | Após a janela de 10 segundos usada no fluxo de referência, POST .../payments/{payment_id}/three-ds/continue com {}. |
THREE_DS_CHALLENGE | Formulário POST direcionado a iframe visível. Campo creq recebe next_action.creq, sem alteração. O pagador interage com a interface do emissor. | Consultar o pagamento; preservar o iframe durante o polling. |
THREE_DS_DECOUPLED | Mostrar que a autenticação está pendente fora da página. | Consultar respeitando next_action.poll_after_seconds. |
Ausente ou null | Ler status; se não terminal, continuar consultando. | GET .../payments/{payment_id}. |
O destino dos formulários é next_action.url, em HTTPS. Usar application/x-www-form-urlencoded, com form.target apontando para o iframe. Uma navegação GET à URL não envia o desafio. Não decodifique nem recodifique creq/three_ds_method_data: o Core já entrega o valor a encaminhar. Esse mecanismo está documentado pelo provedor em 3DS Method e Challenge.
Exemplo de resposta de desafio, com dados ilustrativos:
Exemplo JSON, conforme o schema CheckoutCardPayment:
{
"id": "33333333-3333-4333-8333-333333333333",
"status": "AUTHENTICATING",
"amount": "149.90",
"currency": "BRL",
"installments": 1,
"payment_method": {
"id": "22222222-2222-4222-8222-222222222222",
"type": "CREDIT",
"brand": "VISA",
"last_four": "1111"
},
"next_action": {
"type": "THREE_DS_CHALLENGE",
"url": "https://acs.example.com/challenge",
"creq": "VALOR_OPACO_RETORNADO_PELA_API"
}
}Durante o desafio, mantenha o container montado. Deduplicar a ação por ID da tentativa, URL e conteúdo do desafio evita reiniciar a autenticação em cada GET. Se uma resposta intermediária vier sem next_action enquanto o pagamento continua AUTHENTICATING, preserve a tela existente e consulte novamente.
A política CSP do site precisa permitir os destinos de iframe e formulário envolvidos na integração homologada. Não tente ler o DOM do emissor ou inferir aprovação por um evento load do iframe. Os callbacks são recebidos pela AlliancePay; o resultado para o parceiro vem da consulta à API.
6.2 Helpers de navegador: cifra, formulário e acompanhamento
O módulo abaixo inclui o envio dos formulários, a espera do Method, a preservação do Challenge e o polling. Os callbacks get e continueThreeDs chamam o backend do parceiro, conforme a seção 4. Iniciar apenas um controlador por tentativa/tela.
O limite de cinco minutos é uma escolha de interface do exemplo para pausar a atualização automática. Ele não cancela a tentativa nem define sua validade no servidor. Ao pausar, oferecer “Atualizar status” e continuar usando o mesmo ID.
Arquivo: browser-3ds.mjs
// Helpers para o navegador. Nenhuma credencial OAuth é recebida por este módulo.
export function publicTokenFromCheckoutUrl(checkoutUrl) {
const url = new URL(checkoutUrl);
const segments = url.pathname.split("/").filter(Boolean);
const token = segments.at(-1);
if (url.protocol !== "https:" || segments.at(-2) !== "payment-links" || !/^[A-Za-z0-9_-]{1,128}$/.test(token ?? "")) {
throw new Error("checkout_url inválida");
}
return token;
}
export function browserPayload() {
return {
http_accept_browser_value: "text/html,application/xhtml+xml",
http_browser_java_script_enabled: true,
http_browser_java_enabled: navigator.javaEnabled?.() ?? false,
http_browser_language: navigator.language,
http_browser_color_depth: screen.colorDepth,
http_browser_screen_height: screen.height,
http_browser_screen_width: screen.width,
http_browser_time_difference: new Date().getTimezoneOffset(),
user_agent_browser_value: navigator.userAgent,
};
}
export async function importCheckoutKey(publicKeyPem) {
const pem = publicKeyPem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, "");
const bytes = Uint8Array.from(atob(pem), character => character.charCodeAt(0));
return crypto.subtle.importKey("spki", bytes, { name: "RSA-OAEP", hash: "SHA-1" }, false, ["encrypt"]);
}
export async function encryptCheckoutField(key, value) {
const ciphertext = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, key, new TextEncoder().encode(value));
return btoa(String.fromCharCode(...new Uint8Array(ciphertext)));
}
export function wait(ms, signal) {
return new Promise((resolve, reject) => {
signal?.throwIfAborted();
const abort = () => {
clearTimeout(timer);
reject(signal.reason ?? new DOMException("Interrompido", "AbortError"));
};
const timer = setTimeout(() => { signal?.removeEventListener("abort", abort); resolve(); }, ms);
signal?.addEventListener("abort", abort, { once: true });
});
}
export function mountThreeDsForm({ url, field, value, visible, container }) {
const target = new URL(url);
if (target.protocol !== "https:" || target.username || target.password) throw new Error("URL 3DS inválida");
if (visible && !container) throw new Error("Informe o container visível para o desafio");
const frame = document.createElement("iframe");
frame.name = `three-ds-${crypto.randomUUID()}`;
frame.title = visible ? "Autenticação do cartão pelo banco emissor" : "Verificação do navegador";
frame.hidden = !visible;
if (visible) { frame.style.width = "100%"; frame.style.minHeight = "600px"; frame.style.border = "0"; }
(visible ? container : document.body).append(frame);
const form = document.createElement("form");
form.hidden = true;
form.method = "POST";
form.action = target.href;
form.target = frame.name;
form.enctype = "application/x-www-form-urlencoded";
const input = document.createElement("input");
input.type = "hidden";
input.name = field;
input.value = value;
form.append(input);
document.body.append(form);
try { form.submit(); } catch (error) { frame.remove(); throw error; } finally { form.remove(); }
return () => frame.remove();
}
export function createThreeDsController({ container, mountForm = mountThreeDsForm, sleep = wait }) {
let active;
const completedMethods = new Set();
const dispose = () => { active?.remove(); active = undefined; };
async function handle(resource, { continueThreeDs, signal }) {
const action = resource.next_action;
if (!action) {
// Uma resposta incompleta enquanto AUTHENTICATING não deve desmontar o desafio.
if (resource.status !== "AUTHENTICATING") dispose();
return null;
}
if (action.type === "THREE_DS_DECOUPLED") { dispose(); return null; }
if (!["THREE_DS_METHOD", "THREE_DS_CHALLENGE"].includes(action.type)) {
throw new Error("Ação 3DS desconhecida; consulte novamente ou acione o suporte");
}
const challenge = action.type === "THREE_DS_CHALLENGE";
const value = challenge ? action.creq : action.three_ds_method_data;
if (!action.url || !value) throw new Error("Ação 3DS incompleta; preserve a tentativa e consulte novamente");
const key = JSON.stringify([resource.id, action.type, action.url, value]);
if (active?.key !== key) {
dispose();
active = {
key,
remove: mountForm({ url: action.url, field: challenge ? "creq" : "threeDSMethodData", value, visible: challenge, container }),
methodWaited: false,
};
}
if (challenge || completedMethods.has(key)) return null;
if (!active.methodWaited) {
const current = active;
await sleep(10000, signal);
signal?.throwIfAborted();
if (active !== current) return null;
current.methodWaited = true;
}
// O Core determina a conclusão do Method pelos callbacks. O corpo é {}.
const result = await continueThreeDs(resource.id, {});
completedMethods.add(key);
return result;
}
return { handle, dispose };
}
export function isTerminal(resource, kind = "payment") {
const statuses = kind === "card_setup"
? ["COMPLETED", "FAILED", "EXPIRED", "CANCELED"]
: ["APPROVED", "DECLINED", "FAILED", "EXPIRED"];
return statuses.includes(resource.status);
}
// get e continueThreeDs chamam exclusivamente o backend do parceiro.
export async function trackCardOperation({
initial, get, continueThreeDs, controller, kind = "payment", onUpdate = () => {},
onTransientError = () => {}, signal, sleep = wait, now = Date.now, maxMs = 300000,
}) {
const started = now();
let resource = initial;
while (now() - started < maxMs) {
signal?.throwIfAborted();
onUpdate(resource);
if (isTerminal(resource, kind)) { controller.dispose(); return { outcome: "terminal", resource }; }
try {
const continued = await controller.handle(resource, { continueThreeDs, signal });
if (continued) { resource = continued; continue; }
} catch (error) {
signal?.throwIfAborted();
if (error.status >= 400 && error.status < 500 && error.status !== 429) throw error;
onTransientError(error); // Falha de rede ou de renderização não comprova falha financeira.
}
const suggested = resource.next_action?.type === "THREE_DS_DECOUPLED"
? Number(resource.next_action.poll_after_seconds) : 20;
const seconds = Number.isFinite(suggested) && suggested > 0 ? Math.max(1, suggested) : 20;
await sleep(seconds * 1000, signal);
try { resource = await get(resource.id); } catch (error) {
signal?.throwIfAborted();
if (error.status >= 400 && error.status < 500 && error.status !== 429) throw error;
onTransientError(error);
}
}
// Preservar a tentativa e a interface; oferecer "Atualizar status".
return { outcome: "paused", resource };
}O container de desafio precisa existir antes de iniciar o acompanhamento:
<p id="payment-status" role="status" aria-live="polite"></p>
<div id="three-ds-challenge-slot"></div>Exemplo de conexão dos helpers à interface, após receber payment:
import { createThreeDsController, trackCardOperation } from "./browser-3ds.mjs";
const abort = new AbortController();
const controller = createThreeDsController({
container: document.getElementById("three-ds-challenge-slot"),
});
const result = await trackCardOperation({
initial: payment,
get: id => call(`/payments/${id}`),
continueThreeDs: (id, body) => call(`/payments/${id}/three-ds/continue`, { method: "POST", body }),
controller,
signal: abort.signal,
onUpdate: resource => {
document.getElementById("payment-status").textContent =
resource.status === "APPROVED" ? "Pagamento aprovado" : `Status: ${resource.status}`;
},
onTransientError: () => {
document.getElementById("payment-status").textContent = "Aguardando confirmação. Preserve esta tentativa.";
},
});
if (result.outcome === "paused") {
// Habilitar botão "Atualizar status" que consulta result.resource.id.
}
// Ao desmontar a tela: abort.abort(); controller.dispose();
// Tratar rejeições do acompanhamento na camada de UI, preservando o ID da tentativa.6.3 Continuação e callbacks
POST /acquiring/payment-links/public/{public_token}/payments/{payment_id}/three-ds/continue
Authorization: Bearer {access_token}
Content-Type: application/json
{}O campo opcional three_ds_method_completed é aceito pelo contrato, mas o Core determina a conclusão do Method a partir de seu estado/callback. Enviar true não substitui a autenticação do emissor. Em THREE_DS_CHALLENGE, a interface executa o desafio e acompanha o GET; não repete a criação do pagamento.
Os callbacks do provedor são tratados pela AlliancePay. O parceiro não precisa implementar um endpoint de callback 3DS neste contrato e não deve mudar as URLs contidas nos dados da ação. O Core consulta o provedor para confirmar o resultado e continua o processamento da mesma tentativa.
6.4 Interpretar o estado financeiro
status do pagamento | Tratamento |
|---|---|
CREATED, AUTHENTICATING | Tentativa em andamento; executar next_action quando presente |
PROCESSING, PENDING_CONFIRMATION | Aguardar e consultar; resultado ainda pendente |
APPROVED | Pagamento aprovado; confirmar o pedido no backend de forma idempotente |
DECLINED | Pagamento recusado; apresentar mensagem apropriada |
FAILED | Tentativa falhou; considerar failure_code/failure_message retornados |
EXPIRED | Tentativa expirada; consultar o link antes de oferecer outra tentativa |
O backend do parceiro confirma o resultado pela API antes de liberar o pedido, independentemente da mensagem exibida no navegador. Um 200/201, a conclusão do desafio ou next_action=null não substituem APPROVED.
7. Pix: gerar cobrança e acompanhar o recebimento
7.1 Criar a cobrança
POST /acquiring/payment-links/public/{public_token}/pix-payments
Authorization: Bearer {access_token}
Idempotency-Key: {chave_do_pix}
Content-Type: application/json
{}O corpo aceita {}. payer, phone e card_setup_id são opcionais conforme o contrato. O Pix usa os requisitos específicos de checkout_requirements_by_payment_method.PIX.
| HTTP | Significado |
|---|---|
201 | Nova cobrança criada |
200 | Resultado de repetição idempotente |
202 | Emissão pendente ou incerta; acompanhar a tentativa existente |
Exemplo ilustrativo de cobrança disponível para pagamento:
Exemplo JSON, conforme o schema CheckoutPixPayment:
{
"id": "44444444-4444-4444-8444-444444444444",
"payment_method": "PIX",
"status": "PENDING_CONFIRMATION",
"amount": "149.90",
"currency": "BRL",
"txid": "IDENTIFICADOR_RETORNADO_PELA_API",
"copy_paste": "PAYLOAD_PIX_REAL_RETORNADO_PELA_API",
"expires_at": "2030-01-15T18:30:00Z",
"approved_at": null,
"poll_after_seconds": 5,
"pix": { "phase": "WAITING_PAYMENT", "processing_reason": null, "can_pay": true },
"recurring_acceptance": null
}O texto PAYLOAD_PIX_REAL_RETORNADO_PELA_API é um marcador. Para exibir o QR Code, renderize o valor real de copy_paste com uma biblioteca QR; a API não devolve uma imagem QR neste contrato. Exiba o código para pagamento somente quando pix.can_pay=true e houver copy_paste.
7.2 Consultar até confirmar
GET /acquiring/payment-links/public/{public_token}/pix-payments/{payment_id}
Authorization: Bearer {access_token}A consulta reconcilia o recebimento e retorna poll_after_seconds, atualmente 5. O pagador autoriza a transferência em sua instituição financeira; emitir o QR Code não realiza essa transferência.
pix.phase | Tratamento da interface |
|---|---|
ISSUING, ISSUANCE_UNKNOWN | Aguardar emissão; não gerar outra chave por resultado incerto |
WAITING_PAYMENT | Mostrar copia e cola/QR quando can_pay=true |
UNDER_REVIEW | Aguardar reconciliação, sem confirmar pagamento |
PAID | Confirmar em conjunto com status=APPROVED |
EXPIRED, FAILED | Parar de oferecer aquele código e apresentar o estado retornado |
Arquivo: browser-pix.mjs
import { wait } from "./browser-3ds.mjs";
// get consulta a MESMA tentativa pelo backend do parceiro; nunca cria outra cobrança.
export async function trackPixPayment({
initial, get, onUpdate = () => {}, onTransientError = () => {}, signal,
sleep = wait, now = Date.now, maxMs = 300000,
}) {
let payment = initial;
const started = now();
while (now() - started < maxMs) {
signal?.throwIfAborted();
onUpdate(payment);
if (payment.status === "APPROVED" && payment.pix?.phase === "PAID") {
return { outcome: "paid", payment };
}
if (["DECLINED", "FAILED", "EXPIRED"].includes(payment.status)) {
return { outcome: "terminal", payment };
}
const suggested = Number(payment.poll_after_seconds);
const seconds = Number.isFinite(suggested) && suggested > 0 ? Math.max(1, suggested) : 5;
await sleep(seconds * 1000, signal);
try { payment = await get(payment.id); } catch (error) {
signal?.throwIfAborted();
if (error.status >= 400 && error.status < 500 && error.status !== 429) throw error;
onTransientError(error);
}
}
return { outcome: "paused", payment };
}Exemplo de interface com Pix copia e cola:
<textarea id="pix-code" readonly aria-label="Pix copia e cola"></textarea>
<button id="copy-pix" type="button">Copiar Pix</button>
<p id="pix-status" role="status" aria-live="polite"></p>import { trackPixPayment } from "./browser-pix.mjs";
const pix = await call("/pix-payments", {
method: "POST", body: {}, idempotencyKey: checkoutState.pixKey,
});
const field = document.getElementById("pix-code");
const copy = document.getElementById("copy-pix");
copy.onclick = () => navigator.clipboard.writeText(field.value);
const result = await trackPixPayment({
initial: pix,
get: id => call(`/pix-payments/${id}`),
onUpdate: payment => {
const canPay = payment.pix.can_pay && Boolean(payment.copy_paste);
field.value = canPay ? payment.copy_paste : "";
field.hidden = !canPay;
copy.disabled = !canPay;
document.getElementById("pix-status").textContent =
payment.status === "APPROVED" && payment.pix.phase === "PAID"
? "Pix confirmado" : `Status: ${payment.pix.phase}`;
},
});
// Se paused, preservar pix.id e oferecer atualização manual do status.O backend também deve acompanhar o pagamento quando o pagador fecha a página. Este contrato oferece consultas; não depende de um webhook de saída para o parceiro. O processamento interno de confirmação do pedido deve evitar efeitos duplicados em consultas repetidas.
8. Recorrência: cartão cadastrado e pagamento são estados diferentes
Na criação do link recorrente, usar billing_type=RECURRING e recurrence.installments, com ao menos duas ocorrências e datas crescentes. Não enviar o amount avulso junto com a recorrência. Crédito deve estar habilitado; para permitir Pix na primeira ocorrência, essa ocorrência deve vencer na data corrente aceita pela API. Consulte as opções de criação com billing_type=RECURRING e leia o checkout_mode retornado, em vez de tentar defini-lo no corpo de criação.
8.1 Início futuro, sem cobrança imediata
Para checkout_mode=CARD_SETUP, seguir tokenização e usar:
POST /acquiring/payment-links/public/{public_token}/card-setups
Authorization: Bearer {access_token}
Idempotency-Key: {chave_da_adesao}
Content-Type: application/jsonO corpo segue os dados do cartão/pagador/browser, com payment_method=CREDIT, installments=1, recurring_consent=true e consent_terms_version obrigatória e não vazia. Enviar a versão dos termos efetivamente apresentados e aceitos pelo pagador. O campo apply_payment_method_to_remaining_installments não é aceito em /card-setups.
Uma resposta 201 pode exigir 3DS. Consultar GET .../card-setups/{setup_id} e continuar Method por POST .../card-setups/{setup_id}/three-ds/continue, com {}.
Para reutilizar o controlador:
const result = await trackCardOperation({
initial: setup,
kind: "card_setup",
get: id => call(`/card-setups/${id}`),
continueThreeDs: (id, body) => call(`/card-setups/${id}/three-ds/continue`, { method: "POST", body }),
controller,
onUpdate: resource => { /* Atualizar a interface da adesão. */ },
});COMPLETED confirma a adesão do cartão, com amount_due_now="0.00". As cobranças futuras pertencem à recorrência. CREATED, AUTHENTICATING, VALIDATING e PENDING_CONFIRMATION são intermediários; FAILED, EXPIRED e CANCELED encerram aquela tentativa sem concluir a adesão.
8.2 Primeira ocorrência por Pix
Nesse fluxo, o link pode estar em modo PAYMENT e ainda exigir uma adesão por /card-setups para as cobranças futuras. Leia checkout_requirements_by_payment_method.PIX.card_setup_required e recurring_acceptance.
As etapas PIX_PAYMENT e CARD_SETUP podem ser iniciadas em qualquer ordem quando card_setup_order=FLEXIBLE. Cada uma tem sua chave idempotente e seu ID. Não conclua a contratação da recorrência apenas porque o Pix foi pago ou o cartão foi cadastrado.
| Campo | Interpretação |
|---|---|
required_steps | Etapas exigidas |
pending_steps | Etapas ainda necessárias |
steps.pix_payment | ID, status/fase, estado da etapa e can_start |
steps.card_setup | ID, status, estado da etapa e can_start |
confirmed=true e status=CONFIRMED | Aceite recorrente ativado |
Não iniciar outra tentativa quando can_start=false. Em expiração/cancelamento antes da ativação, consultar também o contexto refund, quando retornado. Para recorrência iniciada com cobrança por cartão, o pagamento usa /payments com consentimento; /card-setups é o fluxo de adesão sem cobrança imediata.
9. Idempotência e retomada após falhas
Persistir no backend, vinculados ao pedido: payment_link_id, public_token, tipo da operação, chave de idempotência, ID da tentativa, último status e datas de atualização. Não armazenar PAN/CVV como parte do registro de retomada.
Gerar uma chave separada para cada nova operação: tokenização, pagamento com cartão, Pix ou adesão. Gerar e persistir a chave antes da escrita; ao repetir a mesma tentativa, reutilizar a mesma chave e o mesmo payload. Uma consulta ou timeout não deve gerar nova chave.
Pagamento com cartão
GET /acquiring/payment-links/public/{public_token}/payments/by-idempotency-key
Authorization: Bearer {access_token}
Idempotency-Key: {chave_original_do_pagamento}Após recuperar o ID, consultar /payments/{payment_id} e continuar a mesma tentativa. Se uma consulta logo após timeout retornar 404, a escrita original ainda pode estar em andamento: aguardar e consultar novamente, em vez de gerar outra chave.
Pix
GET /acquiring/payment-links/public/{public_token}/pix-payments/by-idempotency-key
Authorization: Bearer {access_token}
Idempotency-Key: {chave_original_do_pix}Essa consulta trata também aliases e reconciliação próprios do Pix. Depois, acompanhar /pix-payments/{payment_id}.
Adesão de cartão
Não existe GET /card-setups/by-idempotency-key. Recuperar pelo setup_id persistido ou por card_setup.id na consulta do link. Enquanto o payload original ainda estiver disponível em memória, a repetição do POST com a mesma chave e payload também pode recuperar a tentativa. Após recarregar a página, preferir os IDs; não armazenar CVV para viabilizar replay.
Uma falha de rede, timeout, HTTP 503 ou pausa da interface não comprova recusa financeira. O gateway não repete chamadas automaticamente. O limite de requisição é de até 29 segundos; corpos estão sujeitos a 16 KB no firewall, token a 4096 bytes e respostas encaminhadas a 4 MB. Use consultas e recuperação idempotente para operações pendentes.
10. Consultas de gestão e pós-pagamento
O backend do parceiro pode acompanhar o resultado por payment_link_id, mesmo sem manter uma página de checkout aberta:
| Método e caminho | Uso |
|---|---|
GET /acquiring/payment-links/ | Listar links conforme filtros do contrato |
GET /acquiring/payment-links/{payment_link_id} | Estado do link, recorrência e checkout_url |
GET /acquiring/payment-links/{payment_link_id}/payments/ | Listar tentativas do link |
GET /acquiring/payment-links/{payment_link_id}/payments/{payment_id} | Detalhe operacional de uma tentativa |
GET /acquiring/payment-links/{payment_link_id}/history/ | Eventos de link, pagamentos, 3DS, adesões, estornos e recorrência |
POST /acquiring/payment-links/{payment_link_id}/payments/{payment_id}/reversal | Solicitar estorno com Idempotency-Key |
POST /acquiring/payment-links/{payment_link_id}/cancel | Cancelar disponibilidade do link |
POST /acquiring/payment-links/{payment_link_id}/agreement/cancel | Cancelar acordo recorrente |
POST /acquiring/payment-links/{payment_link_id}/agreement/installments/{installment_id}/recovery-link | Criar/recuperar link para uma ocorrência |
POST /acquiring/payment-links/{payment_link_id}/agreement/installments/{installment_id}/reprocess | Reprocessar ocorrência com Idempotency-Key |
O histórico usa limit padrão 50, máximo 500, e offset padrão 0. Use-o para explicar eventos; consulte o pagamento para decidir o estado financeiro atual. Estorno pode permanecer pendente e ser acompanhado repetindo o POST idempotente. Reprocessamento é acompanhado pelo pagamento retornado.
O link de recuperação possui outro public_token e usa as mesmas rotas de checkout. Aplicar o novo cartão às próximas ocorrências depende de recovery.can_apply_payment_method_to_remaining_installments e dos campos de consentimento do contrato.
11. Erros e situações que a interface precisa tratar
| Situação | Ação da integração |
|---|---|
| HTTP 400 | Corrigir campos conforme o erro e os requisitos do link; não adicionar campos desconhecidos |
| HTTP 401 | Verificar credencial, expiração, revogação e ambiente; renovar token quando aplicável |
| HTTP 403 | Verificar permissão/configuração; pode haver bloqueio do firewall |
| HTTP 404 | Verificar IDs, ambiente, vínculo do link ao parceiro e escopo publicado |
| HTTP 409 | Consultar a tentativa e o estado do link antes de repetir ou iniciar outra |
| HTTP 429 | Reduzir ritmo e retomar consultas respeitando os intervalos; evitar loops imediatos |
| Timeout / 5xx | Tratar resultado como incerto; recuperar com o ID ou a chave original |
| Desafio fechado ou página recarregada | Retomar a mesma tentativa e executar a próxima ação que a API ainda solicitar |
Dados incompletos de next_action | Preservar tentativa/interface, consultar novamente e acionar suporte se persistir |
Pix sem copy_paste ou can_pay=false | Acompanhar o estado; não oferecer código indisponível para pagamento |
12. Roteiro de homologação da integração
Em HML, com os dados de teste e as habilitações fornecidos para o parceiro, verificar:
- OAuth, criação/consulta de link e rejeição de um link fora do escopo do parceiro.
- Cartão com resultado sem desafio, com Method e com Challenge, incluindo recusa e expiração.
- Manutenção do iframe durante polling, navegação móvel e retomada após recarregar a página.
- Timeout na criação: recuperação da mesma tentativa sem cobrança duplicada.
- Pix emitido, emissão pendente, recebimento confirmado e expiração.
- Adesão sem cobrança imediata e recorrência com primeiro Pix, nas duas ordens de execução.
- Consulta de gestão e confirmação idempotente do pedido no backend, mesmo com a página fechada.
Os exemplos demonstram os contratos e a coordenação do fluxo. A sessão de checkout, persistência dos pedidos, controles de acesso e apresentação visual pertencem à aplicação do parceiro. O estado financeiro retornado pela API permanece a referência para concluir a operação.
Recent Releases
The Basics
Style the widget to match your brand
Connect with third-party services
Manage resources from your terminal
Learn how we secure your data
Troubleshoot common issues
Connect to a storage provider
Updated about 1 hour ago