DOCUMENTATION

From failed to fixed.

Put Hookjail between a provider and your app. You keep every delivery, see what went wrong, and replay the exact request once your code is fixed.

How it works

Your provider sends webhooks to a Hookjail URL. Hookjail stores the delivery (encrypted), forwards it to your app right away, and hands your app’s answer back to the provider. Nothing about your app’s normal behaviour changes. When your app fails, the delivery is already safe here.

Provider → Hookjail URL → your app

Because Hookjail passes your app’s real status code back, the provider’s own retry schedule keeps working. A failed delivery shows up in the inbox and the provider retries it.

1. Create an endpoint

Sign in, open New endpoint, choose the provider, name it, and enter the HTTPS URL of your webhook handler. Hookjail shows two values once:

  • The receiving URL, which you paste into your provider. Treat it like a secret; you can rotate it at any time.
  • The signing secret (hjs_…), used to verify replays. Shown only when created or rotated.

Your destination must be a public HTTPS hostname on port 443. IP addresses, internal names and URLs that redirect are refused.

2. Connect your provider

ProviderWhereStored signature header
StripeDevelopers → Webhooks → Add endpointStripe-Signature
ShopifySettings → Notifications → Webhooks (JSON)X-Shopify-Hmac-Sha256
GitHubRepository → Settings → Webhooks (application/json)X-Hub-Signature-256
PaddleDeveloper tools → NotificationsPaddle-Signature
Anything elsePOST to the URLheaders you list

Only an allowlist of headers is stored: the provider’s signature and event headers, Content-Type, User-Agent, X-Request-Id. Authorization, cookies and API-key headers are never stored, even if you list them.

3. When a delivery fails

A delivery is a dead letter when your app answers with a 4xx/5xx, times out after 10 seconds, cannot be reached, or answers with a redirect. Open it from Dead letters: you see the status, the problem, every attempt, and whether the provider sent the same event again.

If you pause an endpoint, deliveries are still captured and marked Held, but nothing is forwarded. Replay them after you resume.

4. Inspect safely

The inbox shows a redacted preview: names, e-mail addresses, phone numbers, amounts, tokens and card numbers are masked, and card numbers, private keys, JWTs and API keys are flagged. Search works on metadata only (event type and ID), never on payload content.

When you truly need the original, choose Reveal payload and give a reason. The reason and the access are written to the audit log before the data is shown, and the view closes after five minutes.

5. Replay and verify

Open a delivery and choose Replay. A dry run checks the destination and shows what would be sent without sending anything. A live replay sends the original bytes with the original headers. If the destination already handled the event, Hookjail asks you to confirm first.

Every replay also carries a fresh Hookjail signature, because some providers’ own signatures expire after a few minutes (Stripe’s libraries reject signatures older than five minutes by default). For replays, verify Hookjail-Signature, not the provider’s.

HeaderMeaning
Hookjail-Signaturet=<unix>,v1=<hex HMAC-SHA256> over "<t>." + raw body, keyed with your signing secret
Hookjail-Replaytrue on replays
Hookjail-Delivery-IdStable per delivery. Use it as your idempotency key
Hookjail-Original-TimestampWhen Hookjail first received the event (unix seconds)
Hookjail-Attempt1 for the first delivery, 2 and up for later attempts
// Node 18+. Verify a Hookjail replay before trusting it.
// Express: read the RAW bytes (express.raw({ type: "*/*" })); never re-serialise parsed JSON.
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyHookjail(
  secret,
  header,
  rawBody,
  { toleranceSeconds = 300, now = Date.now() } = {},
) {
  const parts = Object.fromEntries(
    String(header ?? "")
      .split(",")
      .map((p) => p.split(/=(.*)/s).slice(0, 2)),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;
  if (Math.abs(Math.floor(now / 1000) - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret)
    .update(`${t}.`)
    .update(rawBody)
    .digest("hex");
  const given = Buffer.from(parts.v1);
  const want = Buffer.from(expected);
  return given.length === want.length && timingSafeEqual(given, want);
}

Always hash the raw request bytes. Re-serialising parsed JSON changes the bytes and the signature will not match. These snippets are tested against shared test vectors in CI.

Make your handler idempotent. A replay can repeat side effects (a second e-mail, a second charge). Record the Hookjail-Delivery-Id or the provider’s event ID when you process an event and ignore repeats.

Limits

  • Request body up to 1 MB. Your app has 10 seconds to answer.
  • Hookjail passes back your app’s status, content type and up to 64 KB of the body. Redirects are never followed.
  • Original payloads and delivery records are kept for the retention your plan allows (Settings). After that, replay and reveal stop working for that delivery.
  • Live replay and alerts are part of paid plans; dry runs are available on every plan.

Just want to look at a webhook?

The temporary webhook needs no account: it gives you a URL that lives for 30 minutes so you can see exactly what a provider sends. Only your browser can read it, and everything is deleted afterwards.

Create a temporary webhook