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.
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"]}'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.succeeded3. Verify the signature
- 1Parse
t(timestamp) andv1(signature) fromX-RebatePay-Signature. - 2Reject if
|now - t| > 300seconds (replay protection). - 3Compute HMAC-SHA256 of
${t}.${raw body}with your signing secret. - 4Compare against the received
v1using 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
| Category | Events |
|---|---|
| Products/Customers | product.created, price.created, customer.created |
| Subscriptions | subscription.created, subscription.canceled, subscription.ended |
| Billing/Payments | billing_record.created, billing_record.paid, payment.succeeded, payment.failed, payment.unknown |
| Refunds | refund.requested, refund.succeeded, refund.failed |
| Payment Links | payment_link.created, payment_link.completed |
| Usage billing (Sandbox / Early Access) | usage.recorded, usage.reversed, credit.granted, credit.low, credit.exhausted, entitlement.changed, entitlement.exhausted |
localhost.
ReBate Pay Docs