---
title: "Webhooks"
description: "Receba um POST assinado quando uma geração termina ou uma recarga é paga."
source_url: "https://staging-hypio.doti.gg/docs/webhooks"
updated: "2026-11-11"
---

# Webhooks

Receba um POST assinado quando uma geração termina ou uma recarga é paga.

## Configurar

Em [Configurações → Webhooks](https://staging-hypio.doti.gg/configuracoes/webhooks), cadastre uma URL `https` pública e escolha os eventos. Até 10 por organização. O segredo começa com `whsec_`.

## Eventos

| Evento | Quando |
| --- | --- |
| `generation.completed` | A geração terminou com sucesso. |
| `generation.failed` | A geração falhou e a reserva voltou. |
| `credits.topup_succeeded` | Uma recarga foi paga e o saldo entrou. |

## Corpo

generation.completed:

```json
{
  "id": "generation.completed:0b7c1e5a-4d2f-4c8e-9a51-6f3d2b8e7c10",
  "type": "generation.completed",
  "created_at": "2026-11-11T14:02:11.000Z",
  "data": {
    "generation_id": "0b7c1e5a-4d2f-4c8e-9a51-6f3d2b8e7c10",
    "kind": "image",
    "model_id": "seedream-5.0-flash",
    "status": "succeeded",
    "output_count": 1,
    "amount_cents": 17,
    "currency": "brl",
    "error": null
  }
}
```

O `id` é estável por evento: use-o para descartar entregas repetidas. Em `credits.topup_succeeded`, `data` traz `checkout_session_id`, `amount_cents`, `currency` e `credits`. `amount_cents` está na moeda de `currency` (`brl`, `usd` ou `eur`); para somar em reais, use `credits`, que está sempre em centavos de real.

## Headers

| Header | Valor |
| --- | --- |
| `Hypio-Signature` | `t=<unix>,v1=<hex>` |
| `Hypio-Webhook-Id` | O `id` do evento. |
| `Hypio-Event` | O `type` do evento. |

## Verificar a assinatura

`v1` é o HMAC-SHA256, em hexadecimal, de `<t>.<corpo cru>` com o segredo como chave. Use o corpo exatamente como chegou e recuse `t` antigo para barrar reenvio.

JavaScript:

```javascript
import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const received = Buffer.from(parts.v1 ?? "");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && received.length === expected.length && crypto.timingSafeEqual(received, Buffer.from(expected));
}
```

Python:

```python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts.get("v1", ""))
```

## Entrega e novas tentativas

- Qualquer resposta `2xx` conta como entregue.
- Responda em até 10 segundos. Depois disso a entrega conta como falha.
- Falhas são repetidas depois de 30 s, 2 min, 10 min, 30 min, 2 h: 6 tentativas no total.
