Welcome to AlliancePay

📘

AlliancePay - Partner Guide


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

AmbienteBase da APIToken OAuth
Homologaçãohttps://api-hml.alliancepay.appPOST /oauth/token
Produçãohttps://api.alliancepay.appPOST /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

IdentificadorOrigemUso
access_tokenPOST /oauth/tokenAutenticar o parceiro no gateway
payment_link_idid da criação/detalhe do linkGestão do link, pagamentos e histórico
public_tokenÚltimo segmento do pathname de checkout_urlIdentificar o link nas rotas de checkout
tokenization_idid da tokenizaçãoConfirmar e consultar a tokenização
payment_method_idTokenização com status READYReferenciar o cartão na criação do pagamento/adesão
payment_idid da tentativa de cartão ou PixConsultar o pagamento e continuar seu 3DS
setup_idid da adesão de cartãoConsultar a adesão e continuar seu 3DS
Idempotency-KeyGerada e preservada pelo parceiroIdentificar 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/json

Exemplo 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, status e expires_at: disponibilidade do link.
  • payment_methods e max_installments: opções permitidas.
  • checkout_mode: PAYMENT para pagamento ou CARD_SETUP para adesão sem cobrança imediata.
  • checkout_requirements e checkout_requirements_by_payment_method: campos exigidos do pagador.
  • card_setup, recurrence, recovery e recurring_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 parceiroOperação AlliancePay
GET /api/checkout/{pedido}GET /acquiring/payment-links/public/{public_token}
POST /api/checkout/{pedido}/card-tokenizationsPOST .../card-tokenizations
POST /api/checkout/{pedido}/card-tokenizations/{id}/confirmPOST .../card-tokenizations/{id}/confirm
GET /api/checkout/{pedido}/card-tokenizations/{id}GET .../card-tokenizations/{id}
POST /api/checkout/{pedido}/paymentsPOST .../payments
GET /api/checkout/{pedido}/payments/{id}GET .../payments/{id}
GET /api/checkout/{pedido}/payments/by-idempotency-keyGET .../payments/by-idempotency-key
POST /api/checkout/{pedido}/payments/{id}/three-ds/continuePOST .../payments/{id}/three-ds/continue
POST /api/checkout/{pedido}/pix-paymentsPOST .../pix-payments
GET /api/checkout/{pedido}/pix-payments/{id}GET .../pix-payments/{id}
GET /api/checkout/{pedido}/pix-payments/by-idempotency-keyGET .../pix-payments/by-idempotency-key
POST /api/checkout/{pedido}/card-setupsPOST .../card-setups
GET /api/checkout/{pedido}/card-setups/{id}GET .../card-setups/{id}
POST /api/checkout/{pedido}/card-setups/{id}/three-ds/continuePOST .../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/json

Exemplo 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/json

Exemplo 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çãoComo implementarPróxima chamada
THREE_DS_METHODFormulá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_CHALLENGEFormulá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_DECOUPLEDMostrar que a autenticação está pendente fora da página.Consultar respeitando next_action.poll_after_seconds.
Ausente ou nullLer 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 pagamentoTratamento
CREATED, AUTHENTICATINGTentativa em andamento; executar next_action quando presente
PROCESSING, PENDING_CONFIRMATIONAguardar e consultar; resultado ainda pendente
APPROVEDPagamento aprovado; confirmar o pedido no backend de forma idempotente
DECLINEDPagamento recusado; apresentar mensagem apropriada
FAILEDTentativa falhou; considerar failure_code/failure_message retornados
EXPIREDTentativa 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.

HTTPSignificado
201Nova cobrança criada
200Resultado de repetição idempotente
202Emissã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.phaseTratamento da interface
ISSUING, ISSUANCE_UNKNOWNAguardar emissão; não gerar outra chave por resultado incerto
WAITING_PAYMENTMostrar copia e cola/QR quando can_pay=true
UNDER_REVIEWAguardar reconciliação, sem confirmar pagamento
PAIDConfirmar em conjunto com status=APPROVED
EXPIRED, FAILEDParar 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/json

O 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.

CampoInterpretação
required_stepsEtapas exigidas
pending_stepsEtapas ainda necessárias
steps.pix_paymentID, status/fase, estado da etapa e can_start
steps.card_setupID, status, estado da etapa e can_start
confirmed=true e status=CONFIRMEDAceite 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 caminhoUso
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}/reversalSolicitar estorno com Idempotency-Key
POST /acquiring/payment-links/{payment_link_id}/cancelCancelar disponibilidade do link
POST /acquiring/payment-links/{payment_link_id}/agreement/cancelCancelar acordo recorrente
POST /acquiring/payment-links/{payment_link_id}/agreement/installments/{installment_id}/recovery-linkCriar/recuperar link para uma ocorrência
POST /acquiring/payment-links/{payment_link_id}/agreement/installments/{installment_id}/reprocessReprocessar 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çãoAção da integração
HTTP 400Corrigir campos conforme o erro e os requisitos do link; não adicionar campos desconhecidos
HTTP 401Verificar credencial, expiração, revogação e ambiente; renovar token quando aplicável
HTTP 403Verificar permissão/configuração; pode haver bloqueio do firewall
HTTP 404Verificar IDs, ambiente, vínculo do link ao parceiro e escopo publicado
HTTP 409Consultar a tentativa e o estado do link antes de repetir ou iniciar outra
HTTP 429Reduzir ritmo e retomar consultas respeitando os intervalos; evitar loops imediatos
Timeout / 5xxTratar resultado como incerto; recuperar com o ID ou a chave original
Desafio fechado ou página recarregadaRetomar a mesma tentativa e executar a próxima ação que a API ainda solicitar
Dados incompletos de next_actionPreservar tentativa/interface, consultar novamente e acionar suporte se persistir
Pix sem copy_paste ou can_pay=falseAcompanhar 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:

  1. OAuth, criação/consulta de link e rejeição de um link fora do escopo do parceiro.
  2. Cartão com resultado sem desafio, com Method e com Challenge, incluindo recusa e expiração.
  3. Manutenção do iframe durante polling, navegação móvel e retomada após recarregar a página.
  4. Timeout na criação: recuperação da mesma tentativa sem cobrança duplicada.
  5. Pix emitido, emissão pendente, recebimento confirmado e expiração.
  6. Adesão sem cobrança imediata e recorrência com primeiro Pix, nas duas ordens de execução.
  7. 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


Did this page help you?