Checkout no iframe

Incorpore o checkout no seu site, com as suas cores, e reaja ao pagamento sem redirecionar.

Em vez de mandar o cliente para a url do link, você pode abrir o checkout dentro da sua página com um <iframe>. O cliente paga sem sair do seu site, e o checkout avisa a sua página por postMessage quando carregou, quando muda de altura e quando o pagamento termina.

Veja funcionando

A loja de exemplo storetest.xpayz.us tem o código completo desta página: cole o id de um link de sandbox, troque as cores e acompanhe os eventos ao vivo.

Como funciona

  1. 1
    Crie o link
    POST /links devolve a url do checkout.
  2. 2
    Abra no iframe
    Acrescente ?embed=1 (e suas cores) à url e use como src.
  3. 3
    Escute os eventos
    Ajuste a altura no resize e reaja a success e error.

Crie o link no seu servidor, como em qualquer integração, e entregue a url para a página:

curl -X POST "$XPAYZ_URL/links" \
  -H "Content-Type: application/json" \
  -H "X-Secret-ID: $XPAYZ_SECRET_ID" \
  -H "X-Secret-Key: $XPAYZ_SECRET_KEY" \
  -d '{ "description": "Pedido #123", "amount": "100.00", "external_ref": "pedido-123" }'
200 OK
{
  "id": "b3e1c2d4-5f67-4890-9abc-1234567890ab",
  "url": "https://checkout.xpayz.us/p/b3e1c2d4-5f67-4890-9abc-1234567890ab"
}

Nunca crie o link pelo navegador: isso exporia a sua X-Secret-Key.

Monte a URL do iframe

Use a url devolvida pela API, sem montar o domínio na mão, e acrescente os parâmetros:

ParâmetroValoresPara quê
embed1Obrigatório. Ativa o modo iframe: layout sem moldura e envio dos eventos.
langpt, en, esIdioma do checkout. Padrão no iframe: en.
primaryhex de 6 dígitos, sem #Cor dos botões e destaques. Ex.: 7c3aed.
bghex de 6 dígitos, sem #Cor de fundo. Fundos escuros ativam o tema escuro automaticamente.
const src = new URL(link.url);
src.searchParams.set('embed', '1');
src.searchParams.set('lang', 'pt');
src.searchParams.set('primary', '7c3aed');
src.searchParams.set('bg', 'ffffff');
// https://checkout.xpayz.us/p/b3e1...?embed=1&lang=pt&primary=7c3aed&bg=ffffff

Cores inválidas são ignoradas e o checkout usa as cores padrão.

Coloque o iframe na página

<iframe
  id="xpayz-checkout"
  src="https://checkout.xpayz.us/p/b3e1...?embed=1&lang=pt"
  allow="payment"
  style="width: 100%; height: 560px; border: 0; transition: height .15s ease"
></iframe>
  • allow="payment" libera os meios de pagamento do navegador dentro do iframe.
  • Comece com uma altura fixa (560px). A altura real chega depois, pelo evento resize.

Escute os eventos

const frame = document.getElementById('xpayz-checkout');
const checkoutOrigin = new URL(frame.src).origin;

window.addEventListener('message', (event) => {
  // 1. Só aceite mensagens do iframe do checkout
  if (event.origin !== checkoutOrigin) return;
  if (event.source !== frame.contentWindow) return;

  const data = event.data;
  if (!data || data.source !== 'paggo-checkout') return;

  switch (data.event) {
    case 'resize':
      // 2. Acompanhe a altura do conteúdo: sem barra de rolagem nem espaço em branco
      if (typeof data.height === 'number') frame.style.height = `${data.height}px`;
      break;
    case 'ready':
      // checkout carregado: esconda o seu loading
      break;
    case 'success':
      // pagamento recebido: mostre a confirmação
      break;
    case 'error':
      // data.code: payment_failed | link_expired | link_paid
      break;
  }
});

Eventos

Toda mensagem é um objeto com source: "paggo-checkout" e um event:

eventQuandoCampos
readyO checkout carregou.paymentId
resizeA altura do conteúdo mudou. Chega várias vezes.height (px, número)
successO pagamento foi recebido.paymentId, amount, currency
errorO pagamento não pode ser concluído.paymentId, code, reason (opcional)

paymentId é o id do link (o mesmo devolvido em POST /links), não o ID de um pagamento. amount não vem em links sem valor fixo.

Códigos de error

codeSignificadoSugestão
payment_failedO pagamento foi recusado ou cancelado.Para uma nova tentativa, recarregue o iframe (veja abaixo).
link_expiredO link passou da expiration.Crie um link novo.
link_paidO link já foi pago.Mostre o pedido como pago ou crie outro link.

ready, success e error são enviados uma vez por carregamento do iframe. Depois de um error, se o cliente tentar de novo dentro do mesmo iframe, o success dessa nova tentativa não chega. Para uma nova tentativa, recarregue o iframe (por exemplo, frame.src = frame.src). O webhook continua valendo para qualquer tentativa.

Exemplos de mensagens
{ "source": "paggo-checkout", "event": "ready", "paymentId": "b3e1c2d4-5f67-4890-9abc-1234567890ab" }
{ "source": "paggo-checkout", "event": "resize", "height": 812 }
{ "source": "paggo-checkout", "event": "success", "paymentId": "b3e1c2d4-...", "amount": "100", "currency": "BRL" }
{ "source": "paggo-checkout", "event": "error", "paymentId": "b3e1c2d4-...", "code": "payment_failed" }

A altura chega depois

A comunicação com o iframe é assíncrona: no load a altura certa ainda não existe. O checkout mede o próprio conteúdo e manda resize sempre que ele muda (ao trocar de Pix para cartão, ao mostrar o QR Code, ao exibir um erro). Por isso:

  • Sempre trate o resize. Sem ele o iframe fica cortado ou com espaço em branco.
  • Dê uma altura inicial (560px) para a página não "pular" enquanto a primeira medida não chega.
  • Não use height: 100vh nem scrolling="no" sem tratar o resize.

Segurança

success não é confirmação de pagamento

Mensagens do navegador servem para a experiência (mostrar "pagamento recebido"). Para liberar o pedido, confirme no servidor pelo webhook event.completed ou consultando os pagamentos do link.

  • Valide event.origin e event.source antes de ler a mensagem. O campo source: "paggo-checkout" pode ser enviado por qualquer página, então ele sozinho não prova nada.
  • Não use valores da mensagem em innerHTML. Use textContent.

Exemplo em React

XPayZCheckout.tsx
import { useEffect, useRef, useState } from 'react';

type CheckoutEvent =
  | { event: 'ready'; paymentId: string }
  | { event: 'resize'; height: number }
  | { event: 'success'; paymentId: string; amount?: string; currency?: string }
  | { event: 'error'; paymentId: string; code: 'payment_failed' | 'link_expired' | 'link_paid'; reason?: string };

export function XPayZCheckout({
  url,
  lang = 'pt',
  primary,
  onSuccess,
  onError,
}: {
  url: string; // a `url` devolvida por POST /links
  lang?: 'pt' | 'en' | 'es';
  primary?: string; // hex sem #
  onSuccess?: (e: Extract<CheckoutEvent, { event: 'success' }>) => void;
  onError?: (e: Extract<CheckoutEvent, { event: 'error' }>) => void;
}) {
  const frame = useRef<HTMLIFrameElement>(null);
  const [height, setHeight] = useState(560);

  const src = new URL(url);
  src.searchParams.set('embed', '1');
  src.searchParams.set('lang', lang);
  if (primary) src.searchParams.set('primary', primary);

  useEffect(() => {
    const origin = src.origin;

    function onMessage(event: MessageEvent) {
      if (event.origin !== origin || event.source !== frame.current?.contentWindow) return;
      const data = event.data as CheckoutEvent & { source?: string };
      if (data?.source !== 'paggo-checkout') return;

      if (data.event === 'resize') setHeight(data.height);
      if (data.event === 'success') onSuccess?.(data);
      if (data.event === 'error') onError?.(data);
    }

    window.addEventListener('message', onMessage);
    return () => window.removeEventListener('message', onMessage);
  }, [src.origin, onSuccess, onError]);

  return (
    <iframe
      ref={frame}
      src={src.toString()}
      allow="payment"
      style={{ width: '100%', height, border: 0, transition: 'height .15s ease' }}
    />
  );
}

Testando

Use um link criado no sandbox e os dados de Simular pagamentos dentro do iframe. Com 4200 0000 0000 0000 você recebe success; com 4200 0000 0000 0001, error com payment_failed.

Nesta página