Verify approval webhooks before your agent resumes
Verify Pushary webhook signatures over the raw body, reject unknown decisions and save one recheck job across retries. Run the Node.js example and tests.

Verify the callback. Save it once. Check the decision before your agent acts. This example gives you a working Pushary webhook receiver with tests for forged requests, duplicate delivery and storage failure.
A callback can arrive more than once. Your receiver can restart after saving it. Neither should turn one approval into two actions.
What a valid signature proves
Pushary signs the raw callback body with HMAC-SHA256 and sends the hex signature in X-Pushary-Signature. Use verifyWebhookSignature from @pushary/server with your site's webhook secret. Verify the original body before parsing JSON. Parsing and serializing it again changes the bytes, even if the fields look identical.
A valid signature proves the body matches the signature and secret. It does not prove the delivery is new, that the decision belongs to the operation you are running, or that the action is approved.
The receiver therefore uses correlationId to look up a decision your backend already saved. It ignores callback context for routing. Unknown decision IDs are rejected.
Run the receiver tests
Use Node.js 24 and a new local directory:
mkdir pushary-webhook-demo
cd pushary-webhook-demo
npm init -y
npm install @pushary/server@2.5.0
curl -fsS https://pushary.com/examples/verify-decision-webhook.mjs -o verify-decision-webhook.mjs
curl -fsS https://pushary.com/examples/verify-decision-webhook.test.mjs -o verify-decision-webhook.test.mjs
node --test verify-decision-webhook.test.mjsThe suite uses synthetic data and a local HTTP server. It needs no Pushary account, sends no phone notification and executes no customer action.
It checks raw-body signatures, missing and incorrect signatures, altered JSON, malformed payloads, unknown decisions, concurrent duplicates, denials, large requests and storage failure. It also starts fresh receiver processes against the same SQLite file to check that duplicate protection survives a restart.
Download the receiver and test suite separately if you prefer.
Connect it to a saved decision
When your backend creates a decision, save its decisionId with the intended customer and operation. The example's registration command does that locally. Replace all three identifiers with values from your own trusted workflow:
node verify-decision-webhook.mjs register inbox.sqlite decision-id customer-id operation-idFetch the site's webhook secret through the Partner webhook setup using an authorized administration credential. Set PUSHARY_WEBHOOK_SECRET in the receiver's server environment, then start it:
node verify-decision-webhook.mjs serve inbox.sqliteIt listens on 127.0.0.1:3000 at /webhooks/pushary. For real callbacks, put it behind your HTTPS server and use that public HTTPS URL as the decision's callbackUrl. Pushary cannot deliver to localhost. Set PUSHARY_WEBHOOK_PORT to change the local port.
A verified callback for a known decision inserts one recheck job into SQLite. Only then does the receiver return 204. A duplicate also gets 204, without another job. If storage fails, it returns 503 so delivery can be retried.
Recheck the decision before acting
Read saved jobs with:
node verify-decision-webhook.mjs list inbox.sqliteEach job contains the saved decisionId, externalId and operationId. Your worker must retrieve that decision with decisions.get(), confirm the customer and decision IDs match, and check its type, status and answer. For a confirmation, require an explicit approval. A denial, pending answer, expiry or cancellation must not run the action.
The receiver queues a recheck for a signed denial too. It never treats callback text as permission and never executes the protected action. The retry and resume example shows the separate approval check.
Keep polling as a backup for missed callbacks and expiry. Record completion in your workflow and use the operation ID to prevent duplicate side effects at the system that performs them. Saving a webhook once does not make a refund or deployment execute exactly once.
What this example covers
This is a small receiver with a persistent local inbox, tested on Node.js 24.3.0. It uses Node's built-in SQLite module, which emits an experimental warning on that version. Keep the database on persistent storage; an ephemeral serverless filesystem will lose it.
The example stops at saving recheck jobs. Your application owns worker retries, completion records, access to the database and the final action. Start with the tests, then connect the receiver to your existing workflow.