# Connect agents built in code

Send the tool calls of an agent you build in code to withHuman for approval.

If you build your own agent with a framework such as LangGraph, Pydantic AI or Mastra, the Agent Approval Protocol (AAP) SDKs send its tool calls to withHuman. Each call runs only once it's approved.

The SDKs work with any AAP provider. This guide covers what you need for withHuman. The [AAP SDK docs](https://agentapprovalprotocol.io/docs/sdks/overview) cover every option in depth.

## Before you start

You'll need:

- **An agent in withHuman** for your code to connect to. Use a service agent for an agent your organization runs. See [Agent creation](/docs/web-app/agents#agent-creation).
- **Permission to issue credentials** for that agent. Owners and admins have it.
- **Approval rules** for its calls. See [Set up approval pipelines](/docs/getting-started/set-up-approval-pipelines).

## 1. Get an API key

Open **Agents**, select your agent and select **New instance**. Choose **Generate an API key**, enter an instance name such as `support-server-01`, and select **Generate API key**.

Copy the **Agent API key** before you leave the page. It's shown only once. Store it where your agent reads its secrets. The examples read it from the `WITHHUMAN_TOKEN` environment variable.

Generate a separate key for each place the agent runs, so you can manage each one on its own. The page shows when the key expires.

To let runs pause while they wait, choose **Pause runs while waiting** instead. See [Pausing instead of waiting](#pausing-instead-of-waiting).

If your platform starts new copies of the agent by itself, register each copy with a [provisioner token](/docs/web-app/provisioner-tokens) instead.

## 2. Add the SDK

Every example passes the same two values:

- **The AAP address.** For hosted withHuman, that's `https://app.withhuman.ai/api/aap`. On a self-hosted deployment, use its address followed by `/api/aap`.
- **The agent API key** from step 1.

Find your framework below. Add the SDK to every agent and sub-agent you run, so none of their calls skip approval.

### LangChain, LangGraph and Deep Agents

```sh
pip install "agentapprovalprotocol[langgraph]"
```

```python
import os
from agentapprovalprotocol.langgraph import ApprovalMiddleware

approvals = ApprovalMiddleware("https://app.withhuman.ai/api/aap", os.environ["WITHHUMAN_TOKEN"])
agent = create_agent(model, tools=tools, middleware=[approvals])
```

For Deep Agents, also pass your subagents through `deep_agent_subagents`. See the [LangGraph guide](https://agentapprovalprotocol.io/docs/sdks/langgraph).

### Pydantic AI

Needs Python 3.11 or later.

```sh
pip install "agentapprovalprotocol[pydantic-ai]"
```

```python
import os
from agentapprovalprotocol.pydantic_ai import ApprovalCapability

approvals = ApprovalCapability("https://app.withhuman.ai/api/aap", os.environ["WITHHUMAN_TOKEN"])
agent = Agent(model, tools=tools, capabilities=[approvals])
```

See the [Pydantic AI guide](https://agentapprovalprotocol.io/docs/sdks/pydantic-ai).

### Google Agent Development Kit

```sh
pip install "agentapprovalprotocol[adk]"
```

```python
import os
from agentapprovalprotocol.adk import ApprovalPlugin, check_coverage

approvals = ApprovalPlugin("https://app.withhuman.ai/api/aap", os.environ["WITHHUMAN_TOKEN"])
app = App(name="support", root_agent=agent, plugins=[approvals])
check_coverage(app, approvals)
```

Register the plugin before your other plugins. `check_coverage` tells you about agents whose calls the plugin can't see. See the [ADK guide](https://agentapprovalprotocol.io/docs/sdks/adk).

### OpenAI Agents SDK

```sh
pip install "agentapprovalprotocol[openai-agents]"
```

```python
import os
from agentapprovalprotocol.openai_agents import Approvals

approvals = Approvals("https://app.withhuman.ai/api/aap", os.environ["WITHHUMAN_TOKEN"])
agent = approvals.protect(Agent(name="Support", tools=tools, handoffs=handoffs))
result = await Runner.run(agent, "Refund the duplicate charge")
```

Call `protect` after the agent is fully configured. See the [OpenAI Agents SDK guide](https://agentapprovalprotocol.io/docs/sdks/openai-agents).

### Claude Agent SDK

```sh
pip install "agentapprovalprotocol[claude-agent-sdk]"
```

```python
import os
from agentapprovalprotocol.claude_agent_sdk import ApprovalHooks

approvals = ApprovalHooks("https://app.withhuman.ai/api/aap", os.environ["WITHHUMAN_TOKEN"])
options = approvals.options(ClaudeAgentOptions(mcp_servers=servers))
async for message in query(prompt=prompt, options=options):
    ...
```

If you've connected Claude Code on the same machine with the withHuman CLI, don't load your user settings in the SDK. Otherwise every call asks for approval twice. See the [Claude Agent SDK guide](https://agentapprovalprotocol.io/docs/sdks/claude-agent-sdk).

### CrewAI

CrewAI supports Python 3.10 to 3.13. Its extra can't be installed alongside `openai-agents`.

```sh
pip install "agentapprovalprotocol[crewai]"
```

```python
import os
from agentapprovalprotocol.crewai import ApprovalHooks

ApprovalHooks("https://app.withhuman.ai/api/aap", os.environ["WITHHUMAN_TOKEN"]).install()
crew.kickoff()
```

CrewAI runs always wait for each decision; they can't pause. See the [CrewAI guide](https://agentapprovalprotocol.io/docs/sdks/crewai).

### Vercel AI SDK

Needs AI SDK 7 and Node.js 22 or later.

```sh
npm install @agentapprovalprotocol/ai-sdk @agentapprovalprotocol/sdk ai
```

```ts
import { Client, Gate } from "@agentapprovalprotocol/sdk";
import { approvals } from "@agentapprovalprotocol/ai-sdk";

const gate = new Gate(new Client("https://app.withhuman.ai/api/aap", process.env.WITHHUMAN_TOKEN!));
const agent = new ToolLoopAgent({ model, tools, ...approvals({ gate }) });
```

`approvals` works the same with `generateText` and `streamText`. See the [AI SDK guide](https://agentapprovalprotocol.io/docs/sdks/ai-sdk).

### Mastra

Needs `@mastra/core` 1.76 and Node.js 22.13 or later.

```sh
npm install @agentapprovalprotocol/mastra @agentapprovalprotocol/sdk
```

```ts
import { Client, Gate } from "@agentapprovalprotocol/sdk";
import { Approvals } from "@agentapprovalprotocol/mastra";
import { Agent } from "@mastra/core/agent";

const approvals = new Approvals(new Gate(new Client("https://app.withhuman.ai/api/aap", process.env.WITHHUMAN_TOKEN!)));
const agent = new Agent(approvals.config({ id: "support", name: "Support", instructions, model, tools }));
await approvals.check(agent);
```

`check` throws if a sub-agent or tool would skip approval. See the [Mastra guide](https://agentapprovalprotocol.io/docs/sdks/mastra).

### Your own tool loop

If you call tools yourself, check each call with a `Gate` before running it.

**Python**

```sh
pip install agentapprovalprotocol
```

```python
import os
from agentapprovalprotocol import Call, Client, Gate

gate = Gate(Client("https://app.withhuman.ai/api/aap", os.environ["WITHHUMAN_TOKEN"]))

with gate.guard(Call(key=f"{run_id}:{tool_call.id}", tool=tool_call.name, arguments=tool_call.args)) as verdict:
    result = run_tool(tool_call) if verdict else verdict.reason
```

**TypeScript**

```sh
npm install @agentapprovalprotocol/sdk
```

```ts
import { Client, Gate } from "@agentapprovalprotocol/sdk";

const gate = new Gate(new Client("https://app.withhuman.ai/api/aap", process.env.WITHHUMAN_TOKEN!));

const result = await gate.guard(
  { key: `${runId}:${toolCall.id}`, tool: toolCall.name, arguments: toolCall.args },
  () => runTool(toolCall),
);
const output = result.kind === "allow" ? result.value : result.reason;
```

The `key` names the call within its run, and must stay the same if the call is retried. See the [AAP SDK docs](https://agentapprovalprotocol.io/docs/sdks/overview).

## What to expect

Every tool call your agent makes becomes an approval request. Your [approval pipelines](/docs/concepts/approval-pipelines) approve it, deny it or send it to a reviewer in the [review queue](/docs/web-app/review-queue).

- **While a call waits for a reviewer, your agent waits too.** After about nine minutes without a decision, the call doesn't run and the agent is told why.
- **A denied call doesn't run.** The agent gets the reason as the tool's result, so it can explain or try something else.
- **An approved call runs once.** The request page then shows **Used**, when the call started, and **Result**, whether it succeeded or failed.

Tools that run at the model provider, such as a model's built-in web search, can't wait for approval. The SDKs stop a run that uses one, so use a local tool instead.

## Pausing instead of waiting

A run can pause at a call that needs a reviewer, so your process doesn't sit waiting. When the decision arrives, withHuman notifies your **receiver**: a small web endpoint from the SDK that resumes the run. Each framework guide in the [AAP SDK docs](https://agentapprovalprotocol.io/docs/sdks/overview) shows how to turn on pausing with `mode="async"`.

Before you start:

- **withHuman must be able to reach the receiver** at a public HTTPS address. Private and local addresses are refused. For local development, expose the receiver through a tunnel such as ngrok.
- **The receiver is set up with the instance.** An instance from **Generate an API key** can't add one later. Use a new instance instead.
- **An approval stays valid for 30 minutes.** If the run hasn't resumed by then, the call doesn't run and the agent is told the approval expired.

### Receiver setup

1. Open your agent, select **New instance** and choose **Pause runs while waiting**.
2. Select **Generate signing secret**. Copy the **Registration ID** and **Signing secret**. The secret is shown only once, and you have an hour to finish.
3. Start your receiver with both values. With FastAPI and LangGraph:

   ```python
   import os
   from fastapi import FastAPI
   from agentapprovalprotocol import AsyncClient, Receiver
   from agentapprovalprotocol.langgraph import resumer

   receiver = Receiver(
       secret=os.environ["AAP_WEBHOOK_SECRET"],
       registration_id=os.environ["AAP_REGISTRATION_ID"],
       client=lambda: AsyncClient("https://app.withhuman.ai/api/aap", load_token()),
       resumers={"langgraph": resumer(agent)},
   )
   app = FastAPI()
   app.add_route("/aap/events", receiver.asgi(), methods=["POST"])
   ```

   Add the receiver as a route, as shown. Mounting it with `app.mount` adds a redirect, which withHuman doesn't follow. `load_token()` stands for reading the API key from your secret store; it runs the first time a run resumes.

4. Back in withHuman, enter an **Instance name** and your **Receiver URL**, such as `https://agent.example.com/aap/events`. Select **Verify receiver**.
5. withHuman checks that the receiver answers, then shows the instance's **Agent API key** once. Give it to your agent and your receiver.

If verification fails, fix the receiver and select **Verify receiver** again. The secret stays on the page. To register a receiver from code instead, see the [AAP SDK docs](https://agentapprovalprotocol.io/docs/sdks/overview).

## Checking it works

Ask your agent to do something that uses a tool your rules send for review. Then:

1. Open the agent's **Instances** tab. **Last seen** for your instance should show that it just contacted withHuman.
2. Open **Review queue** and approve the request. The [Review queue guide](/docs/web-app/review-queue) explains deciding requests.
3. Open the request again. **Used** and **Result** show that your agent ran the call.

Calls your rules decide automatically don't reach the queue. Find them under the agent's **Approval requests**.

## Troubleshooting

| What you see | What to do |
| --- | --- |
| The agent is told the approval provider refused its credential. | The key is wrong, expired or revoked, or the agent or instance is disabled. Check the instance in **Instances**, and generate a new key if needed. |
| The agent is told the adapter couldn't obtain a valid AAP decision. | Calls aren't reaching withHuman. Check that the address ends in `/api/aap` and that the machine can reach it over HTTPS. |
| Some calls never appear in withHuman. | Add the SDK to every agent and sub-agent. For ADK and Mastra, run the coverage check shown in your framework's section. |
| Every call asks for approval twice. | Something else on the machine also sends calls to withHuman, such as the Claude Code hook. Use one or the other. |
| The receiver isn't verified. | Check that it's running at the public HTTPS address you entered, with the registration ID and signing secret from the page. For local development, use a tunnel. |
| The registration expired. | Generate a new signing secret and start the receiver with the new values. |
| **Pause runs while waiting** isn't offered. | Your deployment can't send webhooks. Ask whoever runs it. |
| A paused run never resumes. | Check that the receiver is still running at the registered address, and that the instance was registered with it. |
| The agent is told the approval expired. | The run resumed more than 30 minutes after the approval. Ask the agent to try the call again. |
