Skip to main content
Tutorial focado em implementar o receptor de webhooks. Referência completa de eventos: guia de webhooks.

1. Endpoint no seu servidor

Exponha uma rota POST pública com HTTPS:
POST https://api.suaempresa.com/goatpay/webhook
Em produção a GoatPay exige HTTPS. Em desenvolvimento use ngrok, Cloudflare Tunnel ou similar.
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

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)

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)

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:
if (await db.webhookDelivery.exists(deliveryId)) {
  return res.status(200).json({ ok: true });
}

5. Eventos que você provavelmente precisa

EventoQuando usar
payment.paidPIX recebido — liberar pedido
payment.createdCobrança registrada (opcional)
payment.pix.expiredQR expirou — cancelar checkout
transfer.completedSaque PIX concluído
payment_link.paidLink de pagamento pago
refund.completedReembolso confirmado
med.createdDisputa MED aberta
Lista completa: guia de webhooks — eventos.

6. Escopo por API key

Como o endpoint foi criadoEventos recebidos
POST /v1/webhooks/createSó transações da mesma API key
Pelo app em Integrações → WebhooksToda 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 · Cadastrar endpoint