Skip to Content
ConceitosReembolsos
View as Markdown

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.

Os reembolsos também podem ser emitidos pelo app do lojista.

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. 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 reembolsoCard 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.
Dashboard do lojista24 horasManual — o lojista cola a URL em um e-mail / SMS.

Você pode sobrescrever o padrão com o campo ttl_seconds no body. Nenhum mínimo ou máximo é aplicado; valores comuns vão de 1 minuto a 7 dias.

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 mais antigo { "order_id": "..." } ainda é aceito e é tratado como ref_type=order_id, 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 cria um token de pedido de reembolso (igual à chamada B2B acima) e 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. Envia o pedido de renovação (sem credenciais; o próprio link o autoriza)
  2. Opcionalmente captura uma nota em texto livre (customer_note) que o comprador pode deixar para você
  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 um token ACTIVE novo é emitido, 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 a InfraIO Pay vê essa transação atingir a contagem de confirmações necessária (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.

Reembolsos em TRON, Solana e TON

O fluxo é o mesmo: você envia o reembolso da sua própria carteira e depois envia o hash da transação. Os detalhes seguem a rede:

  • A tela de reembolso no dashboard mostra o destino, o valor, a rede e o token, além de um QR code quando a rede oferece suporte: um QR do Solana Pay na Solana e um link de transferência TON na TON. Na TRON, mostra o endereço de destino para copiar (nenhum link de carteira carrega o valor), então informe o valor você mesmo.
  • token_address é o endereço do token nessa rede: o contrato TRC-20, o mint SPL ou o endereço do Jetton master.
  • Os formatos do hash da transação diferem: hex puro na TRON, uma assinatura base58 na Solana e um hash hex ou base64 na TON.
  • A plataforma verifica essa transação exata on-chain e então move o reembolso para EXECUTED, usando as contagens de confirmação de Redes e ativos.

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