Skip to navigation

Passthrough credentials

Send your end users' credentials on each tool call instead of storing them in Agent Handler

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

LinkPassthrough
Where credentials liveAgent Handler stores them against the Registered UserYour product stores them
Who runs OAuthAgent Handler, using your Application CredentialsYour product, using your own OAuth app with each provider
Who refreshes tokensAgent HandlerYour product
When a credential stops workingThe agent calls an authenticate_* tool and the user reconnects through LinkThe 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.

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.

curl https://ah-api.merge.dev/api/v1/connectors/zendesk/?auth_mode=runtime \
-H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY"
{
"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.

curl https://ah-api.merge.dev/api/v1/connectors/zendesk/ \
-H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY"
{
"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.

curl https://ah-api.merge.dev/api/v1/connectors/zendesk/?include_tool_details=true \
-H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY"
{
"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
{ "auth_type": "OAuth2", "access_token": "eyJhbGciOi...", "subdomain": "acme" }
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.

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": {}}}'
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 happenedWhat you get back
Passthrough isn’t turned on for your organizationJSON-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 SecretsJSON-RPC error -32602 with the reason
The call is to an authenticate_* tool with X-Tool-Auth setJSON-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 expiredA tool result with isError: true and error_reason reauth_required

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

{
"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