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.
This describes how the product works. Accounts open soon; leave your email and we write once, when they do.
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
| Provider | Where | Stored signature header |
|---|---|---|
| Stripe | Developers → Webhooks → Add endpoint | Stripe-Signature |
| Shopify | Settings → Notifications → Webhooks (JSON) | X-Shopify-Hmac-Sha256 |
| GitHub | Repository → Settings → Webhooks (application/json) | X-Hub-Signature-256 |
| Paddle | Developer tools → Notifications | Paddle-Signature |
| Anything else | POST to the URL | headers 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.
| Header | Meaning |
|---|---|
| Hookjail-Signature | t=<unix>,v1=<hex HMAC-SHA256> over "<t>." + raw body, keyed with your signing secret |
| Hookjail-Replay | true on replays |
| Hookjail-Delivery-Id | Stable per delivery. Use it as your idempotency key |
| Hookjail-Original-Timestamp | When Hookjail first received the event (unix seconds) |
| Hookjail-Attempt | 1 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.