Webhooks
RefundHalt's own signed events — structured JSON about every refund case, delivered Stripe-style with HMAC signatures and retries.
Webhooks are RefundHalt telling your systems what happened: a refund request arrived, a response was sent, money was saved. They're structured JSON we author — not to be confused with notification forwarding, which relays the store's raw notifications unchanged.
Create endpoints in Dashboard → Webhooks. Each endpoint gets a signing secret (shown once) and can subscribe to specific event types, for all apps or one app.
Event catalog
| Event | Fires when |
|---|---|
refund_request.created | A store opened a refund case for one of your apps |
refund_request.responded | We delivered your response to the store |
refund_request.failed | The store rejected our response |
refund.received | The store granted a refund |
refund.declined | The store declined the refund — money saved |
refund.reversed | The store reversed an earlier refund decision |
transaction.created | A new transaction was recorded |
transaction.updated | A transaction changed state |
app.credentials_verified | Store credentials verified successfully |
app.credentials_failed | Store credentials started failing |
Verifying signatures
Every delivery carries an X-RefundHalt-Signature header:
X-RefundHalt-Signature: t=<unix-timestamp>,v1=<hex hmac-sha256(secret, "{t}.{raw-body}")>import { createHmac, timingSafeEqual } from "crypto";
export function verify(req: Request, secret: string, raw: string) {
const header = req.headers.get("X-RefundHalt-Signature")!;
const { t, v1 } = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const expected = createHmac("sha256", secret)
.update(`${t}.${raw}`)
.digest("hex");
return (
timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) &&
Date.now() / 1000 - Number(t) < 300
);
}Always verify against the raw request body (before any JSON parsing), and reject signatures older than a few minutes to prevent replays.
Delivery semantics
- Respond with a 2xx quickly; do the heavy work asynchronously.
- Failed deliveries are retried with exponential backoff.
- Every event carries an idempotency key (
event_id) — deduplicate on it, since retries can occasionally deliver twice.