Docs
Cartões

Exiba o desafio 3DS

Abra o desafio em um iframe quando a cobrança voltar RequiresAction, receba as mensagens do navegador e confirme o resultado pelo servidor.

Quando o emissor pede que o comprador prove ser o dono do cartão (com senha do app do banco ou código por SMS), a criação da cobrança retorna status: "RequiresAction" e um nextAction com a URL do desafio. O seu checkout abre essa URL num iframe; o resto da conversa com o banco acontece lá dentro.

201 Created (trecho)
{
  "id": "e092c6cc-8d6f-47d3-8616-df99670286dd",
  "object": "payment_intent",
  "amount": 10000,
  "currency": "BRL",
  "status": "RequiresAction",
  "statusDetail": "PENDING_CHALLENGE",
  "nextAction": {
    "type": "IFRAME_CHALLENGE",
    "url": "URL_DE_DESAFIO_RETORNADA_PELA_API",
    "authenticationId": "6c868afc-d6bc-45c4-9886-ed97d20a2729",
    "expiresAt": "2026-09-08T15:10:00Z"
  }
}

Use nextAction.url inteira, sem montar a URL a partir do authenticationId.

1. Abra o iframe

Registre o listener de mensagens antes de carregar o iframe, para não perder uma resposta rápida.

checkout.js
const frame = document.createElement('iframe');
frame.title = 'Verificação do pagamento';
frame.style.width = '100%';
frame.style.maxWidth = '420px';
frame.height = '600';

const hooksOrigin = new URL(DM_HOOKS_BASE).origin;

function onChallengeMessage(event) {
  // Aceite só mensagens da Hooks, vindas deste iframe.
  if (event.origin !== hooksOrigin) return;
  if (event.source !== frame.contentWindow) return;

  let msg;
  try {
    msg = JSON.parse(event.data); // a mensagem é uma string JSON
  } catch {
    return;
  }
  if (!msg || typeof msg !== 'object') return;

  if (msg.type === 'card_3ds.processing') {
    showPending(); // não feche o iframe: o resultado final ainda pode chegar
    return;
  }

  if (msg.type === 'card_3ds.succeeded' || msg.type === 'card_3ds.failed') {
    window.removeEventListener('message', onChallengeMessage);
    frame.remove();
    refreshOrderFromYourBackend(paymentIntentId);
  }
}

window.addEventListener('message', onChallengeMessage);
frame.src = nextAction.url;
document.getElementById('challenge-container').appendChild(frame);

showPending e refreshOrderFromYourBackend são funções do seu checkout. A segunda consulta o seu backend, que por sua vez consulta o PaymentIntent na API. O navegador nunca libera o pedido sozinho.

O desafio funciona somente em iframe. Aberto como pop-up ou por redirecionamento da página inteira, o comprador consegue se autenticar, mas o seu checkout não recebe nenhuma mensagem.

2. Interprete as mensagens

Cada mensagem é uma string JSON com três campos: type, authentication_id e detail.

typedetailSignificadoO checkout deve
card_3ds.succeedednullA autenticação terminou.Fechar o iframe e consultar o resultado da cobrança no servidor.
card_3ds.failednullA autenticação falhou, foi recusada ou expirou.Fechar o iframe e consultar o resultado no servidor.
card_3ds.failed{ "reason": "cancelled" }O comprador clicou em Cancelar.Fechar o iframe e oferecer outro meio de pagamento.
card_3ds.processing{ "reason": "awaiting_outcome" }O banco respondeu e o resultado está sendo apurado. Costuma resolver em segundos.Mostrar "processando" sem remover o iframe.
card_3ds.processing{ "reason": "ui_timeout" }Dez minutos se passaram sem retorno do banco. O comprador pode ainda estar digitando.Mostrar "processando" sem remover o iframe.
card_3ds.processing{ "reason": "tail_timeout" }O resultado demorou mais de 90 segundos para ser apurado.Mostrar "processando" e acompanhar a cobrança pelo servidor.

Regras de entrega:

  • Cada desafio envia no máximo uma mensagem final (succeeded ou failed) e no máximo uma processing.
  • processing não encerra o fluxo: uma mensagem final pode chegar depois dela.
  • O authentication_id pode mudar dentro do mesmo pagamento, se o banco pedir um segundo desafio. Não use esse campo para filtrar mensagens.

Autenticação não é pagamento

card_3ds.succeeded diz que o comprador se autenticou, não que o pagamento foi aprovado. O emissor ainda pode recusar a cobrança depois do desafio. O resultado financeiro é sempre o status do PaymentIntent.

3. Confirme pelo servidor

Depois de qualquer mensagem final, o seu backend consulta GET /api/v1/paymentintents/{id} e decide:

statusAção
SucceededConcluir o pedido, de forma idempotente.
FailedLer lastPaymentError e orientar o comprador.
ProcessingManter o pedido em processamento e aguardar webhook ou nova consulta.
RequiresActionO desafio ainda está aberto. Se nextAction.expiresAt não passou, reabra o iframe com a mesma URL.

O webhook payment_intent.succeeded também confirma a aprovação. Não há webhook específico para o desafio 3DS.

Comprador que sai e volta

Fechar o iframe ou recarregar a página não cancela a cobrança. Quando o comprador voltar ao checkout, consulte o PaymentIntent: se ele continuar RequiresAction com nextAction.expiresAt no futuro, reabra o desafio com a mesma nextAction.url. Se já tiver um desfecho, siga por ele.

Nesta página