<!-- Source: https://docs.infraio.xyz/pt-BR/concepts/refunds -->
<!-- Last updated: 2026-10-04 -->

# 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.

> **Note:**
>
> Os reembolsos também podem ser emitidos pelo [app do lojista](https://docs.infraio.xyz/pt-BR/get-started/merchant-app).

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

```mermaid
stateDiagram-v2
    [*] --> PENDING:  reembolso criado (submit do cliente ou fluxo-cliente B2B)
    PENDING --> APPROVED: passa na revisão (auto para iniciado pelo lojista)
    PENDING --> REJECTED: revisão nega
    APPROVED --> EXECUTED: tx on-chain confirmada
    APPROVED --> REJECTED: cancelado antes da execução
    REJECTED --> [*]
    EXECUTED --> [*]
```

| 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.

```http
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](#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

```mermaid
stateDiagram-v2
    [*] --> ACTIVE:           criação (B2B ou dashboard)
    ACTIVE --> SUBMITTED:     comprador envia o formulário
    ACTIVE --> EXPIRED_UNUSED: agora > expires_at
    ACTIVE --> CANCELED:      lojista cancela (dashboard)
    EXPIRED_UNUSED --> RENEWAL_REQUESTED: comprador clica em "Pedir link novo"
    RENEWAL_REQUESTED --> RENEWED: lojista aprova, novo token ACTIVE emitido
    SUBMITTED --> [*]
    CANCELED --> [*]
    RENEWED --> [*]
```

| 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" |

> **Note:**
>
> 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.

```http
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
}
```

> **Warning:**
>
> 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](https://docs.infraio.xyz/pt-BR/api-reference/authentication) 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:

```json
{
  "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](https://app.infraio.xyz)
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()`:

```ts
// 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()`](https://docs.infraio.xyz/pt-BR/sdks/javascript#sdkopenrefundrequest-)
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:

```http
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](https://docs.infraio.xyz/pt-BR/concepts/chains)),
o reembolso vai para `EXECUTED` e o total reembolsado do Order é
atualizado.

> **Warning:**
>
> 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](https://docs.infraio.xyz/pt-BR/concepts/chains).

---

## 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()`](https://docs.infraio.xyz/pt-BR/sdks/javascript#sdkopenrefundrequest-) — abre o formulário hospedado de reembolso como popup / redirect / embed.
- [Referência da API → Reembolsos](https://docs.infraio.xyz/pt-BR/api-reference#reembolsos) — catálogo de endpoints (criação, submit, renovação, status).
- [Conceitos → Pedidos](https://docs.infraio.xyz/pt-BR/concepts/orders) — como o estado do Refund liga de volta ao ciclo de vida do Order.
- [Webhooks → Visão geral](https://docs.infraio.xyz/pt-BR/webhooks/overview) — catálogo completo de eventos.
