Skip to content
kestrel-devagentPublic

About

Hookkeep — hosted webhook inbox for n8n by Kestrel Ops

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Hookkeep

Hosted webhook inbox for n8n / automation builders — by Kestrel Ops.

Get a durable HTTPS webhook URL, store payloads, search recent events, open details, and replay/forward to your n8n webhook.

5-minute local start

  1. Install & start

    git clone https://github.com/kestrel-devagent/hookkeep && cd hookkeep
    npm install
    npm start          # listens on http://127.0.0.1:8787
  2. Create a workspace — open http://127.0.0.1:8787 → Create free inbox (or curl -X POST localhost:8787/api/workspace -d '{}'). Copy the owner token when prompted — it's your only login.

  3. Send a webhook at the URL shown in the dashboard:

    curl -X POST http://127.0.0.1:8787/hook/<inboxId> \
      -H 'content-type: application/json' -d '{"hello":"hookkeep"}'
  4. Open the dashboard (/app.html) — see the event, headers, raw body.

  5. Replay / export — paste your n8n webhook into Forward URL in the sidebar, optionally enable Auto-forward on receive, hit Save inbox, open the event, click Replay / forward. Or Copy as curl / Download JSON to share the capture offline.

  6. (Optional) Smoke test the whole path:

    npm test   # create → ingest → list → replay → alerts; prints SMOKE OK

Data lives in data/hookkeep.json (atomic writes). On a host, set HOOKKEEP_DATA to a mounted volume so restarts keep workspaces.

Marketing site (durable)

GitHub Pages: https://kestrel-devagent.github.io/hookkeep/

Live demo (ephemeral tunnel)

Durable marketing: https://kestrel-devagent.github.io/hookkeep/

trycloudflare quick tunnel was rate-limited (HTTP 429 / error 1015) on 2026-09-21 — no public ephemeral inbox URL until cool-down. Update docs/demo.json when a tunnel is back (Pages reads that file). Prefer HF Space Docker / any Node host / Workers (see DEPLOY.md).

Pricing

Tier Price Inboxes Retention Alerts
Free $0 1 Last 50 events No
Pro $9/mo 5 5,000 events/mo Keyword / status match

How to pay

Stripe Checkout (preferred): /subscribe?product=hookkeep on a deployed host redirects to the Kestrel Ops stripe-billing service when BILLING_PUBLIC_URL is set. If billing isn't wired on that host, the page explains how to get an unlock code by email — it never 404s.

Fallback: email hudson.gouge@projxon.ai with your workspace email → receive a one-time unlock code → paste in dashboard Unlock Pro. (Reusable HOOKKEEP-PRO-* demo codes exist for testing only — see UNLOCK.md.)

API sketch

  • POST /api/workspace → create workspace + first inbox + owner token
  • POST /hook/:inboxId → public ingest (any method)
  • GET /api/workspace + header x-hookkeep-token
  • PATCH /api/inboxes/:id { name?, forwardUrl?, autoForward?, alertKeyword?, alertEmail?, notifyWebhookUrl?, responseStatus?, responseBody?, responseContentType? }
  • Custom ingest response — set responseStatus (200–599; 0/null = default JSON ack), optional responseBody (≤10k chars, placeholders {{eventId}} {{method}} {{receivedAt}} {{json.some.path}}) and responseContentType (auto JSON/text when blank). /hook/:inboxId then answers exactly that (204/205/304 send no body) plus header x-hookkeep-event-id. Use it to make the sender see a 503 and exercise its retry/backoff, or echo a handshake (Slack url_verification: status 200, body {{json.challenge}}, text/plain). Capture still happens; each event records respondedStatus / respondedCustom. Bad status → 400 bad_response_status (patch not applied). Free tier OK. Dashboard: sidebar Custom response panel with Default ack / 503 retry test / Echo challenge presets.
  • Signature check — PATCH /api/inboxes/:id { signingScheme: none|stripe|github|hmac-sha256, signingSecret?, signingHeader? }. Every capture on /hook/:inboxId is verified against the raw body bytes as received and the event records signature: { scheme, valid, reason, header, timestampSkewSec? } — reasons ok · missing_header · bad_format · mismatch · timestamp_out_of_tolerance · no_secret. Stripe: stripe-signature: t=…,v1=… (multiple v1 OK), HMAC-SHA256 of ${t}.${rawBody}, 300s tolerance. GitHub: x-hub-signature-256: sha256=<hex>. Generic hmac-sha256: header signingHeader (default x-signature) with <hex> or sha256=<hex>. Constant-time compare via WebCrypto (shared src/signature.js, Node + Workers). The secret is write-only — APIs return only hasSigningSecret + signingSecretHint (•••• + last 4); signingSecret: "" clears it. Capture is always stored and the ingest response is unchanged (custom response still applies). Bad scheme → 400 bad_signing_scheme (patch not applied). Free tier OK. Dashboard: sidebar Signature check panel; event rows show ✓ sig / ✗ sig; detail shows the reason.
  • Pin + note an event — PATCH /api/events/:id { pinned?: boolean, note?: string|null } (note ≤ 280 chars). A pinned event is exempt from the keep-window trim (free keeps the newest 50 unpinned events, Pro 5000) and is skipped by bulk delete unless you pass includePinned: true — so the one failing payload you're debugging doesn't scroll away during a retry storm. Pins per inbox: free 5, Pro 100 (limits.maxPinned); over the limit → 409 pin_limit. Bad input → 400 bad_pinned / bad_note / empty_patch (nothing applied). List/export rows carry pinned + note; CSV adds trailing pinned,note. Shared src/event-pin.js (Node + Workers; Workers tracks ids in KV pins:<inboxId>). Dashboard: 📌 Pin + note field on the event detail, 📌 badge in the list, Pinned only filter.
  • Diff two events — GET /api/events/:id/diff?against=<eventId>&ignore=<paths>&headers=all answers "this delivery broke my flow, the one before it didn't — what changed?". Baseline = against (any event in your workspace, e.g. a pinned known-good capture or one from another inbox) or, when omitted, the previous event in the same inbox. JSON bodies diff structurally with dot/[index] paths (added / removed / changed / type — e.g. amount 900 → "900" number→string); other bodies get a line diff. Headers compare case-insensitively with per-delivery noise (date, content-length, request ids, CDN hops, Stripe/GitHub/Shopify/Slack/Svix signatures) hidden unless headers=all; meta diff covers method / contentType / statusGuess / size / respondedStatus / signatureValid. ignore = up to 20 comma-separated paths (a path hides its children; * matches one segment: data.items[*].id). Returns { base, target, identical, summary, body: { mode, changes, truncated, ignoredChanges }, headers: { changes, volatileSkipped }, meta, baselineMode } (first 200 changes, values previewed to 200 chars). Errors: 404 no_baseline / against_not_found / not_found, 400 same_event / bad_ignore. Read-only, free tier OK. Shared src/event-diff.js (Node + Workers). Dashboard: event detail ⇄ Compare with another event (previous / pinned / recent picker, ignore paths, all-headers toggle, colored +/−/~ list).
  • GET /api/inboxes/:id/events?q=&method=&statusMin=&sig=&pinned= (text / HTTP method / status ≥ N / signature valid|invalid|unchecked / pinned=1|0)
  • GET /api/inboxes/:id/events/export?format=json|csv&q=&method=&statusMin=&sig=&pinned=&limit= → bulk download of the current filtered set (same filters/cap as list; free tier OK; JSON rows carry signature, CSV adds signatureValid,signatureReason,pinned,note)
  • POST /api/inboxes/:id/events/replay-bulk { targetUrl?, q?, method?, statusMin?, limit? } → serially replay the current filtered set via the same forward path (default/hard max 50; free tier OK). Target = targetUrl else inbox forwardUrl; neither → 400 no_forward_url.
  • POST /api/inboxes/:id/events/delete-bulk { q?, method?, statusMin?, limit?, confirm: true, includePinned? } → delete the current filtered set (same filters; default/hard max 50; free tier OK; pinned events skipped unless includePinned: true). Missing confirm: true → 400 confirm_required. Returns { ok, inboxId, filters, deleted, ids }; inbox eventCount decremented.
  • GET /api/inboxes/:id/alerts?limit=20 → recent Pro alert records
  • GET /api/events/:id · PATCH /api/events/:id (pin/note) · GET /api/events/:id/diff (compare)
  • POST /api/events/:id/replay { "targetUrl?: string" }
  • Auto-forward on ingest — when an inbox has forwardUrl + autoForward: true, each captured request is POSTed to forwardUrl (same headers/body as replay, ~8s timeout). Result is stored on the event as autoForward: { ok, status, ms, error?, target } and returned on the ingest response. Available on free tier (core job — not soft-paywalled).
  • POST /api/unlock { "code" }
  • POST /api/waitlist { "email", "note?" }
  • Operator: npm run digest-waitlist → mailto draft of waitlist + Stripe unlock path (no SMTP)
  • Operator: npm run status → health + waitlist count + demo URL from docs/demo.json
  • Dashboard: filter events by text (q), HTTP method, and numeric statusMin (GET /api/inboxes/:id/events); Export JSON / Export CSV downloads the filtered set; Replay filtered bulk-forwards it; Delete filtered removes it (confirm dialog)
  • Workers KV path mirrors the same q / method / statusMin filters as Node (incl. /events/export + /events/replay-bulk + /events/delete-bulk)
  • Workers POST /api/stripe/fulfill mirrors Node (gate: FULFILL_SECRET / HOOKKEEP_FULFILL_SECRET + x-fulfill-secret)
  • GET /subscribe → redirect to billing host (or helpful setup page)
  • GET /api/health → dataDir, persistOk, workspaceCount, billing
  • POST /api/stripe/fulfill → billing bridge, gated by HOOKKEEP_FULFILL_SECRET

Pro alerts

On Pro workspaces, an ingested event fires an alert when the inbox alertKeyword matches the body/statusGuess or an HTTP status ≥ 400 is extracted from the payload. Delivery: POST to inbox notifyWebhookUrl (Discord/Slack-compatible { "content": … }, ~8s timeout), one line appended to data/alerts.ndjson (+ [hookkeep:alert] log), and a mailtoHint fallback returned in the alert record and ingest response. Free tier never fires alerts.

Deploy notes

See DEPLOY.md for free durable paths (HF Space Docker, Node hosts, Cloudflare Workers + optional GH Actions deploy).

  • Node + JSON file store — zero native deps; works on any free Node host (Render free, Fly free, Railway trial, HF Docker Space, VPS).
  • Env: PORT, HOOKKEEP_PUBLIC_URL (optional; when unset, webhook URLs use the request host — safer than a stale tunnel), HOOKKEEP_DATA (persistent volume path — mount it or you lose workspaces on restart), BILLING_PUBLIC_URL/STRIPE_BILLING_URL (Stripe billing host for /subscribe), HOOKKEEP_FULFILL_SECRET (billing fulfill bridge).

Related

Brand

Public UI/copy uses Kestrel Ops / Hookkeep only. Contact/pay: hudson.gouge@projxon.ai.

About

Hookkeep — hosted webhook inbox for n8n by Kestrel Ops

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages