The optional state endpoint
Four of Hookproof's seven checks (replay, duplicate-delivery-id,
out-of-order-delivery, and the "never applied" half of bad-signature)
depend on knowing what actually happened inside your system after a test delivery — not just
what HTTP status you returned. Without this endpoint, Hookproof asks you instead — the report
tells you exactly what was sent, plus one specific question you check against your own logs.
With it, Hookproof asks your system directly and settles all seven automatically. One round
trip instead of a back-and-forth.
Is this required?
No. Every check still runs without it — you just get a worksheet for the four above instead of a verdict. This is worth building if you'd rather not manually check logs after a run, or if you want repeatable re-runs (a retainer engagement) to settle automatically each time.
What it needs to do
One route, on a host you control — typically staging, never production. Read-only: it reports state, it never accepts writes.
GET {your-base-url}/{runId}
Hookproof calls this once per check, per run, with the run's actual ID swapped in. You give
Hookproof {your-base-url} when a test is scheduled — it's a URL you supply, not
something forced onto a fixed path on your host.
What it needs to return
HTTP 200, JSON body, with whichever of these four fields are known for that run:
{
"appliedCount": 1,
"appliedSequence": [1, 2, 3],
"duplicateAttempts": 1,
"signatureRejections": 0
}
- appliedCount — how many times a delivery from this run was actually processed (your real effect: the row inserted, the balance changed, the email sent). Duplicates you correctly detected and skipped do not count.
- appliedSequence — the order deliveries were actually applied in, using the
sequencenumber Hookproof puts on each test delivery. Not necessarily wire order — if you buffer and reorder, or reject stale sequences outright, that's a legitimate design; report what your system actually did. - duplicateAttempts — how many deliveries you recognized as duplicates and skipped instead of re-applying.
- signatureRejections — how many deliveries you rejected for a bad signature.
Optional; only used to corroborate
bad-signature, which mainly grades your HTTP response directly.
A run ID Hookproof hasn't sent you yet, or one you've never seen, should return
200 with everything at zero/empty — not a 404. A few checks call this before any
delivery for that run has landed, and a 404 there would look identical to "endpoint not
implemented" and quietly turn off auto-determination for your whole run.
Minimal implementation
This is genuinely small. A per-run counter, no database required for a staging deployment:
const runs = new Map(); // runId -> { appliedCount, appliedSequence, duplicateAttempts }
function stateFor(runId) {
if (!runs.has(runId)) {
runs.set(runId, { appliedCount: 0, appliedSequence: [], duplicateAttempts: 0 });
}
return runs.get(runId);
}
// ...inside your normal webhook handler, alongside your real dedupe logic:
if (alreadySeen) {
stateFor(runId).duplicateAttempts += 1;
} else {
stateFor(runId).appliedSequence.push(event.sequence);
stateFor(runId).appliedCount += 1;
}
// the read-only inspection route:
app.get('/inspect/:runId', (req, res) => {
const state = runs.get(req.params.runId) || { appliedCount: 0, appliedSequence: [], duplicateAttempts: 0 };
res.status(200).json(state);
});
runId is a value Hookproof generates per test suite — it isn't your own delivery
ID or idempotency key, so it needs to be something your handler can read off each test delivery
(Hookproof sends it as a field on the payload) and use as the map key above.
Security notes
- Put this behind whatever staging already has — an IP allowlist, a basic-auth gate, a VPN. It exposes counts and sequence numbers, not payloads or secrets, but it shouldn't be casually public either.
- Never deploy it to production. It only needs to exist for the duration of a test engagement.
- It never receives your signing secret or any request body content — only counts you're already tracking for your own dedupe logic.
Have questions about wiring this into your specific stack, or want to confirm the URL before a scheduled run? Just ask — this is meant to save you time, not become its own project.
Get in touch