A practical guide for your team
n8n approval webhooks: duplicate callbacks and recovery.
Keep approval against the exact proposal in durable storage. When a callback repeats or a destination response fails, check the stored decision and destination receipt before making another write. This n8n recovery lab shows that pattern with authenticated HTTP callbacks and a synthetic destination.
What to keep in mind
The 5 October 2026 run passed 18 of 18 scenarios across 30 real n8n executions on version 2.35.4. It tested separate HTTP deliveries, ten concurrent approvals and recovery after abruptly restarting both lab processes. Decisions and receipts live in separate SQLite databases. The destination is fictional; these results do not establish production CRM reliability.
Run the webhook recovery lab
Download the complete recovery lab and open the reproduction instructions. On a Linux host with Python 3 and Docker, review the source and run sudo python3 run-lab.py from the extracted directory. A Linux user with UID 1000 and Docker access can run without sudo. The recorded host was ARM64 Linux; other host platforms were not evaluated.
The runner creates two disposable containers on a private network: n8n and a synthetic approval/destination fixture. It generates temporary header credentials, imports and publishes the three-node workflow inside that disposable instance, sends the test callbacks, exports results and removes the lab containers and network. It exposes no host port and uses separate state directories. Review the named-resource cleanup before running it.
Importing the workflow JSON alone is insufficient. The workflow needs the private fixture, seeded proposals and matching lab credentials. Keep it in the disposable lab; the fixed reviewer and synthetic writer are teaching components.
Authenticate the caller before accepting a decision
The n8n Webhook node supports authentication at the callback route. This lab uses Header Auth for one fixed reviewer credential. Its HTTP Request node maps the credential to reviewer-a; incoming actor or reviewer body fields do not choose the identity.
Missing and incorrect credentials were denied. A valid credential also failed to approve a proposal assigned to another reviewer. Supplying that other reviewer's name in the request body did not change the result. Authentication answers who can call; proposal ownership answers who can make this particular decision.
A real review portal needs its own user sessions, reviewer assignments and access policy. This lab's single credential is sufficient for its measured boundary but does not provide those capabilities. The fixture source shows where ownership, version and payload are checked.
Give concurrent callbacks one durable decision
Bind the approval to the proposal identifier, version, target record and exact note. The fixture hashes that tuple, compares it with the stored proposal and rejects a changed tuple. The server controls the deadline; an incoming timestamp cannot extend an expired review.
The decision transaction uses SQLite BEGIN IMMEDIATE and a unique decision row for each proposal. Ten concurrent approval requests produced one decision and one destination receipt. A concurrent approve/reject pair also produced one terminal decision. The first valid decision committed by the fixture wins; the test accepts either winner and verifies the matching destination effect.
This concurrency test used one n8n process and one fixture process. It does not evaluate queue mode, multiple workers or competing database processes. Adapt the transaction and ownership rules to the actual database rather than assuming this result transfers unchanged to another topology.
Check the destination after a response timeout
Approval and delivery have separate states. The approval transaction records an approved proposal with a pending delivery before calling the destination. The synthetic writer checks a stable action identifier for an existing receipt before creating an entry. Its receipt and target-version change commit in the same destination transaction.
In the lost-response scenario, the fixture committed the write and deliberately returned HTTP 504. The destination had one receipt while the delivery record remained pending. After the process restart, the harness sent the same callback once. The writer found the original receipt, returned reconciled and marked delivery complete. Its receipt identifier stayed unchanged.
HTTP 504 here is an injected failure response, not a measured external-provider network timeout. The fixture owns its receipt database and can enforce the unique action identifier. Before adapting this pattern to a CRM, establish which destination reference can be read back and what its write API guarantees. An unknown write result requires reconciliation; repeated execution alone cannot establish that another write is safe.
Preserve the pending action across a restart
The harness stopped both isolated containers with SIGKILL after the initial callback responses had completed. It then started them with the same database mounts. One proposal was still pending. Two other proposals were approved with pending delivery: one had failed before a write, and the other had a committed receipt with a failed response.
The three post-restart checks passed. The pending proposal could still be approved. The before-write failure produced one receipt after replay. The lost-response case reused its existing receipt. The source and dated result file expose the pre-restart records alongside the final counts.
Recovery was explicitly initiated by the test harness. The kit includes no autonomous recovery scheduler. It also does not test a process kill during the destination transaction or a resumed Wait execution. The Wait node's timeout and resumption settings are a separate transport choice. An expired review must still be rejected before any action is authorized.
Inspect all 18 recorded results
Recorded on 5 October 2026 with n8n 2.35.4 and Node 24.18.1. All 18 scenario assertions passed. The run retained 30 n8n execution records; the two injected destination failures are expected failed executions. A passing scenario means the resulting decision and receipt count matched the test, not that every webhook execution succeeded.
| Scenario | Check | Decision state | Delivery state | Receipts |
|---|---|---|---|---|
| Authenticated approval | Pass | approved | delivered | 1 |
| Missing callback credential | Pass | pending | none | 0 |
| Incorrect callback credential | Pass | pending | none | 0 |
| Authenticated reviewer does not own the proposal | Pass | pending | none | 0 |
| Incoming body tries to choose the reviewer | Pass | pending | none | 0 |
| Reviewed note has changed | Pass | pending | none | 0 |
| Reviewed target has changed | Pass | pending | none | 0 |
| Callback refers to a superseded version | Pass | pending | none | 0 |
| Approval arrives after rejection | Pass | rejected | none | 0 |
| Server deadline expired; incoming time is ignored | Pass | expired | none | 0 |
| Decision is neither approve nor reject | Pass | pending | none | 0 |
| Ten concurrent approvals for one action | Pass | approved | delivered | 1 |
| Approve and reject arrive concurrently | Pass | approved | delivered | 1 |
| Destination version changed after preparation | Pass | approved | conflict | 0 |
| Replay changes the already delivered note | Pass | approved | delivered | 1 |
| Pending proposal survives process restart | Pass | approved | delivered | 1 |
| Write fails before commit; replay after restart | Pass | approved | delivered | 1 |
| Write commits, response fails; reconcile after restart | Pass | approved | delivered | 1 |
Download the assertion and callback driver, container runner, raw results and source checksums. Reproduction overwrites the extracted local result file with the new run's timestamps. Compare scenario outcomes and evidence, not execution identifiers across separate instances.
Use the results to scope a real approval workflow
Keep the existing 13-case human approval lab for decisions inside one execution. Use this companion for the separate callback and recovery boundary. Neither fixture establishes production customer outcomes.
A deployed integration still needs real reviewer sessions, a destination adapter, durable state appropriate to its workers, recovery ownership and checks under its actual load. Record those results in the production acceptance worksheet; its rows remain untested until your implementation supplies evidence. The new lab's measured results do not complete that worksheet.
If an approval protects a website-to-CRM handoff, bring the exact proposed change, the reviewer rule and a destination receipt example to a workflow implementation review. Use the migration checklist if the change also replaces an existing Zap.
How do I handle duplicate n8n approval callbacks?
Store one decision against the exact proposal version, authorize the reviewer and use a stable destination action identifier. In this synthetic lab, ten concurrent approval callbacks produced one decision and one receipt. That result applies to the documented single-process fixture; test your own workers, database and destination API.
Can a pending approval survive an n8n restart?
The pending proposal in this lab lived in an external SQLite store and remained available after both lab processes were forcibly stopped and restarted. The later callback completed the action. This tests the durable proposal pattern, not n8n Wait-node resumption or every possible interruption point.
What happens when the destination writes successfully but its response fails?
Treat the result as uncertain and check for a destination receipt before writing again. The lab injected HTTP 504 after committing a synthetic write. Following restart and replay, it reused the same receipt and kept one destination entry. A real CRM needs an equivalent, verified reconciliation boundary.
Does this lab provide a production approval portal?
It provides a downloadable, tested teaching kit with one authenticated lab reviewer and a synthetic destination. User sessions, multiple reviewers, a real CRM, queue mode, monitoring and production load tests require separate work.
Built around your business
Put these ideas to work in your business.
Book a demoShow us how your team works today. We’ll help you choose a useful first step.