Webhooks

Webhooks let your system know a payment truly succeeded — you can't rely on the success screen in the customer's browser alone, since they can close the tab at any time.

View as Markdown

A checkout success page is only shown to the customer — it doesn't mean your backend knows about it yet. The customer might close the tab, lose connectivity, or share the URL with someone else before ever seeing it. Webhooks are the authoritative source your system should actually trust.

1. Register a webhook endpoint

curl -sS -X POST "$BASE/api/WebhookEndpoint" \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.example/webhooks/rebatepay","events":["payment.succeeded","payment.failed","refund.succeeded"]}'
The response's signingSecret (whsec_...) is shown exactly once. Later GETs only expose secretPrefix. Store it immediately — if you lose it, disable the old endpoint and create a new one.

2. What you'll receive

POST /your-url
Content-Type: application/json
X-RebatePay-Signature: t=<unix>,v1=<hmac_sha256(secret, "{t}.{raw_body}")>
X-RebatePay-Event-Id: evt_...
X-RebatePay-Delivery-Id: wda_...
X-RebatePay-Event-Type: payment.succeeded

3. Verify the signature

  1. 1Parse t (timestamp) and v1 (signature) from X-RebatePay-Signature.
  2. 2Reject if |now - t| > 300 seconds (replay protection).
  3. 3Compute HMAC-SHA256 of ${t}.${raw body} with your signing secret.
  4. 4Compare against the received v1 using a constant-time comparison, not ===.
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, signatureHeader, rawBody) {
  const [tPart, vPart] = signatureHeader.split(",");
  const t = Number(tPart.split("=")[1]);
  const v1 = vPart.split("=")[1];
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

4. Idempotency (the same event may arrive more than once)

Delivery is at-least-once — the same event can be redelivered on retry. Use id (or X-RebatePay-Event-Id) as your dedup key: check whether you've already processed that event id before applying side effects, and just return 200 if you have.

5. Failures and retries

If your endpoint doesn't return 2xx (or times out), ReBate Pay retries with backoff; after enough failures the delivery is marked terminal (dead-lettered). Inspect delivery history with GET /api/WebhookEvent and GET /api/WebhookDelivery, or self-serve retry a specific delivery with POST /api/WebhookDelivery/{id}/Retry (needs webhooks:write and an Idempotency-Key). There is no /replay endpoint.

Event types

CategoryEvents
Products/Customersproduct.created, price.created, customer.created
Subscriptionssubscription.created, subscription.canceled, subscription.ended
Billing/Paymentsbilling_record.created, billing_record.paid, payment.succeeded, payment.failed, payment.unknown
Refundsrefund.requested, refund.succeeded, refund.failed
Payment Linkspayment_link.created, payment_link.completed
Usage billing (Sandbox / Early Access)usage.recorded, usage.reversed, credit.granted, credit.low, credit.exhausted, entitlement.changed, entitlement.exhausted
For local development to actually receive deliveries, your receiver needs a publicly reachable URL (e.g. a tunneling tool like ngrok) — ReBate Pay cannot deliver to localhost.