# Webhooks

O plicflix avisa o **servidor do seu jogo** quando algo acontece com ele: o jogo
foi publicado ou recusado, o servidor caiu ou voltou, abriu um caso de
reclamação, uma fatura foi paga. É opcional — o portal e os e-mails já contam
tudo isso para as pessoas do estúdio —, mas permite automatizar (abrir um
chamado, avisar no Slack/Discord, pausar uma campanha).

## Cadastrar

No developers → **Webhooks** (`/webhooks`):
1. o endereço `https://…` do seu servidor que recebe os avisos;
2. os eventos (nenhum marcado = todos);
3. **Criar webhook** → o segredo `whsec_…` aparece no cartão (clique para
   copiar). Guarde no servidor como `PLICFLIX_WEBHOOK_SEGREDO`;
4. **Enviar teste** manda um evento `teste` na hora: confira na lista
   "Últimos envios".

Até 5 webhooks por estúdio. Só **dono** e **desenvolvedor** cadastram.

## O que chega

Um `POST` com `Content-Type: application/json`:

```json
{"evento": "jogo.publicado", "criado": "2026-11-02T18:00:00Z", "dados": {"jogo": "ilha-das-luzes"}}
```

| cabeçalho | o que é |
|---|---|
| `Plicflix-Evento` | o evento (o mesmo do corpo) |
| `Plicflix-Envio` | o número do envio — **o mesmo nas novas tentativas**: use para não processar duas vezes |
| `Plicflix-Assinatura` | `t=<unix>,v1=<hex>`: v1 = HMAC-SHA256(segredo, `t + "." + corpo`) |

| evento | quando | `dados` |
|---|---|---|
| `jogo.em_revisao` | o estúdio enviou para revisão | `jogo` |
| `jogo.publicado` | a equipe publicou | `jogo` |
| `jogo.recusado` | a equipe pediu ajustes | `jogo`, `motivo` |
| `jogo.tirado` | saiu do catálogo | `jogo` |
| `servidor.fora_do_ar` | a rota de status parou de responder 200 | `jogo`, `status` |
| `servidor.no_ar` | voltou | `jogo`, `status` |
| `caso.aberto` | reclamações demais no mês: prazo de 48 h úteis | `jogo`, `mes`, `prazo`, `reclamacoes`, `jogadores` |
| `caso.decidido` | a equipe decidiu o caso | `jogo`, `caso`, `mes`, `decisao` |
| `fatura.paga` | uma fatura do estúdio foi paga | `fatura`, `meio` |
| `teste` | o botão "Enviar teste" | `mensagem` |

## Conferir a assinatura

Use o **corpo bruto** (antes de interpretar o JSON) e recuse assinaturas com
mais de 5 minutos.

```js
// Node 18+
import crypto from "node:crypto";
function confere(cabecalho, corpoBruto, segredo) {
  const p = Object.fromEntries(cabecalho.split(",").map((x) => x.trim().split("=")));
  const esperado = crypto.createHmac("sha256", segredo).update(p.t + "." + corpoBruto).digest("hex");
  const recente = Math.abs(Date.now() / 1000 - Number(p.t)) <= 300;
  return recente && crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(p.v1 ?? ""));
}
```

```go
// Go
func confere(cabecalho string, corpo []byte, segredo string) bool {
	var t, v1 string
	for _, p := range strings.Split(cabecalho, ",") {
		k, v, _ := strings.Cut(strings.TrimSpace(p), "=")
		if k == "t" { t = v } else if k == "v1" { v1 = v }
	}
	ts, _ := strconv.ParseInt(t, 10, 64)
	if time.Since(time.Unix(ts, 0)).Abs() > 5*time.Minute { return false }
	m := hmac.New(sha256.New, []byte(segredo))
	m.Write([]byte(t + "."))
	m.Write(corpo)
	return hmac.Equal([]byte(hex.EncodeToString(m.Sum(nil))), []byte(v1))
}
```

## Respostas e novas tentativas

- Responda **2xx em até 10 s** (enfileire o trabalho pesado do seu lado).
- Qualquer outra resposta (ou sem resposta): novas tentativas em **1 min, 5 min,
  30 min, 2 h, 6 h, 12 h e 24 h**. Depois disso, o plicflix desiste e o envio
  aparece como "desistiu" na lista.
- Redirecionamentos não são seguidos: use o endereço final.
