> This page is for Agent Handler.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.merge.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.merge.dev/_mcp/server.

# Building an agent

> End-to-end setup for an agent inside your product: API keys, Tool Packs, Registered Users, Link, and the MCP call from your backend.

This is the setup guide for **Building an agent**. Your product runs an agent, and each user of your product connects their own Salesforce, Slack, GitHub, and the rest. Agent Handler holds their credentials, scoped per Registered User, and your backend orchestrates everything.

For how this fits alongside the other ways to use Agent Handler, see [Use cases](/merge-agent-handler/use-cases).

## What you're building

By the end of this guide your stack will look like this:

* Your backend creates one **Registered User** per user of your product when they sign up.
* A **Tool Pack** in Agent Handler defines what tools your agent can call.
* When a customer needs to authenticate a Connector, your frontend opens **Link**. Credentials get stored against the customer's Registered User.
* When the agent runs, your backend hits Agent Handler's MCP endpoint with the Registered User's ID in the URL and an **Access Key** in the header, ideally one scoped to that user rather than your organization-wide key. The MCP URL never reaches the browser.

## Step 1: Get your API key

Production keys live at [Settings → API Keys](https://ah.merge.dev/settings/api-keys). You start with one production key and any number of test keys; test keys are sandboxed and isolated from production data. Use a test key while you build; switch when you're ready for real customers.

> **Warning**
>
> Never expose the API key client-side. It authorizes calls on behalf of every one of your customers' Registered Users. Keep it in your backend secret store.

### Scope the key to the user

The key above is organization-wide: it can call tools as any of your customers. That is what your backend should hold, and it is not what an agent session should carry. Once your agent serves more than one person, mint a **user-scoped key** and give the agent that instead. A user-scoped key cannot call tools as any Registered User it does not name, whatever ID appears in the MCP URL, which is what keeps one customer's agent out of another customer's data.

Your backend authenticates once with the organization-wide key, then mints a short-lived key per session:

```bash
curl -X POST https://ah-api.merge.dev/api/v1/access-keys/ \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "session-user_a3f9b2",
    "registered_user_ids": ["f9813dd5-e70b-484c-91d8-00acd6065b07"],
    "tool_pack_ids": ["1d8b6f4a-1b3e-4f9b-9d3a-3a8e8b9c0d1f"],
    "scopes": ["runtime:all"],
    "expires_in": 3600
  }'
```

The two ids come from Steps 2 and 3. `runtime:all` permits MCP tool calls and nothing else, so the session key cannot read or change your configuration. `key` is returned once, on the create response.

The same call scopes a key to a tenant or a service: name the Registered Users that tenant covers, or the Tool Packs that service needs. A minted key can only ever be narrower than the key that minted it.

See [Scoping access per user](/merge-agent-handler/secure/scoping-access-per-user) for the full exchange and the boundaries it enforces.

## Step 2: Create a Tool Pack

Tool Packs decide what tools your agent can call. Create one in the [Tool Packs dashboard](https://ah.merge.dev/tool-packs), add the Connectors you need, and pick the specific tools to expose (most Connectors have 30–80 tools; you usually want a subset). Copy the pack's ID from the URL - you'll need it on every MCP call.

For one-vs-many packs, lifecycle, and overrides, see [Tool Packs](/merge-agent-handler/build/tools/tool-packs).

## Step 3: Create a Registered User

Create one Registered User per customer user, the moment they sign up for your product. The `origin_user_id` you pass is your own user ID - that's how you find this Registered User later.

```bash
curl -X POST https://ah-api.merge.dev/api/v1/registered-users \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "origin_user_id": "user_a3f9b2",
    "origin_user_name": "Alice Chen",
    "shared_credential_group": {
      "origin_company_id": "company_acme",
      "origin_company_name": "Acme Inc."
    }
  }'
```

```json
{
  "id": "f9813dd5-e70b-484c-91d8-00acd6065b07",
  "origin_user_id": "user_a3f9b2",
  "origin_user_name": "Alice Chen",
  "shared_credential_group": {
    "origin_company_id": "company_acme",
    "origin_company_name": "Acme Inc.",
    "custom_groupings": {}
  },
  "user_type": "HUMAN",
  "authenticated_connectors": [],
  "is_test": false
}
```

Store `id` against your user record - that is the Registered User ID every later call needs. The API call is idempotent on `origin_user_id` - calling it twice returns the existing Registered User rather than creating a duplicate.

The `shared_credential_group` is optional. Pass it when you want to Group users by tenant - typical in B2B products where one customer org has multiple users. See [Companies and Groups](/merge-agent-handler/build/users/Companies-and-Groups) for the details.

## Step 4: Embed Link

When a customer needs to authenticate a Connector, your frontend opens Link. There are two ways the flow gets triggered:

* **At agent runtime.** The model tries to call a tool the user hasn't authenticated. The Connector returns an `authenticate_meta` tool result with a `link_token`. Your frontend catches it and opens Link.
* **Pre-flight, from a settings page.** The user clicks "Connect Slack" in your UI. Your backend generates a link token and your frontend opens Link.

Pre-flight is more common; users prefer to set up integrations once instead of being interrupted mid-conversation.

### Generate a link token (backend)

```bash
curl -X POST https://ah-api.merge.dev/api/registered-users/$REGISTERED_USER_ID/link-token \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "Connector": "slack" }'
```

```json
{
  "link_token": "ltk_•••••",
  "expires_at": "2026-05-04T18:30:00Z"
}
```

Link tokens are short-lived (30 minutes) and single-use. Generate one per Link session.

### Open Link (frontend)

Install the React component:

```bash
npm install @mergeapi/react-agent-handler-link
```

Open Link with the token your backend just minted:

**`ConnectButton.tsx`**

```tsx ConnectButton.tsx
import { useAgentHandlerLink } from "@mergeapi/react-agent-handler-link";

export function ConnectSlackButton({ linkToken }: { linkToken: string }) {
  const { open, isReady } = useAgentHandlerLink({
    linkToken,
    onSuccess: () => {
      // Credentials are now stored against the Registered User.
      // Refresh your UI, or kick off the agent action.
    },
    onExit: (error) => {
      // User closed Link or auth failed.
      // `error` is null on a clean exit, populated on failure.
    },
  });

  return (
    <button disabled={!isReady} onClick={() => open()}>
      Connect Slack
    </button>
  );
}
```

For the `authenticate_meta` flow, intercept the tool call result in the agent's response stream and feed the `link_token` field straight into the same component. See [Link](/merge-agent-handler/build/authentication/link).

For email-driven authentication or any context where rendering a React component isn't an option, use [Magic Link](/merge-agent-handler/build/authentication/magic-link) instead - your backend mints a signed URL, you send it to the user, they authenticate in their own browser.

## Step 5: Call MCP from your backend

When the agent runs, your backend constructs the MCP URL and calls Agent Handler:

```
https://ah-api.merge.dev/api/v1/tool-packs/<TOOL_PACK_ID>/registered-users/<REGISTERED_USER_ID>/mcp
```

A minimal Python client using the official MCP SDK:

**`agent_runtime.py`**

```python agent_runtime.py
import os
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client

API_KEY = os.environ["MERGE_AGENT_HANDLER_API_KEY"]
TOOL_PACK_ID = "tp_..."

async def run_agent_for_user(registered_user_id: str, prompt: str) -> str:
    url = (
        f"https://ah-api.merge.dev/api/v1/tool-packs/{TOOL_PACK_ID}"
        f"/registered-users/{registered_user_id}/mcp"
    )
    headers = {"Authorization": f"Bearer {API_KEY}"}

    async with streamablehttp_client(url, headers=headers) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            # Hand `tools` to your model; route any tool calls back through:
            # await session.call_tool(name, arguments)
            ...
```

If you're building a custom MCP client (for example, in a language without an SDK), you're speaking JSON-RPC 2.0 over HTTP. See [MCP integration](/merge-agent-handler/build/connecting-agents/mcp-integration) for the wire-level walkthrough.

## What can go wrong

A few real failure modes worth anticipating.

**Tool call returns 403 with `unauthorized_tool`.** The tool isn't in the Registered User's Tool Pack. Check the Tool Pack contains the tool, and that you're calling the right Tool Pack ID in the URL.

**Tool call returns 401.** Your API key is invalid, expired, or you're using a test key against a production Registered User (or vice versa). Test and production data are isolated.

**Tool call returns `reauth_required`.** The user's stored OAuth token is expired or revoked. Generate a fresh link token for the same Connector and reopen Link. The user re-authenticates; the call retries cleanly.

**Tool call result is missing fields.** The Security Gateway redacted them. Check the [Alerts dashboard](https://ah.merge.dev/security/alerts) to see which rule fired. If you want the data through unredacted for one Tool Pack, add an override.

**Link won't open.** Most often the link token expired (30-minute window) or it's being reused. Generate a new token and open Link with that one.

For a fuller error catalog, see [Troubleshooting](/merge-agent-handler/resources/troubleshooting).

## Production checklist

Before you flip the switch on real customers:

* Production API key created, rotated, and stored in your secret manager.
* User-scoped keys minted per session, tenant, or service, rather than an organization-wide key in the hands of an agent.
* Tool Pack reviewed: the only tools in it are ones your agent should be able to call.
* Standard Entity Rules configured for your data sensitivity profile (PII at minimum; PHI if you're in healthcare; payment data if you handle cards).
* Allowed callback origins set in [Settings → API Keys](https://ah.merge.dev/settings/api-keys) for every domain that hosts your frontend.
* Webhook subscriptions wired up if you want tool-call events in your own pipeline.
* Tool-call logs spot-checked. You should be able to find a recent call in seconds.

## Next

Read [Tool Packs](/merge-agent-handler/build/tools/tool-packs) for the full tool-pack lifecycle, or jump to [Security Gateway](/merge-agent-handler/secure/security-gateway) to set up your data rules.