Blog
Engineering

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.

Aadil Ghani
Aadil Ghani
Founder, Pushary
Oct 19, 20263 min read
Share

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.mjs

The 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-id

Fetch 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.sqlite

It 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.sqlite

Each 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.

Aadil Ghani
Aadil Ghani
Founder, Pushary

Building Pushary so an AI agent can reach you on your phone and wait for a yes before it does something you would not want.

Read next

Engineering

The agent lost the response. Did the action happen?

Approval, a committed effect and workflow status answer different questions. Two runnable examples show what survives a lost response.

Oct 16, 20264 min readAadil Ghani
Engineering

Your agent retried. Your customer should not have to approve twice.

Give the operation a stable identity, save the decision, and resume from the answer. A practical example of the part that comes after sending an approval request.

Oct 15, 2026Updated Oct 16, 20265 min readAadil Ghani
Engineering

Your customer should know exactly what they are approving.

An approval button is the last step. First, name the person, show the action and make sure the answer stays attached to the work.

Oct 15, 20265 min readAadil Ghani

Let your agents work. Stay in control.

Get the questions that need you on your phone or Mac. Approve, reject, or answer so your agent knows what to do next.