Blog
Engineering

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

Keep one customer approval attached to one agent action through retries and restarts. A runnable Pushary Partner example, with explicit limits and failure cases.

Aadil Ghani
Aadil Ghani
Founder, Pushary
Oct 15, 20264 min read
Share

One operation should keep one approval request, even when the agent job retries. With Pushary Partner APIs, that means a stable idempotencyKey for creation, a saved decisionId for recovery, and an explicit approval check before the action runs.

A retry is normal. Asking the customer again because your worker restarted should not be.

This was one of the problems behind building our Partner APIs. Sending the question is only the start. The product needs to remember what it asked, who it asked, and what happened next.

Give the operation an identity before asking

Consider a support agent proposing a EUR 4.80 refund. This is a synthetic example, not a customer incident.

Your application creates an operation record before calling Pushary. It stores the recipient, order, amount, currency and action version. That record stays stable across worker attempts. The approval key comes from that operation, not a random value generated every time the job runs.

Change the action or recipient and you need a new approval. A second refund is a second operation, even if its amount and question happen to match the first.

Pushary scopes the creation key to the site and recipient, with a 24-hour deduplication window. Within that window, a repeated create reuses the decision, including an answer it already received. After that window, resume using the saved decision ID. Do not treat the creation key as permanent storage.

Run the example without executing a refund

Download retry-approval.mjs. It uses @pushary/server, creates a confirmation question and prints its state. It never calls a payment provider.

Use a server-side Partner key and a recipient enrolled with your application. For a simulator instead of a person, follow the sandbox setup and its limits. Keep the key in your server environment.

npm install @pushary/server@2.5.0
node retry-approval.mjs operation-demo-4471 customer-demo

Set PUSHARY_API_KEY in the environment before running the script. The first call returns a decision ID. Save it with the immutable operation record. Running the same command again within the creation window uses the same key.

After a restart, pass the saved ID:

node retry-approval.mjs operation-demo-4471 customer-demo YOUR_SAVED_DECISION_ID

That path reads the existing decision. It does not create another question. The script checks that the returned decision belongs to the saved recipient and requires a confirmation answer before reporting approved: true.

Illustrative output while waiting:

{
  "operationId": "operation-demo-4471",
  "externalId": "customer-demo",
  "decisionId": "example-decision-id",
  "status": "pending",
  "approved": false,
  "actionExecuted": false
}

Even after approval, this example reports actionExecuted: false. It is a decision walkthrough, not a refund implementation.

A wait ending is not a decision expiring

decisions.ask() is convenient when the person may answer quickly. Its default caller timeout is 55 seconds. That timeout does not turn a pending question into a rejection, an approval or an expired decision.

Save the returned decision ID and check it later. Decision expiry is a separate setting. The default is one hour, with supported values from 60 seconds to 24 hours. Pending, expired, cancelled and denied decisions must not run the protected action.

If you receive an idempotency-storage error, stop and retry according to the response, including Retry-After when present. Generating a fresh key to get around that failure defeats the protection against duplicate questions.

One approval does not mean one execution

Two workers can read the same approved decision. Both can then try to refund the order.

Pushary's decision deduplication does not make your payment provider, email service or database transaction run exactly once. Before executing, your application must check that the action still matches the saved proposal and claim the operation atomically. Use the downstream provider's idempotency mechanism too. Record the result so another worker can recover without guessing whether the action happened.

A changed amount, target or recipient needs a new proposal and decision. An old yes does not authorize new work.

Treat callbacks as another delivery path

The downloadable example polls. For callbacks, verify the signature against the raw request body, validate the payload, and match its decision ID to your saved operation. Handle duplicate callbacks without repeating the action. Keep polling available as a recovery path.

The Partner integration guide contains the callback contract and signing details. A callback alone is not permission to execute an unrelated or changed operation.

What we checked

On October 15, 2026, we checked the downloadable script with separate process runs against a local HTTP fixture: duplicate creation, resume by saved ID, approval, denial, pending, expired, cancelled and wrong-recipient results. The fixture uses synthetic identifiers and does not send a notification or execute a refund.

We also ran Pushary's create-route tests, which exercise the real request handler and idempotency resolver with test storage and delivery boundaries. These cover concurrent retries, failed persistence, recipient isolation and unavailable idempotency storage. That is evidence for those code paths, not a claim that this walkthrough tested every phone or a production payment.

The lesson is simple: keep the decision attached to the operation, and keep execution attached to the decision. Start with the Partner integration guide, then test the unanswered and retry cases before connecting a real action.

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

Approve refunds over $500: the four things that decide whether a threshold holds

Picking the number is the easy part. Rule ordering, a missing field, a numeric string, and a retry are the four places a threshold leaks.

Aug 23, 20265 min readAadil Ghani
Engineering

Build vs buy: the real cost of a human-in-the-loop approval loop

The prototype takes a weekend. The system that will not silently approve a refund takes a quarter. An honest itemization, including when building is right.

Jul 17, 20263 min readAadil Ghani
Engineering

Fail-closed by default: why agent approvals should block instead of fire and forget

The failure case for approvals is not exotic. It is a phone in a pocket. Fail-closed design decides in advance that the ordinary case means stop.

Jul 17, 20263 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.