Test your integration
Test your human in the loop integration. Simulate approve, deny and no answer with a sandbox key, then check retries, webhooks and real phone delivery.
Before you turn on real customer actions, check that your code handles every answer. Start with a sandbox key, which settles each decision as approve, deny or no answer without sending a notification. Then test the real channel your customers will use. Keep the action behind the approval replaced with a harmless local operation until both pass.
While signed in to your Pushary dashboard, run this in the browser's developer console to get a sandbox key:
const response = await fetch("/api/agent/sandbox", { method: "POST" })
const sandbox = await response.json()
if (!response.ok) throw new Error(sandbox.error)API sandbox
The API sandbox needs a signed-in workspace with a site, permission to act on agents, and an active or trialing subscription on any plan. It does not need the Partner plan. Without an active or trialing subscription, sandbox calls return 403 with code: "subscription_required". It is separate from the public demo and from the Send a test approval card in the Partner dashboard.
Get a sandbox key
Call POST /api/agent/sandbox while signed in, with the snippet above. The response's key is shown once. Copy it into your local server environment as PUSHARY_API_KEY. Do not paste your dashboard session cookie into scripts.
| Response | What it means |
|---|---|
| 404 | No current site is set up |
| 403 | Your role cannot act on agents |
| 409 | The workspace already has 5 live sandbox keys. Revoke one first. |
Run all three answers
Install @pushary/server, then run this script with your sandbox key:
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { createPusharyServer } from "@pushary/server"
const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY! })
for (const outcome of ["approve", "deny", "no_answer"] as const) {
const result = await pushary.decisions.ask({
externalId: "integration-test-user",
question: "Allow this test action?",
type: "confirm",
sandboxOutcome: outcome,
idempotencyKey: randomUUID(),
timeoutMs: 1000,
expiresInSeconds: 60,
})
assert.equal(result.approved, outcome === "approve")
assert.equal(result.answered, outcome !== "no_answer")
assert.equal(result.status, outcome === "no_answer" ? "pending" : "answered")
console.log(outcome, result.decisionId, result.status)
}It prints three lines, one per outcome, and every assertion passes.
Check what no answer leaves behind
no_answer leaves the decision open. The one-second SDK wait does not expire it. Save the id and check that it becomes expired after its 60-second lifetime, or cancel it when you are done.
What the sandbox key can do
Sandbox keys can reach identity, authorize, authorization-rules, decisions, authorizations and reachability. They cannot enroll a real device, send notifications, issue keys, or manage webhook secrets.
Without sandboxOutcome, the simulator approves. For a select question, approve records the first option, and deny withdraws the question, so it reads as cancelled.
Sandbox decisions use the real decision store and record the simulator as the answer source. They send no phone, browser or Slack notification and use none of your production notification allowance. They do not prove webhook delivery or any downstream action in your application.
Check retries and recovery
| Check | Expected result |
|---|---|
| Repeat create with the same recipient and idempotency key | Same decision id |
| Retry after HTTP 503 or a failed create | Honor Retry-After when present, reuse the key, and check that the returned id can be read |
| Race two creates with the same key and recipient | One stored decision and one delivery fan-out |
| Read the saved id after restarting your worker | Same stored result |
| Leave a decision unanswered until expiry | expired, and the action stays blocked |
| Cancel an abandoned decision | cancelled, and no action runs |
Use an allow, deny or unmatched authorization rule | allow, deny or requires_human |
| Claim the same authorization permit twice | Second claim refused as already_consumed |
Test your workflow's execution count too. A stored approval must not cause two side effects when a webhook or worker job is replayed.
Test live delivery
Use a live Partner key from Agent settings and an id for a test user you control. Finish enrollment before you send the approval.
Connect a test user
curl --fail-with-body https://pushary.com/api/v1/server/enroll \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalId":"integration-test-user"}'Open the returned universalLink and finish one of these paths:
| Channel | What to check |
|---|---|
| Native app | Install Pushary if needed, open the link, and agree. After a fresh install, tap Invited by a company? and scan the saved QR code from another screen if the link does not reopen the app. |
| Browser | Choose Enable browser notifications in a supported browser and allow notifications. Ordinary iOS Safari tabs cannot receive browser push notifications. Use the native app or a supported Home Screen web app. |
| Your own channel | Create with approvalUrl: true and a full-access key, then send the returned link through your own email, SMS or inbox service. |
Create a decision
curl --fail-with-body https://pushary.com/api/v1/server/decisions \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: integration-test-1" \
-d '{
"externalId":"integration-test-user",
"question":"Allow this test action?",
"type":"confirm",
"requireReachable":true,
"expiresInSeconds":3600
}'Use a new idempotency key for each new test. Leave out requireReachable if you are testing your own approval link instead of push. Your test phone buzzes with "Allow this test action?".
Answer on the device and read the result
Save the returned decision id, answer on the device, then read it:
curl --fail-with-body \
"https://pushary.com/api/v1/server/decisions/DECISION_ID?wait=30" \
-H "Authorization: Bearer $PUSHARY_API_KEY"Check both "yes" and "no". Only a yes should release your test action.
Slack
Connect Slack in your dashboard integrations. A decision with no externalId and no email goes to the configured shared channel, or to the member DM set on the integration. An email sends a DM to the Slack member with that email. If Pushary cannot find that member, it does not fall back to the shared channel, and a customer decision with no email is not sent to Slack.
Test with non-sensitive content. Slack does not count toward end-user push reachability. Review which team members may see and answer customer requests before you turn this channel on.
Webhooks and your own UI
Set your callback URL, fetch the site's webhookSecret, and use the verified callback example. Then check that:
- a real answer reaches your endpoint
- an invalid signature is rejected
- a replayed callback does not run your workflow twice
- polling recovers the answer while the callback is unavailable
For headless answers, check that your backend rejects a user who tries to answer another user's decision. If a phone already recorded "no", a later API answer must keep that result.
What these tests prove
The sandbox checks API behavior and how your code handles each response. Live tests check enrollment, notification permissions, delivery and callback setup. Neither proves an external side effect is safe to retry. Use the downstream provider's idempotency controls and your saved workflow state for that.
Next steps
Read the full Partner guide
Create, resume, webhooks, bound keys, policy and receipts.
Use a framework adapter
Vercel AI SDK, LangGraph, OpenAI Agents SDK, Mastra, Claude Agent SDK and more.