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

# Webhooks na prática

> Endpoint, cadastro, validação HMAC, idempotência e eventos essenciais

Tutorial focado em **implementar** o receptor de webhooks. Referência completa de eventos: [guia de webhooks](/api-reference/guides/webhooks).

## 1. Endpoint no seu servidor

Exponha uma rota POST pública com HTTPS:

```
POST https://api.suaempresa.com/goatpay/webhook
```

<Warning>
  Em produção a GoatPay exige **HTTPS**. Em desenvolvimento use ngrok, Cloudflare Tunnel ou similar.
</Warning>

O handler deve:

1. Ler o **raw body** (antes de parsear JSON)
2. Validar `x-goatpay-signature`
3. Processar ou enfileirar o evento
4. Responder `200` rapidamente

## 2. Cadastrar na GoatPay

```bash theme={null}
curl -X POST https://api.goatpay.com.br/v1/webhooks/create \
  -H "X-API-Key: gp_live_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.suaempresa.com/goatpay/webhook",
    "events": [
      "payment.paid",
      "payment.created",
      "payment.pix.expired",
      "transfer.completed",
      "payment_link.paid"
    ]
  }'
```

Guarde o `whsec_...` da resposta — **só aparece na criação**. Armazene em `GOATPAY_WEBHOOK_SECRET`.

Permissão: `webhooks/create`.

## 3. Validar assinatura (Node.js / Express)

```typescript theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();

// Raw body obrigatório para HMAC
app.post(
  "/goatpay/webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.headers["x-goatpay-signature"] as string | undefined;
    const secret = process.env.GOATPAY_WEBHOOK_SECRET!;

    if (!verifySignature(req.body, signature, secret)) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(req.body.toString());
    const deliveryId = event.id;

    if (await alreadyProcessed(deliveryId)) {
      return res.status(200).json({ ok: true });
    }

    switch (event.event) {
      case "payment.paid":
        await handlePaymentPaid(event.data);
        break;
      case "payment.pix.expired":
        await handlePixExpired(event.data);
        break;
      // ...
    }

    await markProcessed(deliveryId);
    return res.status(200).json({ ok: true });
  },
);

function verifySignature(
  rawBody: Buffer,
  signatureHeader: string | undefined,
  webhookSecret: string,
): boolean {
  if (!signatureHeader?.startsWith("sha256=")) return false;
  const received = signatureHeader.slice("sha256=".length);
  const expected = crypto
    .createHmac("sha256", webhookSecret)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(received, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

### Python (FastAPI)

```python theme={null}
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/goatpay/webhook")
async def goatpay_webhook(request: Request):
    raw = await request.body()
    sig = request.headers.get("x-goatpay-signature", "")
    secret = os.environ["GOATPAY_WEBHOOK_SECRET"]

    if not verify(raw, sig, secret):
        raise HTTPException(401, "Invalid signature")

    event = json.loads(raw)
    # idempotência + processamento...
    return {"ok": True}

def verify(raw: bytes, header: str, secret: str) -> bool:
    if not header.startswith("sha256="):
        return False
    received = header[7:]
    expected = hmac.new(secret.encode(), raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)
```

## 4. Idempotência

A GoatPay pode reenviar a mesma entrega (retentativas: 1 min, 5 min, 15 min, 1 h, 4 h — até 5 tentativas).

Persista o `id` da entrega **antes** de efeitos colaterais:

```typescript theme={null}
if (await db.webhookDelivery.exists(deliveryId)) {
  return res.status(200).json({ ok: true });
}
```

## 5. Eventos que você provavelmente precisa

| Evento                | Quando usar                    |
| --------------------- | ------------------------------ |
| `payment.paid`        | PIX recebido — liberar pedido  |
| `payment.created`     | Cobrança registrada (opcional) |
| `payment.pix.expired` | QR expirou — cancelar checkout |
| `transfer.completed`  | Saque PIX concluído            |
| `payment_link.paid`   | Link de pagamento pago         |
| `refund.completed`    | Reembolso confirmado           |
| `med.created`         | Disputa MED aberta             |

Lista completa: [guia de webhooks — eventos](/api-reference/guides/webhooks#eventos).

## 6. Escopo por API key

| Como o endpoint foi criado         | Eventos recebidos                  |
| ---------------------------------- | ---------------------------------- |
| `POST /v1/webhooks/create`         | Só transações da **mesma** API key |
| Pelo app em Integrações → Webhooks | Toda a conta                       |

Se o webhook não chega, confira se a cobrança foi criada com a mesma chave que cadastrou o endpoint.

## Checklist

* [ ] HTTPS em produção
* [ ] HMAC no raw body com `timingSafeEqual` / `compare_digest`
* [ ] Resposta `2xx` em até alguns segundos
* [ ] Idempotência pelo `id` da entrega
* [ ] `whsec_` em variável de ambiente, não no código
* [ ] Logs com `requestId` da API, nunca a chave completa

## Próximo passo

[Troubleshooting de webhooks](/pages/guides/troubleshooting#webhook-não-chega) · [Cadastrar endpoint](/api-reference/endpoint/webhooks/create)
