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:
| Fluxo | Quem preenche o formulário | Auth | Cai em |
|---|---|---|---|
| Iniciado pelo lojista | Seu dashboard / seu backend | HMAC (sk_…) | APPROVED imediatamente |
| Iniciado pelo cliente | O comprador, na nossa página hospedada | Token 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
| Estado | Significa |
|---|---|
PENDING | Reembolso registrado, aguardando aprovação. Reembolsos iniciados pelo cliente sempre começam aqui. |
APPROVED | Liberado para execução. Reembolsos iniciados pelo lojista pulam direto para cá. |
REJECTED | Reembolso negado. Status do Order não muda. |
EXECUTED | Transferê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
| Estado | Significa | URL para o cliente renderiza |
|---|---|---|
ACTIVE | Token está vivo, now < expires_at | O formulário de reembolso (refund_to_address, reason, amount, nota opcional → metadata.note) |
SUBMITTED | Comprador completou o formulário; existe um registro de reembolso | Card de status espelhando /r/:linkToken |
EXPIRED_UNUSED | TTL passou antes do comprador enviar | Mensagem: “Este link expirou. Peça um novo” |
RENEWAL_REQUESTED | Comprador pediu um link novo | Aviso de espera: “O lojista foi notificado” |
RENEWED | Lojista 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) |
CANCELED | Lojista 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ção | TTL padrão | Por quê |
|---|---|---|
POST /b2b/v1/merchants/{merchant_id}/refund-requests (HMAC) | 30 minutos | Programático — pressupõe entrega imediata ao comprador. |
| Dashboard do lojista | 24 horas | Manual — 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:
- Envia o pedido de renovação (sem credenciais; o próprio link o autoriza)
- Opcionalmente captura uma nota em texto livre (
customer_note) que o comprador pode deixar para você - Move o token para
RENEWAL_REQUESTEDe dispararefund_request.renewal_requestedpara 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.*)
| Evento | Dispara quando |
|---|---|
refund_request.created | Um token foi criado — data.source é b2b / dashboard / renewal |
refund_request.renewal_requested | Um comprador clicou em “Pedir link novo” depois que o token expirou. Inscreva-se neste — é o sinal de que o lojista precisa agir. |
refund_request.renewed | Você aprovou uma renovação e um novo token substituiu o antigo. data.old_token / data.new_token formam a cadeia de auditoria. |
refund_request.canceled | Você 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.*)
| Evento | Dispara quando |
|---|---|
payment.refund.requested | Um novo registro de Refund existe — qualquer origem (submit de formulário, API iniciada pelo lojista, dashboard). |
payment.refund.approved | O reembolso foi aprovado — auto-aprovado (iniciado pelo lojista) ou depois que você chama /approve em um pendente. |
payment.refund.rejected | Você chamou /reject em um reembolso pendente. |
payment.refund.executed | Fundos 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
- Referência do SDK →
sdk.openRefundRequest()— abre o formulário hospedado de reembolso como popup / redirect / embed. - Referência da API → Reembolsos — catálogo de endpoints (criação, submit, renovação, status).
- Conceitos → Pedidos — como o estado do Refund liga de volta ao ciclo de vida do Order.
- Webhooks → Visão geral — catálogo completo de eventos.