Gravity PayDocs

Webhooks

Receba eventos de pagamento assinados, valide a assinatura e reaja a eles.

Webhooks são como a Gravity Pay avisa o seu sistema quando algo acontece — um pagamento confirmado, uma assinatura renovada, um estorno. Eles são a fonte da verdade: chegam mesmo que o cliente feche o navegador.

Duas formas de receber

Endpoints do painel

Cadastre URLs fixas em Integrações → Webhooks e escolha os eventos (e, se quiser, os produtos). Recebem todos os eventos que casarem.

postbackUrl da cobrança

Ao criar uma cobrança via API, informe um postbackUrl. Ele recebe os eventos daquela transação específica — somado aos endpoints do painel.

Eventos

EventoQuando dispara
payment.paidUm pagamento foi confirmado.
payment.failedUm pagamento foi recusado.
payment.refundedUm pagamento foi estornado.
subscription.createdUma assinatura ficou ativa.
subscription.renewedUma assinatura foi renovada (novo ciclo pago).
subscription.canceledUma assinatura foi cancelada.

Formato do payload

Toda entrega é um POST com corpo JSON neste formato:

{
  "id": "whd_9f2c...",
  "event": "payment.paid",
  "createdAt": "2026-06-10T01:19:06.544Z",
  "data": {
    "id": "pay_...",
    "provider": "STRIPE",
    "status": "PAID",
    "amountInCents": 12900,
    "currency": "usd",
    "chargeId": "chg_2e59538f...",
    "customerEmail": "client@acme.com",
    "metadata": { "invoiceId": "1234" }
  }
}

Para eventos de uma cobrança via API, o data.metadata traz a metadata que você enviou na criação — use o invoiceId (ou o que for) para dar baixa.

E os headers:

HeaderDescrição
X-GravityPay-EventO tipo do evento (ex.: payment.paid).
X-GravityPay-DeliveryId único da entrega (= id do payload). Idempotência.
X-GravityPay-SignatureAssinatura HMAC (veja abaixo).

Validando a assinatura

Cada entrega é assinada com HMAC-SHA256. O header tem o formato t=<unix>,v1=<hmac>, e o HMAC é calculado sobre `${t}.${body}` usando o secret.

Qual secret usar?

  • Endpoints do painel: o signing secret exibido ao criar o endpoint.
  • postbackUrl: o postbackSecret retornado na resposta do POST /v1/charges daquela cobrança. Cada cobrança tem o seu.
import crypto from "node:crypto";

function verify(secret, header, rawBody) {
  const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

// Express — use o corpo CRU (raw), não o JSON já parseado.
app.post("/webhooks/gravitypay", express.raw({ type: "*/*" }), (req, res) => {
  const ok = verify(
    process.env.POSTBACK_SECRET,
    req.headers["x-gravitypay-signature"],
    req.body.toString("utf8"),
  );
  if (!ok) return res.status(400).end();

  const event = JSON.parse(req.body.toString("utf8"));
  if (event.event === "payment.paid") {
    // dê baixa na fatura event.data.metadata.invoiceId
  }
  res.status(200).end();
});
import hmac, hashlib

def verify(secret: str, header: str, raw_body: str) -> bool:
    parts = dict(p.split("=") for p in header.split(","))
    expected = hmac.new(
        secret.encode(), f"{parts['t']}.{raw_body}".encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(parts["v1"], expected)

Use o corpo cru

Valide a assinatura sobre o corpo exatamente como recebido (raw). Se você re-serializar o JSON, a assinatura não vai bater.

postbackUrl

Quando você cria uma cobrança com postbackUrl, todos os eventos daquela transação (payment.paid, payment.failed, payment.refunded) são entregues também nessa URL — além dos endpoints do painel. É ideal para reconciliar uma fatura sem precisar manter um endpoint fixo.

O postbackSecret que valida essas entregas vem uma única vez, na resposta da criação da cobrança. Guarde-o junto com o id da cobrança/fatura.

Idempotência

A mesma entrega pode chegar mais de uma vez (ex.: ao reprocessar). Use o X-GravityPay-Delivery (= id do payload) como chave de idempotência e ignore duplicatas.

Resposta e re-tentativas

Responda 2xx rapidamente (idealmente em até alguns segundos) para confirmar o recebimento. Qualquer outro status é tratado como falha. Entregas que falham ficam registradas no painel (Integrações → Webhooks → Entregas), de onde você pode reprocessar manualmente.