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.

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-demoSet 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_IDThat 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.