Skip to Content
ConceitosReembolsos

Reembolsos

Um Refund é uma entidade de primeira classe, não uma flag no Order. Você pode emitir reembolsos parciais, vários reembolsos contra o mesmo Order, ou reembolso + nova cobrança no mesmo fluxo.

Duas formas pelas quais o registro de reembolso pode passar a existir:

FluxoQuem preenche o formulárioAuthCai em
Iniciado pelo lojistaSeu dashboard / seu backendHMAC (sk_…)APPROVED imediatamente
Iniciado pelo clienteO comprador, na nossa página hospedadaToken de uso único (sem credenciais)PENDING — você aprova, ou cai direto se a sua config faz auto-aprovação

O fluxo iniciado pelo cliente usa um token de pedido de reembolso de curta duração. Você cria um token (B2B ou dashboard), entrega a URL para o comprador como preferir, e o comprador completa os detalhes do reembolso em checkout.infraio.xyz/refund-request/:token. O comprador nunca toca na sua API e nunca vê a sua chave de lojista.

Ciclo de vida do reembolso

EstadoSignifica
PENDINGReembolso registrado, aguardando aprovação. Reembolsos iniciados pelo cliente sempre começam aqui.
APPROVEDLiberado para execução. Reembolsos iniciados pelo lojista pulam direto para cá.
REJECTEDReembolso negado. Status do Order não muda.
EXECUTEDTransferência on-chain confirmada. O Order vai para PARTIALLY_REFUNDED / REFUNDED.

Iniciado pelo lojista

Você decide reembolsar (por exemplo, o comprador reclamou no chat). Chame o endpoint de iniciado-pelo-lojista — ele pula a revisão e cai em APPROVED imediatamente.

POST /b2b/v1/merchants/{merchant_id}/refunds { "order_id": "ord_01J5K…", "amount": "49.00", // parcial ou total, na moeda de exibição do pedido "reason": "customer complaint #4521", "refund_to_address": "0xBUYER…", // obrigatório para trilhos cripto "refund_network": "polygon", // slug da rede; veja Conceitos → Redes "refund_token_address":"0xUSDC_CONTRACT" // contrato ERC-20 devolvido; geralmente o token original }

Não há campo currency na requisição de reembolso — reembolsos sempre herdam a moeda de exibição do pedido (USD hoje). O trio (refund_to_address, refund_network, refund_token_address) é o destino on-chain; o payment-service usa para conduzir a saga cripto. São ignorados para trilhos fiat (roteamento automático pelo provedor).

O pedido mantém o status atual até você executar a transferência on-chain (veja Executando um reembolso cripto).


Iniciado pelo cliente — tokens de pedido de reembolso

O comprador preenche o formulário de reembolso na nossa página hospedada, não na sua. O seu único trabalho é criar um token e entregar a URL.

Ciclo de vida do token

EstadoSignificaURL para o cliente renderiza
ACTIVEToken está vivo, now < expires_atO formulário de reembolso (refund_to_address, reason, amount, nota opcional → metadata.note)
SUBMITTEDComprador completou o formulário; existe um registro de RefundCard de status espelhando /r/:linkToken
EXPIRED_UNUSEDTTL passou antes do comprador enviarMensagem: “Este link expirou. Peça um novo”
RENEWAL_REQUESTEDComprador pediu um link novoAviso de espera: “O lojista foi notificado”
RENEWEDLojista aprovou a renovação e emitiu um substituto”Este link foi substituído — confira seu e-mail para o novo” (o novo token não é revelado aqui, para evitar ataques de link encaminhado)
CANCELEDLojista revogou o token pelo dashboard”Este pedido de reembolso foi cancelado”

Tokens são de uso único. Uma vez SUBMITTED, a URL continua válida para o comprador checar o status, mas não pode ser usada para submeter de novo. Para emitir um segundo reembolso contra o mesmo pedido, crie um token novo.

Padrões de TTL

Origem da criaçãoTTL padrãoPor quê
POST /b2b/v1/merchants/{merchant_id}/refund-requests (HMAC)30 minutosProgramático — pressupõe entrega imediata ao comprador.
POST /payment/v1/merchants/{merchant_id}/refund-requests (dashboard JWT)24 horasManual — o lojista cola a URL em um e-mail / SMS.

Os dois endpoints aceitam um campo ttl_seconds no body se você quiser sobrescrever. Não há um limite rígido min/máx aplicado no servidor hoje — valores comuns vão de 1 minuto a 7 dias. Fique nessa faixa para não surpreender compradores nem segurar capacidade em tokens cancelados.

Criação via API B2B

Para backends que querem gerar um link de reembolso programaticamente logo após uma conversa de suporte, um fluxo de cancelamento de pedido, etc.

POST /b2b/v1/merchants/{merchant_id}/refund-requests Content-Type: application/json X-Client-ID: pk_live_… X-Timestamp: 1729536000 X-Signature: 4f2a1b9c8d3e2f1a0b9c8d7e6f5a4b3c… { "ref_type": "order_id", // obrigatório: order_id | order_number | session_id | session_key "ref_value": "ord_01J5K…", // obrigatório: combina com ref_type "amount": "49.00", // obrigatório — trava o máximo que o comprador pode submeter "ttl_seconds": 1800, // opcional — padrão de 1800 (30 min) "metadata": { "support_ticket": "4521" }, // opcional — chave/valor estilo Stripe "hide_summary": false, // flags opcionais de UI para o formulário hospedado "hide_header": false }

A assinatura da requisição B2B é hex bruto minúsculo sem prefixo sha256= — esse prefixo só aparece em assinaturas de webhook de entrada (Infraio → seu servidor). A string de assinatura B2B de saída é METHOD\nPATH\nTIMESTAMP\nBODY; veja Autenticação para o algoritmo canônico.

O valor está no body de criação e é obrigatório. Ele trava o teto que o comprador pode submeter no formulário — ele pode submeter por menos, nunca por mais. (Para reembolsos parciais, crie um token com o valor parcial; para reembolsos totais, crie com o total do pedido.)

O formato legado { "order_id": "..." } ainda é aceito por compatibilidade — internamente é mapeado para (ref_type=order_id, ref_value=...) — mas integrações novas devem usar o par explícito ref_type + ref_value.

Resposta:

{ "token": "rfqt_01J7P3Q9R…", "refund_url": "https://checkout.infraio.xyz/refund-request/rfqt_01J7P3Q9R…", "expires_at": "2026-05-28T10:32:00Z" }

Dispara refund_request.created para os seus endpoints de webhook (para você poder logar / auditar qual token está ativo no momento para um pedido).

Criação via dashboard

O modal Issue Refund no dashboard do lojista  expõe um toggle: Execute now vs Send link to customer. Escolher o segundo chama POST /payment/v1/merchants/{merchant_id}/refund-requests por baixo dos panos (autenticado por JWT, com o mesmo formato de body do B2B acima) e então mostra para você a URL com um botão de copiar e um QR code. Cole no canal que fizer sentido — e-mail, chat de suporte, SMS.

Via o SDK JavaScript — openRefundRequest

Se você já tem @lartech/infraio-checkout-js na sua stack e quer que o comprador complete o reembolso dentro do fluxo da sua página (e não via URL externa), combine a criação B2B com sdk.openRefundRequest():

// Lado do servidor: cria o token const { token } = await fetch("/api/mint-refund-token", { method: "POST" }).then(r => r.json()); // Lado do cliente: abre o formulário hospedado const sdk = await loadInfraIo("pk_live_yourkeyhere"); const close = sdk.openRefundRequest({ token, mode: "popup", // ou "redirect" | "embed" onSuccess: ({ linkToken, refundId }) => { // linkToken → página de status /r/:linkToken para o comprador. // refundId → referência da API B2B para aprovar / rejeitar. window.location.href = `/r/${linkToken}`; }, onCancel: () => { /* comprador fechou o popup */ }, onError: (err) => { /* veja a referência do SDK */ }, });

Veja a referência do SDK → sdk.openRefundRequest() para a tabela completa de opções.

Renovação pelo cliente — re-emissão dirigida pelo comprador

Se o comprador abre a URL depois do token expirado, a página oferece um botão Pedir link novo no lugar do formulário. Clicar nele:

  1. Faz POST para /pub/v1/refund-requests/:token/request-renewal (sem credenciais — o próprio token é a fonte da verdade)
  2. Opcionalmente captura uma nota em texto livre (customer_note) que o comprador pode deixar para o lojista
  3. Move o token para RENEWAL_REQUESTED e dispara refund_request.renewal_requested para o seu webhook

Seu dashboard mostra um badge no widget de pedidos de renovação. Aprove (um clique) e o sistema cria um token ACTIVE novo, dispara refund_request.renewed e te deixa copiar a URL nova para enviar de novo. A URL antiga continua acessível, mas renderiza “Substituído — confira seu e-mail” para que uma cópia encaminhada da URL antiga não consiga pescar a nova.


Executando um reembolso cripto

A API registra a intenção — ela não move fundos. Você assina e transmite a transferência on-chain da carteira do seu lojista, e depois estampa o tx hash de volta no registro de reembolso:

POST /b2b/v1/refunds/:refund_id/submit-tx Content-Type: application/json { "tx_hash": "0xabcd…", "network": "ethereum", "token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }

Os três campos do body são obrigatórios: o mesmo tx hash pode existir em redes diferentes, e você pode reembolsar em uma stablecoin diferente daquela em que o pagamento original foi capturado.

Quando o watcher da rede vê essa tx atingir a contagem de confirmações configurada (veja Redes e ativos), o reembolso vai para EXECUTED e o total reembolsado do Order é atualizado.

A gente deliberadamente não retém custódia dos fundos do lojista, o que significa que não podemos executar reembolsos por você. Incorpore o envio on-chain ao seu ferramental de admin — eth_sendRawTransaction a partir de um multisig ou hot wallet, com um workflow que termina postando o tx hash na API de reembolso.


Eventos de webhook

O subsistema de reembolsos dispara duas famílias de eventos:

Ciclo de vida do token (refund_request.*)

EventoDispara quando
refund_request.createdUm token foi criado — data.source é b2b / dashboard / renewal
refund_request.renewal_requestedUm comprador clicou em “Pedir link novo” depois que o token expirou. Inscreva-se neste — é o sinal de que o lojista precisa agir.
refund_request.renewedVocê aprovou uma renovação e um novo token substituiu o antigo. data.old_token / data.new_token formam a cadeia de auditoria.
refund_request.canceledVocê moveu um token para CANCELED pelo dashboard. Idempotente — só a primeira transição emite. data.reason é a nota opcional do lojista.

Ciclo de vida do reembolso (payment.refund.*)

EventoDispara quando
payment.refund.requestedUm novo registro de Refund existe — qualquer origem (submit de formulário, API iniciada pelo lojista, dashboard).
payment.refund.approvedO reembolso foi aprovado — auto-aprovado (iniciado pelo lojista) ou depois que você chama /approve em um pendente.
payment.refund.rejectedVocê chamou /reject em um reembolso pendente.
payment.refund.executedFundos se moveram (seu tx hash cripto bateu as confirmações exigidas).

payment.failed não dispara para um reembolso — reembolsos têm a própria série de eventos com prefixo payment.refund.*.

Próximos passos