# Build approvals into your agent

Source: https://pushary.com/docs/agents/build/quickstart
Last updated: 2026-11-04

> Add human in the loop approvals to the AI agent in your product. Your user gets a push notification, taps Approve or Deny, and your code gets the answer.

Pushary adds human approval to the agent inside your own product. Before a risky step, such as a refund, a customer email or a delete, your backend asks one of your users. They get a push notification with your product's name, tap Approve or Deny, and your code carries on. If nobody answers, the action does not run.

```bash
npm i @pushary/server
```

Want to see it work first? [Try it in your browser](/try). No signup.

## What you need [#what-you-need]

* A Partner plan, a full-access server key for enrollment, and a runtime key for decisions. Start at <AttributedLink href="/onboarding/partner">Partner onboarding</AttributedLink>, then get the enrollment credential from [Agent settings](/dashboard/agent/settings). The onboarding runtime key cannot enroll users.
* Your own id for each user, such as `user.id`. Pushary calls it `externalId`.
* Your users install the free Pushary app and open your link once. They never sign up or pay.

## Ask your first question [#ask-your-first-question]

<Steps>
  <Step>
    ### Install the SDK and set your key [#install-the-sdk-and-set-your-key]

    <Tabs items="[&#x22;TypeScript&#x22;, &#x22;Python&#x22;]">
      <Tab value="TypeScript">
        ```bash
        npm i @pushary/server
        export PUSHARY_API_KEY=pk_xxx.xxx
        ```
      </Tab>

      <Tab value="Python">
        ```bash
        pip install pushary
        export PUSHARY_API_KEY=pk_xxx.xxx
        ```
      </Tab>
    </Tabs>

    For the enrollment step below, `PUSHARY_API_KEY` must be a full-access key held by your trusted server. Keep that client separate from the agent runtime, which should use a runtime or recipient-bound key for decisions. Never put the full-access key in browser code or a prompt.
  </Step>

  <Step>
    ### Connect a user once [#connect-a-user-once]

    <Tabs items="[&#x22;TypeScript&#x22;, &#x22;Python&#x22;]">
      <Tab value="TypeScript">
        ```ts
        import { createPusharyServer } from '@pushary/server'

        const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY! })

        const { universalLink } = await pushary.enroll(user.id)
        // Show universalLink to this user as a button or a QR code.
        ```
      </Tab>

      <Tab value="Python">
        ```python
        import os
        from pushary import PusharyServer

        pushary = PusharyServer(api_key=os.environ["PUSHARY_API_KEY"])

        enrolled = pushary.enroll(user.id)
        # Show enrolled["universalLink"] to this user as a button or a QR code.
        ```
      </Tab>
    </Tabs>

    Creating a link does not connect the user. They must open it and consent. It opens the Pushary app if installed; otherwise the page offers the app or browser notifications. After installation, reopen the original link or scan its QR code from the app's first screen. A registered channel makes push delivery possible, but does not guarantee the person received or read a notification.

    The link can be redeemed once per channel (the phone app and browser push) and expires after 24 hours. Save the fact that the user connected, not the link.
  </Step>

  <Step>
    ### Ask before the risky step [#ask-before-the-risky-step]

    <Tabs items="[&#x22;TypeScript&#x22;, &#x22;Python&#x22;]">
      <Tab value="TypeScript">
        ```ts
        const decision = await pushary.decisions.ask({
          externalId: user.id,
          question: 'Refund $480 to order #4471?',
        })

        if (decision.approved) {
          await issueRefund()
        }
        ```
      </Tab>

      <Tab value="Python">
        ```python
        decision = pushary.decisions.ask("Refund $480 to order #4471?", external_id=user.id)

        if decision["approved"]:
            issue_refund()
        ```
      </Tab>
    </Tabs>

    Your user's phone buzzes with the question. When they tap Approve, `approved` is true and the refund runs. `issueRefund` is your own function.
  </Step>
</Steps>

## What happens when nobody answers [#what-happens-when-nobody-answers]

`ask` waits up to 55 seconds for an answer. `approved` is true only when the person tapped Approve. Deny, an expired question and no answer at all give `approved: false`, so the refund does not run.

The question stays open after `ask` returns, for 1 hour by default. To act on a late answer, save `decision.decisionId` and read it again later. The [full Partner guide](/docs/agents/embed#4-resume-only-after-approval) shows how.

## What your user sees [#what-your-user-sees]

1. A notification titled with your product name, such as **Acme needs your approval**.
2. They tap **Approve** or **Deny** from the lock screen.
3. Your code gets the answer and carries on.

## Use your framework's approval hook [#use-your-frameworks-approval-hook]

Most agent frameworks can stop before a tool runs. These adapters put your user's phone behind that stop.

<Cards>
  <Card title="Vercel AI SDK" description="pusharyApproval() as the AI SDK's toolApproval." href="/docs/agents/build/vercel-ai-sdk" />

  <Card title="LangGraph" description="An approval node in front of the risky node, in TypeScript or Python." href="/docs/agents/build/langgraph" />

  <Card title="OpenAI Agents SDK" description="Answers needsApproval interruptions from your user's phone." href="/docs/agents/build/openai-agents" />

  <Card title="Mastra" description="Approves or declines tools marked requireApproval." href="/docs/agents/build/mastra" />

  <Card title="Claude Agent SDK" description="A canUseTool that asks your user before a tool runs." href="/docs/agents/build/claude-agent-sdk" />

  <Card title="Every other adapter" description="Eve, CrewAI, Pydantic AI, durable workflows and your own framework." href="/docs/agents/adapters" />
</Cards>

## Next steps [#next-steps]

<Cards>
  <Card title="Read the full Partner guide" description="Callbacks, resuming later, bound keys and answers from your own app." href="/docs/agents/embed" />

  <Card title="Test without a phone" description="A sandbox key settles each question as approve, deny or no answer." href="/docs/agents/embed-testing" />
</Cards>