HookSteadBETA

User guide

HookStead documentation

Everything you need to receive webhooks on a stable URL, forward them to your endpoints with signatures and retries, and find, fix and replay the ones that went wrong.

Last updated: October 2026

01 / Getting started

Getting started

From sign-up to your first delivered webhook in about five minutes.

How HookStead works

HookStead sits between the services that send you webhooks and the endpoints that handle them.

TermWhat it is
SourceA URL you give to a sender (Stripe, GitHub, your own app). It decides who may send and how requests are checked.
EventOne request received on a source: headers, body, and when it arrived.
DestinationYour endpoint. HookStead forwards events to it, signed with its own secret.
ConnectionLinks a source to a destination. One source can feed many destinations, and the other way round.
DeliveryOne event on its way to one destination, with every attempt recorded.
WorkspaceKeeps environments apart, e.g. Production and Staging. Each has its own sources and destinations.
OrganizationYour team and your plan. Members and billing belong to the organization.

Account and organization

  1. Create an account with your email address and a password, then click the link in the verification email.
  2. Create an organization: a name and a URL (e.g. acme). You become its owner, and a Production workspace is created for you.
  3. The workspace page shows a Get started checklist. It ticks itself off as you go and disappears once your first delivery succeeds.

Your first webhook

  1. Sources → enter a name → Create source. Copy the URL it shows.
  2. Send something to it, or use Send a test event on the checklist:
Send an event
curl -X POST "https://<your Hookstead>/in/<source token>"   -H 'content-type: application/json'   -d '{"type":"order.paid","id":"ord_123"}'
  1. Destinations → your endpoint's name and URL → Create destination. Copy the signing secret now: it's shown once.
  2. On the destination's page, pick the source and Connect source.
  3. Send another event. It arrives at your endpoint, and the delivery shows as succeeded.

02 / Receiving webhooks

Receiving webhooks

Every request to a source URL is checked, stored and acknowledged before the sender gets its answer.

Sources

A source URL looks like https://<your HookStead>/in/<token>. Anything after the token is kept as the event's path, so one source can take /in/<token>/orders and /in/<token>/refunds. On the source's page you can disable it (requests then get 404) or rotate the URL if it leaked; rotating stops the old URL immediately. It also shows the methods the source accepts (POST by default) and the status it answers with (202 by default); change those through the API.

The sources list shows each source's status: Receiving (events in the last 24 hours), Quiet (nothing for longer), Rejecting (its latest request, within the hour, failed verification; usually a wrong or rotated secret), Paused or Waiting for first event. Next to it are the last 24 hours: events per hour, delivery success and rejected requests.

A new source's page waits for its first event and updates by itself when it arrives. Each source page also breaks down the last 7 days by event type (for example invoice.paid, read from the field each provider uses) and lists the last 50 rejected requests with the reason, such as a missing header, a timestamp outside the allowed window, or a signature that doesn't match. Bodies of rejected requests are never stored.

Verifying senders

When you create a source, first pick the provider: Stripe, PayPal, Lemon Squeezy, QuickBooks, Shopify, HubSpot, GitHub, GitLab, Linear, Vercel, AWS SNS, Docker Hub, OpenAI, Clerk, Supabase or Standard Webhooks. HookStead then sets up verification for that provider and shows its setup steps, so you only paste the secret. Choose Custom / other to pick the method yourself under Verify requests with. Requests that fail get 401 and are never stored. Signatures are compared in constant time.

OptionChecksSecret
Stripe signatureStripe-SignatureThe endpoint's whsec_… from Stripe (5-minute timestamp tolerance)
GitHub signatureX-Hub-Signature-256The webhook secret you set in GitHub
Shopify signatureX-Shopify-Hmac-Sha256Your app's client secret
Standard Webhookswebhook-signature or svix-signatureThe sender's whsec_… secret (also OpenAI and Clerk)
Custom HMAC-SHA256A header you name, hex or base64, optional prefixYour shared secret
Secret headerA header you name must equal the secretAny long random string
Basic authAuthorization: Basicuser:password
Vercel signaturex-vercel-signatureThe webhook's secret (HMAC-SHA1)
HubSpot signature (v3)X-HubSpot-Signature-v3Your app's client secret (5-minute timestamp tolerance)
AWS SNS signatureCertificate signature in the message body; optional topic ARN allowlistNone: SNS's certificate is fetched from AWS. Subscriptions are confirmed automatically
PayPal signaturePAYPAL-TRANSMISSION-SIGNone: your PayPal webhook ID, plus PayPal's certificate
NoneNothing: anyone with the URL can send—

With None, the URL is the only secret. Use it only for senders that can't sign, and rotate the URL if it leaks.

Step-by-step setups: Stripe, GitHub.

Duplicates

Senders retry, so the same webhook can arrive twice. Set Dedupe header (optional) to a header that is unique per webhook (GitHub: X-GitHub-Delivery). A repeat is answered with 200, "duplicate": true and the original event's id, and isn't stored or delivered again. Through the API you can dedupe on a JSON field instead (e.g. $.id for Stripe).

Redaction and the original

What you see in the dashboard is a copy with sensitive values replaced by [redacted]: headers such as authorization, cookie and anything containing signature, token or secret, and body fields such as password, card_number, cvv and iban. Add your own patterns per source through the API (redactionRules).

The original request is stored encrypted and forwarded unchanged. Admins can open it with Reveal original; every view is recorded in the audit log.

What senders get back

StatusMeaning
202Stored (or your source's configured status). The body has the event id.
200 duplicateSeen before (dedupe). The original event id is returned.
401The verification check failed. Nothing is stored.
404Unknown, disabled or deleted source. Always the same response.
405A method the source doesn't accept.
413The body is over your plan's or the source's size limit.
429Rate limited, or the monthly event quota is used. Retry-After says when to try again.
503Temporarily unable to store. Senders retry these.

03 / Delivering webhooks

Delivering webhooks

Each event goes to every connected destination, byte for byte, until your endpoint accepts it.

Destinations and connections

A destination needs a Name and an HTTPS URL. Optional settings: the Method, the Timeout (seconds) (15 by default, up to 60), Max attempts (capped by your plan), extra headers (stored encrypted, masked in the dashboard), and how many deliveries may be in flight at once (10 by default).

Connect sources from the destination's page. A connection can be disabled without deleting it; new events then aren't delivered there.

Destinations must be public: addresses on private, loopback and cloud-metadata networks are refused, and redirects are never followed.

Retries and backoff

Any 2xx answer counts as delivered. Anything else (or no answer within the timeout) is retried with exponential backoff with jitter: the first retry comes within about 30 seconds, and the wait can double with each attempt, up to 6 hours between attempts. A Retry-After header from your endpoint is honored.

  • 4xx answers (except 408 and 429) are final unless you turn on retrying them: they usually mean the request itself is wrong.
  • 410 Gone stops retries and flags the destination.
  • After the last attempt the delivery is failed (dead). You can retry it any time.

Verifying our signature

Every attempt carries three headers your endpoint should check:

HeaderContent
webhook-idThe delivery id. The same on every retry: store it to skip duplicates.
webhook-timestampUnix seconds when this attempt was signed. Reject anything more than 5 minutes off.
webhook-signature`v1,` plus the base64 HMAC-SHA256 of {id}.{timestamp}.{raw body}, keyed with the destination's secret. Two values while a secret rotates.
verify.mjs
import { createHmac, timingSafeEqual } from "node:crypto";

// secret: your destination's signing secret ("whsec_…")
// headers: the request headers; rawBody: the body as received (a Buffer), before JSON.parse
export function verifyHookstead(secret, headers, rawBody, toleranceSec = 300) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSec) return false;

  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest();
  return signatures.split(" ").some((entry) => {
    const [version, sig] = entry.split(",");
    const given = Buffer.from(sig ?? "", "base64");
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
}

The scheme is Standard Webhooks, so any Standard Webhooks library works. Complete receivers for Node.js, Python and Go: Verify signatures.

The sender's own signature header (e.g. Stripe-Signature) is forwarded unchanged, but it expires after a few minutes, so a later retry or replay fails it. Verify HookStead's signature instead.

Rotating a signing secret

  1. On the destination's page, choose how long the old secret keeps signing (Keep the old secret signing for) and Rotate signing secret. The new secret is shown once.
  2. During that time every delivery carries two signatures, old and new. Update your endpoint to the new secret whenever it suits you.
  3. When the overlap ends, only the new secret signs.

Pausing a destination

Pause deliveries holds new deliveries instead of sending them, for example during a maintenance window. Resume deliveries sends everything that was held. Deleting a destination cancels its pending deliveries.

04 / Debugging and recovery

Debugging and recovery

Find what went wrong, fix your endpoint, and send it again. Nothing is lost while you do.

A delivery's attempts

Open a delivery to see every attempt: when it ran, what triggered it (first try, automatic retry, manual retry or replay), the request that was sent, and the response: status, headers, the start of the body, and how long it took. Errors are named (TIMEOUT_RESPONSE, HTTP_5XX, CONNECTION_REFUSED, …) so you can filter by them.

Retry, replay, bulk retry

ActionWhereWhat it does
RetryA deliverySends the same delivery again now: same webhook-id, so your endpoint can dedupe it.
Replay eventAn eventCreates new deliveries of the event to the destinations you choose, with new webhook-ids. Counts toward your monthly events.
Bulk retryDeliveries, with a filterRetries up to 1,000 matching deliveries in the background, with progress. One bulk job per workspace at a time.

A double click never sends twice: each of these carries an idempotency key. Replays are rate limited per organization and per person.

05 / Monitoring and alerts

Monitoring and alerts

Know about a broken endpoint before your customers do.

Destination health

Each destination shows a health status, from its last 15 minutes:

StatusWhen
HealthyEverything else.
Degraded2 failures in a row, or at least 10% of attempts failed (with 5 or more attempts).
Failing5 failures in a row, or at least 50% of attempts failed (with 5 or more attempts).

Metrics

Metrics shows events received, delivered and failed attempts, the success rate and p95 latency for the last 24 hours, 7 days or 30 days. Each chart has a table view.

Alert rules

Alerts → Create alert rule. Alerts are emailed to members of your organization you pick.

WhenFires when
Failure rateThe share of failed attempts in the window reaches your threshold (with enough attempts).
Failures in a rowA destination fails your chosen number of times in a row.
Failed deliveriesDeliveries give up (fail for good) in the window.
Source inactiveA source receives nothing for the window, e.g. a sender that stopped sending.

You get one email when an incident opens and one when it resolves (after 3 quiet minutes). A quiet period stops repeat emails for an endpoint that keeps flapping. Acknowledge an incident to show your team someone is on it.

06 / Team and settings

Team and settings

Who can do what, and the trail they leave.

Roles

RoleCan
ViewerSee everything in the organization's workspaces: events (redacted), deliveries, metrics, alerts.
MemberAlso create and change sources, destinations, connections and alert rules; retry and replay.
AdminAlso change verification and redaction, view originals, manage workspaces, members, API keys and the audit log.
OwnerAlso manage billing and grant the owner role. Every organization keeps at least one owner.

Invite people under Settings → Members → Send invite. Invitations are single-use, valid for 7 days, and only work for the invited email address.

Workspaces

Settings → Workspaces. Use one per environment, so test traffic never mixes with production. Members and roles apply to all workspaces of the organization. Deleting a workspace deletes its sources, destinations and events.

API keys

Settings → API keys. A key belongs to the organization, has a role (up to Admin), and can expire after 1 to 365 days. It's shown once; we store only a hash. Revoking takes effect immediately. Keys can never manage keys, members or billing.

Audit log

Settings → Audit log (admins) records security-relevant changes: members and roles, API keys, secrets, views of original requests, retries and replays, billing, and deletions, with who did it and from where.

07 / Plans and billing

Plans and billing

Every plan has every feature. Plans differ in volume, retention and team size.

Plans and limits

FreeProTeam
Events per month10,000500,0005,000,000
Retention3 days14 days30 days
Workspaces1310
Sources / destinations per workspace3 / 325 / 25100 / 100
Members1525
Delivery attemptsup to 3up to 8up to 15

Prices are on the pricing page. Events older than your plan's retention are deleted automatically.

Usage and the monthly quota

Settings → Billing shows this month's usage: each webhook received counts once, however many attempts its deliveries take; each replayed delivery counts once more. Owners get an email at 80% and 100%. At 100%, new webhooks are answered with 429 and Retry-After until the month resets (on the 1st, UTC). Senders retry 429s, so upgrading lets them through.

Changing plans

  • Upgrade: Settings → Billing → Upgrade to Pro or Upgrade to Team, monthly or yearly. Checkout is handled by Stripe; the new limits apply as soon as the payment is confirmed.
  • Change, cancel, update your card or download invoices: Manage billing opens Stripe's customer portal.
  • Downgrades and cancellations take effect at the end of the period you paid for. Nothing is deleted, but you can't create more than the new plan allows.
  • A failed payment keeps your plan for 7 days while you update your card.

Only owners can change billing.

08 / REST API

REST API

Everything in the dashboard is in the API, over HTTPS and JSON.

Basics

  • Base URL: https://<your HookStead>/api/v1. Authenticate with Authorization: Bearer hw_live_….
  • Errors are application/problem+json with a stable code (validation_failed, plan_limit_reached, quota_exceeded, …). Another organization's resources always answer 404.
  • Retry, replay, bulk retry and checkout require an Idempotency-Key header: the same key and body return the stored response for 24 hours.
Call the API
curl https://<your Hookstead>/api/v1/me   -H "authorization: Bearer hw_live_…"

Every endpoint, with its parameters and rules: API reference. The OpenAPI 3.1 description for code generators is at /api/openapi.json.

09 / Troubleshooting and FAQ

Troubleshooting and FAQ

The usual suspects, and what to do about them.

Receiving

My sender gets 401 and nothing shows up.

The verification check failed. Check that the source's secret is the one the sender signs with (for Stripe: that endpoint's signing secret, not your API key), and that nothing between the sender and HookStead changes the body. For timestamped schemes (Stripe, Standard Webhooks), the sender's clock must be within 5 minutes.

My sender gets 404.

The URL is wrong, the source is disabled or deleted, or its URL was rotated. Copy the URL again from the source's page.

My sender gets 413 or 429.

413: the body is bigger than your plan or the source allows. 429: you're over the ingest rate, or this month's quota is used up (see Usage). Both say what the limit is; 429 also says when to retry.

Delivering

Deliveries fail with a timeout.

Your endpoint took longer than the destination's timeout. Answer 2xx right away and do the work afterwards (a queue or a background job), or raise the timeout (up to 60 seconds).

My endpoint rejects HookStead's signature.
  • Verify the raw body exactly as received, before any JSON parsing or re-encoding.
  • Use the destination's secret (whsec_…), base64-decoded after the prefix, not the source's or the sender's secret.
  • Accept any of the space-separated signatures: there are two while a secret rotates.
  • Check your server's clock if you enforce the 5-minute window.
The destination URL is refused.

HookStead delivers only to public HTTPS addresses. For local development, expose your endpoint with a tunnel (e.g. a public HTTPS URL that forwards to your machine).

My endpoint received the same webhook twice.

Deliveries are at least once: a retry after a timeout can arrive although your endpoint did process the first attempt. Store each webhook-id you handle and answer 2xx again for a repeat.

Frequently asked questions

Is anything lost if my endpoint is down for hours?

No. Events are stored before the sender gets its answer, and retries continue with backoff until the destination's last attempt. After that, retry or bulk-retry the failed deliveries once you're back, within your plan's retention.

Who can see webhook contents?

Members of your organization see the redacted copy. Only admins can reveal the original, and every view is logged. Originals and secrets are encrypted at rest.

Can I self-host HookStead?

Yes: it ships as Docker images with a production compose file. With billing turned off, every organization gets one plan's limits and nothing talks to Stripe.

Where do I get help?

Email support@hookstead.dev. Include the event or delivery id from the dashboard, never secrets.