Skip to content

Webhooks

Events are delivered by HTTP POST to your active endpoints.

Event catalog

typewhen
payment.succeededpayment confirmed
payment.settledpayment settled — planned, not fired yet (there is no automatic settlement confirmation today; see Settlement)
payment.cancelledpayment cancelled
payment.failedpayment declined

Internally, payment.captured is delivered publicly as payment.succeeded. payment.authorized does not produce a webhook.

Envelope

json
{
  "id": "evt_9f8c...", "type": "payment.succeeded", "created": 1789990000,
  "data": {
    "payment": {
      "id": "pay_9f8c...",
      "amount": 510000,
      "currency": "BRL",
      "gross_amount": 510000,
      "gross_currency": "BRL",
      "fee_amount": 25000,
      "fee_currency": "BRL",
      "net_amount": 95000,
      "net_currency": "USD",
      "payment_method": "pix",
      "status": "succeeded",
      "reference": "pedido-123",
      "failure_code": null,
      "fx": {
        "rate": 5.1,
        "base": "USD",
        "quote": "BRL",
        "locked_at": 1800000000,
        "valid_until": null,
        "rate_history_id": 123
      },
      "settlement": {
        "estimated_date": "2026-09-08",
        "reason": [
          { "type": "cutoff", "note": "..." },
          { "type": "weekend", "date": "2026-09-05" },
          { "type": "holiday", "date": "2026-09-07", "country": "BR", "name": "..." }
        ]
      }
    }
  }
}

The public payload never includes spot_rate, cobertura_centibps, source, median_3_stablecoin, percent_centibps, or rule percentages.

Settlement (settlement)

Present only on payment.succeeded. It tells you when the money should land and why that date — so you can close your books without recomputing the banking calendar (BR/Febraban + US/Fedwire), the 3 p.m. cutoff (America/Sao_Paulo), and the weekend rollover yourself.

  • estimated_date (YYYY-MM-DD, America/Sao_Paulo) is an ESTIMATE, not a guarantee. It reflects the calendar rules in force at confirmation time.
  • reason[] is the chronological trail of what moved the date: cutoff (paid after 3 p.m.), weekend, and holiday (with country and name). An empty array means the date landed directly, with no adjustments.
  • There is no "D+N" field. Use estimated_date; the reason explains the path. Do not add days yourself.
  • Outside the calendar's coverage, the settlement object is omitted — we never send a date without coverage. Treat the absence as "no forecast", not as an error.
  • There is no settlement-confirmation event. payment.succeeded carries the forecast; the platform does not emit an event today confirming that the credit actually happened on estimated_date. Do not assume automatic confirmation — if you need to reconcile the real credit, do it from your bank statement.

Signature

Each delivery carries the header:

Pagooz-Signature: t=1789990000,v1=<hex>

v1 is the HMAC-SHA256, in hex, of "{t}.{body}" (the timestamp, a dot, and the raw request body), using the endpoint's secret. Verify and reject deliveries whose t is more than 5 minutes from now (replay tolerance).

Working example (Node):

js
const crypto = require("crypto");

function verifyPagoozSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5 min
  const expected = crypto.createHmac("sha256", secret)
    .update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1 || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Delivery, retry, and consumer idempotency

  • A delivery only succeeds with a 2xx response.
  • A failure triggers 5 attempts with backoff: immediate, 1 minute, 5 minutes, 30 minutes, and 2 hours.
  • After the 5th failure, the delivery becomes dead_lettered and visible in the cockpit.
  • At-least-once delivery. The same event may arrive more than once (retry, re-notification, redelivery). Your consumer must be idempotent by event.id — process each id only once.

Configuration in the cockpit

One URL per tenant per environment (sandbox and live). The cockpit reads the session's global mode and saves the URL for that environment. The signing secret is shown once, at creation/rotation. The webhook_endpoints table is not used in this version; it is reserved for multiple destinations in the future.


Reconciliation and re-notification

Manual resend in the cockpit

The merchant can resend a failed delivery from the cockpit. The resent payload keeps the same event_id; the new attempt is recorded in webhook_deliveries with origin = "manual".

Delivery history

The cockpit shows the latest deliveries per environment. Each object:

json
{
  "id": "whd_9f8c...", "event_id": "evt_9f8c...",
  "attempt": 3, "status": "failed", "http_status": 503,
  "response_body": "...",
  "origin": "auto", "created": 1789990000, "next_attempt_at": 1789990300
}

It lets you diagnose on your own why a notification did not arrive.