Link

The embedded UI your end users authenticate Connectors through.

Link is the embedded authentication UI. Your product opens Link, the user picks a Connector, completes OAuth (or pastes an API key), lands back in your app. The result is a credential stored against their Registered User - Agent Handler holds it; your code never sees it.

This page covers Link in your product. For email-driven flows or any context where you can’t render a React component, use Magic Link instead.

Two trigger patterns; pick one or both.

Pre-flight, from a settings page. The user clicks “Connect Slack” in your UI before the agent ever runs. Most B2B products do this - users prefer setting up integrations once rather than being interrupted mid-conversation. Your backend mints a link token; your frontend opens Link.

At agent runtime. The agent calls a tool the user hasn’t authenticated. The Connector returns an authenticate_meta tool result with a link_token. Your frontend catches it, opens Link, the user authenticates, the original tool call retries. No separate “missing auth” UI needed.

Backend call. Mint one link token per Link session. Tokens are single-use and expire after 30 minutes.

$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" }'
1{
2 "link_token": "ltk_•••••",
3 "expires_at": "2026-05-04T18:30:00Z"
4}

The Connector field is the Connector slug - lowercase with hyphens, like google-drive, microsoft-teams, salesforce. The full list is in the Connector catalog.

If you want the user to pick from a list of Connectors instead of locking to one, omit Connector from the request body. Link will show the picker for every Connector configured on the Tool Pack.

Install the React component:

$npm install @mergeapi/react-agent-handler-link

Open Link with the token your backend just minted:

ConnectButton.tsx
1import { useAgentHandlerLink } from "@mergeapi/react-agent-handler-link";
2
3export function ConnectSlackButton({ linkToken }: { linkToken: string }) {
4 const { open, isReady } = useAgentHandlerLink({
5 linkToken,
6 onSuccess: (event) => {
7 // Credentials stored. event.Connector is the slug; event.scopes the granted scopes.
8 // Refresh your UI, or kick off the agent action.
9 },
10 onExit: (error) => {
11 // User closed Link or auth failed.
12 // error is null on a clean exit, populated on failure.
13 },
14 });
15
16 return (
17 <button disabled={!isReady} onClick={() => open()}>
18 Connect Slack
19 </button>
20 );
21}

isReady flips to true once the component has loaded the token and is ready to open. Disable your button until then to avoid no-op clicks.

For projects without React, the same UI is reachable via a hosted URL: https://ah.merge.dev/link/<LINK_TOKEN>. Open it in a popup or redirect; on success, your redirect_uri (configured in Link customization) gets hit with the result.

The runtime trigger pattern

When the agent calls a tool the user hasn’t authenticated, Agent Handler returns a special authenticate_meta payload instead of failing the call:

1{
2 "type": "authenticate_meta",
3 "Connector": "slack",
4 "link_token": "ltk_•••••",
5 "expires_at": "2026-05-04T18:30:00Z",
6 "message": "Please connect Slack to continue."
7}

Your frontend should intercept the streamed tool-call result, detect the authenticate_meta shape, and open Link with the embedded token. After onSuccess fires, retry the user’s original prompt - the agent’s next call to the same tool will succeed because credentials are now stored.

Pattern in TypeScript, against an Anthropic-style stream:

1for await (const event of agentStream) {
2 if (event.type === "function_call_result" && event.result?.type === "authenticate_meta") {
3 setPendingLinkToken(event.result.link_token);
4 break; // Stop streaming; resume after auth.
5 }
6}

Then render <ConnectButton linkToken={pendingLinkToken} onSuccess={() => resumeAgent()} />.

Callback URLs and CORS

Link redirects through the third party’s OAuth consent screen and back. The URL the third party redirects to has to be on Agent Handler’s allow-list - that’s a security check.

In Settings → API Keys → Allowed callback origins, add every domain that hosts your frontend. The match is exact - app.example.com and staging.example.com need separate entries. Localhost ports for development need entries too.

If you skip this, you’ll see “unauthorized origin” on the callback. See Troubleshooting → Allowed callback origin error.

Common errors

SymptomCauseFix
open() does nothingLink token expired (30-min window)Generate a fresh token per session
open() fires but Link flashes and closesToken already usedTokens are single-use; generate a new one
OAuth callback “unauthorized origin”Frontend domain not on allow-listAdd to Allowed callback origins
onSuccess fires but tool calls still failToken storage delayed (rare)Retry after a short backoff, or rely on the authenticate_meta retry path

Logo, colors, header copy, redirect URL, allowed Connectors - all configurable per organization at Configure → Link. See Link customization for the asset specs and what end users see if you configure nothing.

Next

For email and mobile flows where you can’t render a React component, use Magic Link instead.