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:
| 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; 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
| 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 Refund | 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. |
POST /payment/v1/merchants/{merchant_id}/refund-requests (dashboard JWT) | 24 horas | Manual — 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:
- Faz POST para
/pub/v1/refund-requests/:token/request-renewal(sem credenciais — o próprio token é a fonte da verdade) - Opcionalmente captura uma nota em texto livre (
customer_note) que o comprador pode deixar para o lojista - 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 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.*)
| 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.