Skip to main content
Practice the signed webhook contract without creating an endpoint or sending a production delivery. This example verifies exact raw bytes, saves the complete event before responding, and recognizes a retry after a restart.

Run locally

Use Node.js 22 or later on macOS or Linux with a local filesystem that supports file and directory sync. Create an empty folder and copy all three files below into it using their displayed names. No package installation is needed.
In a second terminal, set the same local secret in TFA_WEBHOOK_SECRET and run node fixture.mjs. The sender only contacts http://127.0.0.1:8787/webhooks/thefaithapp. First delivery returns 202 {"accepted":true,"duplicate":false}. Repeat it to receive 200 {"accepted":true,"duplicate":true}. Stop and restart the receiver in the same folder, then repeat the fixture to check that the saved event is recognized. Inspect inbox/ to see the complete fictional event.
This loopback HTTP URL is for the local fixture only. It cannot be registered as a TheFaithApp endpoint. A real endpoint must use publicly reachable HTTPS.

What acceptance means

The receiver signs nothing itself: it calculates the expected HMAC-SHA256 over the timestamp, a period, and the exact incoming bytes, then compares with timingSafeEqual. Header event ID and type must match the signed body. Each fixture retry has a fresh timestamp and the same event ID and body. A single process serializes acceptance, writes a restricted-permission file, syncs it, renames it into the inbox and syncs the directory before returning 202. An identical saved event returns 200 after its file and directory are synced again. Changed signed content with the same ID returns 409; storage failure returns 503 so the sender can retry. The five-minute timestamp window and 1 MiB body limit are example policies, not TheFaithApp server settings. Keep the receiver clock synchronized. The tests cover signatures, changed raw bytes, stale and future timestamps, missing/repeated headers, malformed JSON, envelope mismatch, storage failure, concurrent duplicate requests, restart persistence, and waiting for acceptance before acknowledging. The fictional Unicode/slash vector was cross-checked against the PHP sender’s JSON flags and HMAC calculation.

Example files

receiver.test.mjs

Before adapting this for production

Run one process per inbox. This sample has no distributed lock, business worker or database transaction. It accepts events into storage; it does not apply business changes or guarantee exactly-once business processing. For a production service, use a transactional inbox/queue and an idempotent worker, HTTPS ingress, a secret manager, monitoring and a retention policy. Keep accepted records throughout your retry horizon: deleting the inbox also deletes deduplication history. Test persistence on your actual storage. If file or directory sync fails, the example fails rather than acknowledging unsynced data. Use only fictional data locally; the included fixed test secret is not a production credential. The example logs neither request bodies nor secrets. See Node’s file sync and raw HTTP headers references for the underlying Node APIs.