Local webhook development tunnels, test events, replay workflows
Local webhook development is painful because inbound traffic and signature verification are fragile.
This guide shows practical workflows for tunnels, test events, capture/replay, and faster local iteration loops.
No credit card required
TL;DR
- If you must receive webhooks locally, use a tunnel (ngrok/cloudflared) with a stable HTTPS URL.
- For faster iteration, avoid inbound altogether: receive in a managed endpoint and consume locally from a queue/stream.
- Develop against real payloads: capture raw requests, sanitize secrets/PII, and replay deterministically.
- Verify signatures over raw bytes and handle timestamp max-age (clock skew is common on laptops).
- Have a replay workflow: regressions are easiest to catch with saved payload fixtures.
If you are failing signature checks, read webhook security.
Anti-patterns
- Hardcoding ephemeral tunnel URLs into provider settings.
- Skipping signature verification in development and getting surprised in production.
- Testing only “happy path” payloads and ignoring retries/duplicates.
For production triage, use the debugging playbook.
Core concepts
Local dev is a tradeoff between realism (real provider traffic) and speed (tight iteration loops).
Inbound options
Tunnels provide a public HTTPS URL to your laptop. Managed receivers avoid inbound entirely by letting you pull/stream events locally.
Fixtures + replay
Captured raw payloads become regression fixtures. Replay is the fastest way to reproduce bugs and validate fixes.
Signature realism
If you skip signature verification locally, you will ship broken auth. Keep the verification code path consistent with production.
Simple flow
Provider
Sends events
test or real
Receiver
Managed endpoint
persist + verify
Local dev
Pull/stream
iterate fast
Tunnels are still useful, but for many teams, “receive managed, consume locally” is faster and safer.
Local dev checklist
A copy/paste checklist for building a local loop that matches production semantics.
- [ ] Choose approach:
- [ ] Tunnel inbound (ngrok/cloudflared) OR
- [ ] Managed receiver + local consumer (recommended for iteration)
- [ ] Use HTTPS and a stable URL (avoid frequent URL churn)
- [ ] Capture raw payload bytes + headers for debugging and replay
- [ ] Sanitize secrets and PII before sharing fixtures
- [ ] Signature verification over raw bytes (same code path as production)
- [ ] Timestamp validation (max age + clock skew tolerance)
- [ ] Simulate retries and duplicates; verify idempotency
- [ ] Add a replay command/script in your repo (fixtures → processing)
- [ ] Log correlation IDs and processing outcome for every delivery Reference implementation
A local worker pattern that processes events without exposing an inbound endpoint on your laptop.
Node
Local pull + Ack/Nack
// Local worker: pull webhook events from a Hooque consumer queue and process them on your laptop.
// This avoids inbound tunnels for many workflows.
const QUEUE_NEXT_URL =
process.env.HOOQUE_QUEUE_NEXT_URL ??
"https://app.hooque.io/queues/cons_dev_webhooks/next";
const TOKEN = process.env.HOOQUE_TOKEN ?? "hq_tok_replace_me";
const headers = { Authorization: `Bearer ${TOKEN}` };
async function processLocally(payload) {
// TODO: call your local code paths here
console.log("received event:", payload?.type ?? payload?.id ?? "event");
}
while (true) {
const resp = await fetch(QUEUE_NEXT_URL, { headers });
if (resp.status === 204) break;
if (!resp.ok) throw new Error(`Hooque next() failed: ${resp.status}`);
const payload = await resp.json();
const meta = JSON.parse(resp.headers.get("X-Hooque-Meta") ?? "{}");
try {
await processLocally(payload);
await fetch(meta.ackUrl, { method: "POST", headers });
} catch (err) {
await fetch(meta.nackUrl, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ reason: String(err) }),
});
}
} Python
Local pull + Ack/Nack
# Local worker: pull webhook events from a Hooque consumer queue and process them locally.
import json
import os
import requests
QUEUE_NEXT_URL = os.getenv(
"HOOQUE_QUEUE_NEXT_URL",
"https://app.hooque.io/queues/cons_dev_webhooks/next",
)
TOKEN = os.getenv("HOOQUE_TOKEN", "hq_tok_replace_me")
headers = {"Authorization": f"Bearer {TOKEN}"}
def process_locally(payload: dict) -> None:
print("received event:", payload.get("type") or payload.get("id") or "event")
# TODO: call your local code paths here
while True:
resp = requests.get(QUEUE_NEXT_URL, headers=headers, timeout=30)
if resp.status_code == 204:
break
if resp.status_code >= 400:
raise RuntimeError(f"Hooque next() failed: {resp.status_code} {resp.text}")
payload = resp.json()
meta = json.loads(resp.headers.get("X-Hooque-Meta", "{}"))
try:
process_locally(payload)
requests.post(meta["ackUrl"], headers=headers, timeout=30)
except Exception as err:
requests.post(
meta["nackUrl"],
headers={**headers, "Content-Type": "application/json"},
json={"reason": str(err)},
timeout=30,
) Common failure modes
Local workflows break for boring reasons: clock skew, raw-body mismatch, and mismatched environments.
Signature checks fail locally
Likely causes
- Verifying over parsed JSON, not raw bytes.
- Clock skew breaks timestamp max-age.
- Using the wrong secret (test vs prod).
Next checks
- Capture raw bytes and verify offline.
- Sync clock and allow small skew.
- Confirm provider environment and secret version.
Tunnel URL keeps changing
Likely causes
- Ephemeral tunnel domains.
- Provider settings hardcoded to old URL.
- Multiple devs sharing one endpoint.
Next checks
- Use a reserved/static tunnel domain.
- Prefer managed receive + local consume.
- Use separate endpoints per developer when possible.
Local code works, but production fails
Likely causes
- Different parsing/middleware in prod.
- Different timeout and concurrency behavior.
- Missing idempotency under retries.
Next checks
- Keep the same raw-body verification path.
- Simulate retries/duplicates locally.
- Instrument latency and add backoff policies.
How Hooque helps
Hooque gives you a stable, managed receive surface and a simple local dev loop: pull/stream messages and Ack/Nack.
- Hosted webhook endpoints remove the need for tunnels in many workflows.
- Signature verification at ingest keeps local workers focused on business logic.
- REST pull or SSE stream makes local iteration predictable (no inbound connectivity).
- Ack/Nack/Reject control lets you simulate retries and quarantine failures safely.
- Inspection and replay surface makes “capture once, replay many” easy.
FAQ
Common local development questions: tunnels, signatures, fixtures, and replay.
Do I need ngrok to develop webhooks locally?
Not always. Tunnels are useful when you must receive inbound requests on your laptop, but many workflows are easier if you receive in a managed endpoint and consume locally from a queue or stream. With Hooque, you can use a hosted webhook endpoint and pull/stream messages locally without exposing your laptop to inbound traffic.
How can I test webhooks without real provider traffic?
Use provider “test event” tools when available, or capture a real payload once, sanitize it, and replay it through your processing code. Always include signature verification paths in your tests. With Hooque, you can persist real payloads durably and replay them through your workers after changes.
Why do signature checks fail more often in local development?
Clock skew and inconsistent body parsing are common locally. If the provider signs raw bytes, verifying over parsed JSON will fail. Timestamp validation can also fail if your local clock is off. With Hooque, signature verification can happen at ingest so your local worker only sees verified events.
How should I store webhook fixtures for replay?
Store the raw body bytes (or exact JSON text) plus key headers (content-type, signature headers, provider request IDs). Keep fixtures small and sanitize secrets and PII. With Hooque, you can keep an inspectable history of received payloads and use it as your replay source.
How do I simulate retries and duplicates locally?
Replay the same fixture multiple times and ensure your code is idempotent (dedupe keys, unique constraints, idempotency keys). Also simulate out-of-order events if the provider does not guarantee ordering. With Hooque, you can Nack to re-deliver and validate idempotency using explicit Ack/Nack/Reject controls.
What is a good local workflow for webhook debugging?
Capture one failing payload, replay it deterministically, add structured logs around verification and processing, and only then re-enable production delivery or replays. With Hooque, you can inspect the exact payload and metadata that failed and replay it through your worker after a fix.
Start processing webhooks reliably
Build a local loop that matches production delivery semantics — then ship with confidence.
No credit card required