> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loja.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Como receber e validar eventos de pagamento e pedidos da loja.gg.

<Warning>
  **Nunca libere o produto na página de sucesso do checkout.** O comprador pode fechar o navegador antes do redirect, e a confirmação de um PIX pode chegar minutos depois, fora do fluxo dele. A fonte da verdade sobre um pagamento é sempre o **webhook**.
</Warning>

## Configurando um endpoint

Cadastre a URL do seu endpoint no painel (`Loja → Configurações → Webhooks`) e guarde o segredo gerado (`whsec_...`).

## Eventos

| Evento                                                                    | Quando dispara                                                             |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `checkout.session.completed`                                              | Sessão de checkout concluída pelo comprador                                |
| `order.created`                                                           | Pedido criado                                                              |
| `order.paid`                                                              | Pagamento confirmado — **este é o gatilho correto para liberar o produto** |
| `order.failed`                                                            | Pagamento falhou                                                           |
| `order.refunded`                                                          | Pedido reembolsado                                                         |
| `order.chargeback`                                                        | Contestação de pagamento recebida                                          |
| `subscription.created` / `subscription.renewed` / `subscription.canceled` | Eventos de assinatura recorrente                                           |

Corpo recebido:

```json theme={null}
{
  "id": "evt_a1b2c3...",
  "type": "order.paid",
  "livemode": true,
  "created_at": "2026-08-15T14:02:55Z",
  "data": { "order": { "id": "...", "number": 1042, "status": "paid", "items": ["..."], "customer": {"...": "..."} } }
}
```

## Validando a assinatura

Header enviado: `X-Huskpay-Signature: t=1755280000,v1=5f3a9c...`

O `v1` é `HMAC-SHA256(segredo, "{t}.{corpo_cru}")`. Use sempre o **corpo cru** da requisição, antes de qualquer parse de JSON — reserializar o corpo muda os bytes e a assinatura não bate mais.

<CodeGroup>
  ```js Node.js theme={null}
  const crypto = require('crypto');

  function verificar(rawBody, header, segredo) {
    const partes = Object.fromEntries(header.split(',').map(p => p.split('=')));
    const t = Number(partes.t);

    // Rejeita entregas antigas: sem isso, alguém que capturou uma entrega
    // pode reenviá-la para sempre.
    if (Math.abs(Date.now() / 1000 - t) > 300) return false;

    const esperado = crypto.createHmac('sha256', segredo)
      .update(`${t}.${rawBody}`)
      .digest('hex');

    // Comparação em tempo constante.
    return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1));
  }
  ```

  ```go Go theme={null}
  func verificar(rawBody []byte, header, segredo string) bool {
      var ts int64
      var v1 string
      for _, parte := range strings.Split(header, ",") {
          kv := strings.SplitN(parte, "=", 2)
          if len(kv) != 2 { continue }
          switch kv[0] {
          case "t":  ts, _ = strconv.ParseInt(kv[1], 10, 64)
          case "v1": v1 = kv[1]
          }
      }
      if math.Abs(float64(time.Now().Unix()-ts)) > 300 { return false }

      mac := hmac.New(sha256.New, []byte(segredo))
      fmt.Fprintf(mac, "%d.", ts)
      mac.Write(rawBody)
      return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(v1))
  }
  ```

  ```php PHP theme={null}
  function verificar(string $rawBody, string $header, string $segredo): bool {
      parse_str(str_replace(',', '&', $header), $partes);
      $t = (int)($partes['t'] ?? 0);
      if (abs(time() - $t) > 300) return false;

      $esperado = hash_hmac('sha256', "{$t}.{$rawBody}", $segredo);
      return hash_equals($esperado, $partes['v1'] ?? '');
  }
  ```
</CodeGroup>

## Regras de entrega

<CardGroup cols={2}>
  <Card title="Responda 2xx rápido" icon="zap">
    Processamento pesado deve ir para uma fila sua — o timeout do lado da loja.gg é de 15 segundos.
  </Card>

  <Card title="Entrega at-least-once" icon="repeat">
    O mesmo `id` de evento pode chegar mais de uma vez. Guarde os ids processados e trate repetição como no-op.
  </Card>

  <Card title="Reenvio com backoff" icon="clock">
    Falhas são reagendadas em `1m → 5m → 30m → 2h → 6h → 24h`. Depois disso a entrega fica `exhausted` e pode ser reenviada manualmente pelo painel.
  </Card>

  <Card title="Requisitos de rede" icon="shield-check">
    A URL precisa ser `https` na porta 443 e resolver para um IP público. Endereços internos (`127.0.0.1`, `10.x`, `169.254.169.254`) são recusados tanto no cadastro quanto em cada entrega. Redirecionamentos não são seguidos.
  </Card>
</CardGroup>

<Note>
  Um endpoint com falhas contínuas é desativado automaticamente, com aviso ao lojista.
</Note>

## Sem endpoint público? Use polling

```bash theme={null}
GET /storefront/v1/events?limit=50&starting_after=<último_id>
```

Exige chave secreta e escopo `webhooks:read`.

## Checklist antes de ir para produção

* [ ] A chave secreta não aparece em nenhum lugar do bundle do front (`grep -r "hk_live_sec" ./build`)
* [ ] O webhook valida a assinatura **e** a tolerância de 5 minutos
* [ ] O webhook é idempotente por `event.id`
* [ ] A entrega do produto acontece no `order.paid`, não na página de sucesso
* [ ] `Idempotency-Key` é um UUID novo por tentativa de compra, reutilizado apenas nos retries
* [ ] Os totais exibidos vêm de `line_items` / `amount_total`, não de cálculo próprio
* [ ] Origens e URLs de retorno de produção estão cadastradas
* [ ] Testado ponta a ponta com chaves `hk_test_*`

## Entrega em servidor de jogo

Se o que você quer entregar é um item, VIP ou comando dentro de um servidor FiveM, RedM, Minecraft ou outro engine, o webhook `order.paid` é o mesmo gatilho — veja os guias específicos em [FiveM e RedM](/api-reference/fivem-redm) e [Minecraft e outros engines](/api-reference/minecraft-e-outros).
