The problem
I kept hitting the same friction: every time a payment provider fired an event, I rebuilt the same automation from scratch. Copying a previous flow helped for about an hour, then the small differences took over — different payload shapes, different failure modes, different places to look when nothing arrived. So I built a template that captures an invoice webhook, verifies the signature, drops duplicates, and writes a row to Google Sheets.
How it works
- Webhook — the only entry point; every execution starts from a request you can replay.
- HMAC verify — checks the signature before any body is trusted. Runs before parsing on purpose.
-
Idempotency check — drops events whose
idwas already processed. - Append to Sheets — writes the normalized record as one consistent row.
- Error branch — collects failures so they surface instead of disappearing.
The order matters: verifying before parsing means a malformed or hostile body never reaches code that acts on it. The idempotency guard is the piece I used to skip, and it's the piece that actually saves time.
A worked example
POST to the webhook:
{
"id": "inv_001",
"customer": "Acme",
"amount": 4900,
"currency": "USD",
"x-signature": "sha256=..."
}
After HMAC passes and the id is new, one row lands in the Invoices sheet: inv_001 | Acme | 4900 | USD.
What broke
The first version passed every happy-path test and then lost invoices in production for two reasons that only show up under load. First, I treated the HMAC check as optional on "internal" traffic — it isn't, because the moment someone copies the webhook URL into a second service, "internal" stops being true. Second, the error branch wrote to a log nobody opened. An error branch that only appends to a file is not an error branch; it has to end somewhere a human looks.
What I'd do differently
Write the idempotency key format down before writing code. Retrofitting a key scheme onto events already in flight is the least fun migration. And I'd make the error branch post to a channel from day one, not "later when I have time" — later never comes, and that's exactly when invoices go missing.
What it actually does
Captures an inbound invoice webhook, authenticates it with HMAC, drops duplicates via an idempotency guard, then appends a structured row to Google Sheets. Services: any HTTP endpoint that can POST JSON; delivery target is Google Sheets (Slack or Notion are easy to swap in). Extend it with rate limiting, retry queues, or a second destination in parallel.
Setup
- In n8n open Workflows → Import from File and select
workflow.json. - Open the HMAC verify node and set
HMACto your shared secret. - Open the Append row to Google Sheets node and set the spreadsheet and sheet.
- Activate the workflow and copy the production webhook URL.
Key takeaways
- One entry point makes the whole pipeline testable — replay the request.
- Verify before parse. Always.
- An error branch that ends in a log file is a silent failure, not a safeguard.
Try it
Import the workflow, set your secret and sheet, and fire a test event. If a duplicate id comes in, nothing double-writes — that's the guard doing its job.
Top comments (1)
One gotcha worth adding to the HMAC step: most providers sign the raw request bytes, so in n8n you'll want the Webhook node's "Raw Body" option turned on. If the signature is checked against JSON that was already parsed and re-serialized, key order or whitespace can change and perfectly valid events start failing verification. Also curious where you keep the processed ids for the idempotency check: a separate Sheet tab, or n8n's workflow static data?