Pular para o conteúdo principal
Use esta página quando você aceita cartão com Payment Element e precisa de um modelo confiável para three_ds_required, next_action e liquidação final.

O que o 3DS muda no fluxo

O 3DS adiciona uma etapa assíncrona de autenticação do portador entre o envio do cartão e o resultado final do pagamento. Isso significa que:
  • o front-end pode precisar pausar para um desafio do banco emissor
  • o back-end continua criando a transação normalmente
  • o pedido deve permanecer pendente até que webhook ou reconciliação confirmem o estado final

Modelo operacional recomendado

Regras de tratamento de estado

EstadoSignificadoO que o seu sistema deve fazer
pendingo fluxo de cartão começou, mas ainda não terminoumantenha o checkout em espera
three_ds_requireda autenticação do emissor ainda é necessáriadeixe o Payment Element continuar o desafio
authorizedo emissor aprovou, mas sua regra de negócio pode ainda esperar outro estadomantenha pendente, salvo se sua operação libera em autorização
capturedsucesso final seguro para o lojistalibere o pedido
refusedo emissor ou adquirente recusou a cobrançaofereça nova tentativa ou outro meio de pagamento
canceleda tentativa foi cancelada durante o fluxoencerre a tentativa e permita nova cobrança
Não trate three_ds_required como pagamento concluído.

Padrão de front-end

Sempre que possível, deixe elements.submit() orquestrar o 3DS automaticamente.
const result = await elements.submit({
	createTransaction: async (tokenData) => {
		const response = await fetch("/api/pay", {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({
				token: tokenData.token,
				amount: 2490,
				installments: 1,
				orderId: "order_2001"
			})
		});

		return response.json();
	}
});

if (result.status === "error") {
	showCheckoutError(result.error?.message ?? "Pagamento falhou");
	return;
}

showPendingState("Estamos confirmando o seu pagamento");

O que o navegador deve fazer

  • exibir estado de envio ou autenticação enquanto elements.submit() estiver rodando
  • bloquear envios duplicados durante o desafio
  • tratar a resposta imediata como provisória quando o estado ainda não for terminal
  • levar o comprador para uma tela de pendência caso o desafio termine, mas a liquidação continue assíncrona

Padrão de back-end

O back-end deve continuar simples: criar a transação, devolver a resposta da Pagou e deixar o webhook decidir a liberação final.
app.post("/api/pay", async (req, res) => {
	const { token, amount, installments, orderId } = req.body;

	const response = await fetch("https://api-sandbox.pagou.ai/v2/transactions", {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
			Authorization: `Bearer ${process.env.PAGOU_TOKEN}`,
		},
		body: JSON.stringify({
			external_ref: orderId,
			amount,
			currency: "BRL",
			method: "credit_card",
			token,
			installments,
			buyer: {
				name: "Ada Lovelace",
				email: "ada@example.com",
				document: {
					type: "CPF",
					number: "12345678901",
				},
			},
			products: [{ name: "Plan upgrade", price: amount, quantity: 1 }],
		}),
	});

	const payload = await response.json();

	await savePaymentAttempt({
		orderId,
		transactionId: String(payload.data?.id ?? ""),
		status: payload.data?.status ?? "unknown",
	});

	res.json(payload);
});

Webhook como dono do estado final

O consumidor de webhook deve ser a fonte de verdade para liberação do pedido.
export async function handlePaymentWebhook(event) {
	await markWebhookReceived(event.id);

	const transaction = await pagouClient.transactions.retrieve(String(event.data.id), {
		requestId: `webhook_${event.id}`,
	});

	switch (transaction.data.status) {
		case "captured":
		case "paid":
			await markOrderAsPaid(transaction.data.external_ref);
			break;
		case "refused":
		case "canceled":
		case "expired":
			await markOrderAsFailed(transaction.data.external_ref);
			break;
		default:
			await keepOrderPending(transaction.data.external_ref);
	}
}

Falhas que você deve prever

  • O cliente fecha a janela do desafio: mantenha o pedido pendente e reconcilie ou aguarde o webhook.
  • O navegador perde a resposta após o desafio: consulte GET /v2/transactions/{id} antes de pedir nova tentativa.
  • O emissor recusa após o 3DS: mostre uma falha recuperável e permita outro cartão ou nova tentativa.
  • O webhook chega antes de o fluxo do navegador terminar: confie no estado do back-end, não na sessão do browser.

Checklist de produção

  • Use um external_ref estável por tentativa de pedido.
  • Armazene o ID da transação da Pagou retornado pelo back-end.
  • Desabilite envio duplicado enquanto o desafio estiver em andamento.
  • Libere pedido apenas em estado final como captured ou paid.
  • Reconcilie estados incertos com GET /v2/transactions/{id}.

Páginas relacionadas