RefundHalt

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

EventFires when
refund_request.createdA store opened a refund case for one of your apps
refund_request.respondedWe delivered your response to the store
refund_request.failedThe store rejected our response
refund.receivedThe store granted a refund
refund.declinedThe store declined the refund — money saved
refund.reversedThe store reversed an earlier refund decision
transaction.createdA new transaction was recorded
transaction.updatedA transaction changed state
app.credentials_verifiedStore credentials verified successfully
app.credentials_failedStore 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.

On this page