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

# Passthrough credentials

> How to call Agent Handler tools with credentials your product already holds, using the X-Tool-Auth header. Covers finding each Connector's fields, building the header, and handling errors.

Passthrough lets you call tools with third-party credentials your product already holds. You send the end user's OAuth token or API key in the `X-Tool-Auth` header on each tool call, and Agent Handler uses it for that one request. Use it when you build your own connect screen instead of [Link](/merge-agent-handler/build/authentication/link), or when your product already stores your users' credentials and you don't want a second copy in Agent Handler.

Passthrough is turned on per organization. Contact your Merge account team to enable it.

## How passthrough differs from Link

|                                 | Link                                                                                                         | Passthrough                                                                        |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| Where credentials live          | Agent Handler stores them against the Registered User                                                        | Your product stores them                                                           |
| Who runs OAuth                  | Agent Handler, using your [Application Credentials](/merge-agent-handler/administer/application-credentials) | Your product, using your own OAuth app with each provider                          |
| Who refreshes tokens            | Agent Handler                                                                                                | Your product                                                                       |
| When a credential stops working | The agent calls an `authenticate_*` tool and the user reconnects through Link                                | The tool call returns `reauth_required` and your product collects a new credential |

Agent Handler never stores a credential sent in `X-Tool-Auth`. It removes the header before the request is logged, so the credential doesn't appear in [Tool Call Logs](/merge-agent-handler/observe/tool-call-logs).

> **Note**
>
> Once passthrough is on, `tools/list` stops returning `authenticate_*` tools for every Tool Pack in your organization, because the reconnect flow belongs to your product.

## Get the fields each Connector needs

Each Connector supports one or more auth options, OAuth2 or Secrets, and each option needs its own fields. Fetch them once per Connector with `auth_mode=runtime`. The list endpoint, `GET /api/v1/connectors/?auth_mode=runtime`, returns the same shape for every Connector.

```bash
curl https://ah-api.merge.dev/api/v1/connectors/zendesk/?auth_mode=runtime \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY"
```

```json
{
  "name": "Zendesk",
  "slug": "zendesk",
  "auth_options": [
    {
      "type": "Secrets",
      "credential_fields": [
        { "name": "subdomain", "required": true },
        { "name": "email", "required": true },
        { "name": "api_token", "required": true }
      ]
    },
    {
      "type": "OAuth2",
      "credential_fields": [
        { "name": "access_token", "required": true },
        { "name": "subdomain", "required": true }
      ]
    }
  ]
}
```

Runtime mode returns one entry per auth type. For OAuth2 it adds `access_token` as the first required field. When a Connector has more than one option of the same type, the entry lists the fields of all of them together, so send every field marked `required`.

### Build the form

To render a form for your end users, call the same endpoint without `auth_mode`. Each field comes back with a display label, an input type, and the dropdown values for `SELECT` fields. `prefix` and `suffix` show the text around the value, like `https://` and `.zendesk.com` around a subdomain.

```bash
curl https://ah-api.merge.dev/api/v1/connectors/zendesk/ \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY"
```

```json
{
  "auth_options": [
    {
      "type": "Secrets",
      "secrets": [
        { "name": "subdomain", "human_readable_name": "Subdomain", "prefix": "https://", "suffix": ".zendesk.com", "field_type": "TEXT" },
        { "name": "email", "human_readable_name": "Email", "field_type": "TEXT" },
        { "name": "api_token", "human_readable_name": "API token", "field_type": "TEXT" }
      ]
    }
  ]
}
```

The `name` of each field is the key you send in `X-Tool-Auth`. For a `SELECT` field, send the `value` of one of its `options`, not the `label`.

### Find the OAuth scopes

For OAuth2 Connectors, you register your own OAuth app with the provider and run the sign-in yourself. To see which scopes to request, fetch the Connector with `include_tool_details=true`. Each tool lists the scopes it needs.

```bash
curl https://ah-api.merge.dev/api/v1/connectors/zendesk/?include_tool_details=true \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY"
```

```json
{
  "tools": [
    { "name": "list_tickets", "scopes": ["tickets:read"] },
    { "name": "create_ticket", "scopes": ["tickets:write"] }
  ]
}
```

Request the scopes of every tool in your Tool Pack. A tool whose scope you didn't request fails when the provider rejects the call.

## Build the X-Tool-Auth header

`X-Tool-Auth` is a JSON object with `auth_type` set to the option's `type`, plus one key per credential field.

**`OAuth2`**

```json title="OAuth2"
{ "auth_type": "OAuth2", "access_token": "eyJhbGciOi...", "subdomain": "acme" }
```

**`Secrets`**

```json title="Secrets"
{ "auth_type": "Secrets", "subdomain": "acme", "email": "alice@acme.com", "api_token": "zd_live_8f2k..." }
```

Agent Handler checks that every required field is present. It doesn't check the values, so limit `SELECT` fields to their `options` in your UI. A value the provider doesn't accept fails at the provider.

## Call a tool

Send your Access Key in `Authorization` and the end user's credential in `X-Tool-Auth` on the Tool Pack MCP endpoint. The Registered User in the URL is the person the credential belongs to.

```bash
curl -X POST https://ah-api.merge.dev/api/v1/tool-packs/$TOOL_PACK_ID/registered-users/$REGISTERED_USER_ID/mcp \
  -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \
  -H 'X-Tool-Auth: {"auth_type": "OAuth2", "access_token": "eyJhbGciOi...", "subdomain": "acme"}' \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "zendesk__list_tickets", "arguments": {}}}'
```

```python
import asyncio
import json
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def run(tool_pack_id: str, registered_user_id: str, access_token: str):
    headers = {
        "Authorization": "Bearer <YOUR_API_KEY>",
        "X-Tool-Auth": json.dumps(
            {"auth_type": "OAuth2", "access_token": access_token, "subdomain": "acme"}
        ),
    }
    url = f"https://ah-api.merge.dev/api/v1/tool-packs/{tool_pack_id}/registered-users/{registered_user_id}/mcp"

    async with streamablehttp_client(url, headers=headers) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool("zendesk__list_tickets", {})
            print(result)
```

Headers apply to every call in an MCP session, so open one session per Registered User, and a new one after you refresh a token.

## Errors

| What happened                                                                                                                             | What you get back                                                                     |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Passthrough isn't turned on for your organization                                                                                         | JSON-RPC error `-32600`, "Passthrough credentials not enabled for this organization." |
| `X-Tool-Auth` isn't valid JSON, or `auth_type` is missing or not `OAuth2` or `Secrets`                                                    | JSON-RPC error `-32602` with the reason                                               |
| The call is to an `authenticate_*` tool with `X-Tool-Auth` set                                                                            | JSON-RPC error `-32602`, "Authenticate tool is not supported in passthrough mode."    |
| A required field is missing, the Connector doesn't support that `auth_type`, or the provider reports the credential as invalid or expired | A tool result with `isError: true` and `error_reason` `reauth_required`               |

A `reauth_required` result looks like this, with `data` also carrying a `reauth_reason`:

```json
{
  "error_reason": "reauth_required",
  "error_code": 401,
  "error_message": "The provided credential is invalid or expired. Please provide a valid credential.",
  "data": { "tool_name": "zendesk__list_tickets" }
}
```

When you get one, refresh the token if it expired, or ask the end user to reconnect in your product, then retry the call. The message is the same for a missing field and a rejected credential, so check your header against the Connector's `credential_fields` first when you're building.

## Next steps

#### [Registered Users](/merge-agent-handler/build/users/registered-users)

Create the Registered User each credential belongs to

#### [MCP integration](/merge-agent-handler/build/connecting-agents/mcp-integration)

Connect your agent to the Tool Pack MCP endpoint

#### [Tool Call Logs](/merge-agent-handler/observe/tool-call-logs)

Debug passthrough calls by Registered User and tool

#### [Link](/merge-agent-handler/build/authentication/link)

Let Agent Handler store and refresh credentials instead