Callbacks and webhooks

Signed payment and payout notices — the right way to reconcile without polling the API.

WeetPay notifies your system in two ways, which can be used together:

Transaction callbackDashboard webhook
Where to set itcallbackUrl field on each charge or payout in the APIWebhooks in the dashboard, one URL per event
Formatthe compatible API payload (amounts in reais)envelope { id, event, createdAt, data } (amounts in cents)
Signed withthe API client secretthe webhook secret (whsec_…), shown in the dashboard
Eventscharge paid; payout resultcharge created, paid, refunded; payout completed, failed

Both: POST with Content-Type: application/json, signature in the X-WeetPay-Signature header and automatic retries on failure.

Charge paid callback

Sent to the callbackUrl from Create PIX charge when the PIX is paid:

{
  "requestNumber": "order-12345",
  "statusTransaction": "PAID_OUT",
  "idTransaction": "cmh1x2y3z0001ab12cd34ef56",
  "typeTransaction": "PIX",
  "value": 49.9,
  "debtorName": "MARIA DA SILVA",
  "debtorDocument": "12345678909",
  "date": "2026-10-30T13:03:40.000Z",
  "endToEnd": "E18236120202610301303s0123456789"
}
FieldDescription
requestNumberthe identifier you sent on creation
statusTransactionalways PAID_OUT (only payments trigger callbacks)
idTransactionWeetPay charge ID
valuepaid amount, in reais (number)
debtorName, debtorDocumentwho paid, as reported by the bank
datepayment date/time (ISO 8601, UTC)
endToEndPIX End-to-End ID

Payout callback

Sent to the callbackUrl from Send PIX when the payout finishes:

{
  "id": "cmh1x9k2m0004ab12xy98zt76",
  "status": "sucess",
  "date": "2026-10-30T14:10:05.000Z",
  "message": "",
  "value": 150,
  "endToEnd": "E00416968202610301410abcdEFGH1234"
}

status is "sucess" (spelled this way for compatibility with the original gateway API) or "not paid". A payout that is not paid has its amount and fee refunded to your balance automatically; the reason is in message.

Dashboard webhooks

Register a URL for each event you want under Webhooks in the dashboard. The body comes in an envelope:

{
  "id": "cmh1xa0b10007ab12qq11ww22",
  "event": "TRANSACTION_PAID",
  "createdAt": "2026-10-30T13:03:41.000Z",
  "data": {
    "id": "cmh1x2y3z0001ab12cd34ef56",
    "externalId": "order-12345",
    "status": "PAID",
    "amount": 4990,
    "fee": 99,
    "netAmount": 4891,
    "description": "Basic t-shirt",
    "pixCopiaECola": "00020101021226850014br.gov.bcb.pix…",
    "endToEndId": "E18236120202610301303s0123456789",
    "payer": { "name": "MARIA DA SILVA", "document": "12345678909" },
    "expiresAt": "2026-10-30T14:02:11.000Z",
    "paidAt": "2026-10-30T13:03:40.000Z",
    "createdAt": "2026-10-30T13:02:11.000Z"
  }
}
eventWhendata
TRANSACTION_CREATEDcharge createdcharge
TRANSACTION_PAIDcharge paidcharge
TRANSACTION_REFUNDEDcharge refunded / disputedcharge + refund { id, kind, amount, reason }
WITHDRAWAL_COMPLETEDpayout paidpayout
WITHDRAWAL_FAILEDpayout rejected or failed (refunded)payout

On charges, externalId is your requestNumber; amount, fee and netAmount are in cents. Payouts carry id, transactionId, status (PAID, FAILED, REJECTED), amount, fee, pixKeyType, pixKey, endToEndId, reason, paidAt and createdAt.

Headers

HeaderValue
X-WeetPay-Signaturet=<unix>,v1=<hex> — see below
X-WeetPay-Eventevent (TRANSACTION_PAID, WITHDRAWAL_COMPLETED…)
X-WeetPay-Deliverydelivery ID — the same across all attempts; use it to drop duplicates
User-AgentWeetPay-Webhooks/1.0

Validating the signature

v1 is the hex HMAC-SHA256 of "<t>.<raw body>", keyed with the client secret (transaction callbacks) or the webhook secret (dashboard webhooks). Compute it over the body exactly as received, before any parsing, compare in constant time and reject notices whose t is too old (e.g. over 5 minutes).

import { createHmac, timingSafeEqual } from 'node:crypto';

export function validWeetPaySignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Delivery and retries

  • Reply with any 2xx within 10 seconds. Any other status, a timeout or a connection error counts as a failure.
  • Failures are retried after 1, 5, 15, 60, 180, 360, 720 and 1440 minutes (up to 9 attempts, about 2 days). After that the delivery is marked as failed in the history; you can resend it manually from the dashboard.
  • The same notice may arrive more than once: key on idTransaction / id (or X-WeetPay-Delivery) and make processing idempotent.
  • The URL must be public http(s). Redirects are not followed and internal network addresses (localhost, 10.x, 192.168.x…) are refused.
  • Reply fast and process later (queue): the notice does not wait for your system to finish the order.

Do not rely on the notice body alone to release a high-value order: validate the signature or confirm with Get transaction.

On this page