Appearance
Fresh 2026
Magic Link
How Magic Link works in Agent Handler: when to use it instead of embedded Link, callback URLs, and authorization code exchange for server-to-server flows.
Magic Link is a signed URL the user opens in their own browser to authenticate a Connector. The user clicks, completes OAuth at the third party, lands either back in your app (via a callback URL) or on a generic "you're done" page, and credentials are stored against their Registered User.
Magic Link vs embedded Link
Both flows produce the same outcome but apply in different contexts.
| Embedded Link | Magic Link | |
|---|---|---|
| What runs in the user's browser | A React component you embed | A page Agent Handler hosts |
| When to use | Your product is a web app and you control the frontend | Email flows, mobile apps, agent responses, anywhere you don't have a browser context to render a component |
| Where the user starts | A button in your UI | A link they tap from email, chat, an agent message |
| Where they end up | Back in your app via onSuccess | Back at your callback_url, or a generic done page |
Use embedded Link when you can. Use Magic Link when you can't render the component - most commonly when the auth needs to happen in a different browser session than where the user got the link.
Generating a Magic Link
Backend call. Hit the same /link-token endpoint as embedded Link; the response includes a magic_link_url you can hand to the user.
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_slug": "linear" }'json
{
"link_token": "ltk_•••••",
"magic_link_url": "https://ah-api.merge.dev/magic-link/•••••"
}Send magic_link_url to the user - email, chat message, in-app notification, the body of an agent response. They open it in any browser, complete OAuth, and the credential is stored.
At-runtime via the agent
When the agent calls a tool the user hasn't authenticated, the Connector returns an authenticate_meta payload that includes the Magic Link URL ready to share:
json
{
"type": "authenticate_meta",
"Connector": "linear",
"magic_link_url": "https://ah-api.merge.dev/magic-link/•••••",
"link_token": "ltk_•••••",
"message": "Share this link with the user to connect their Linear account. After they authenticate, refetch tools or restart your MCP connection."
}The message field is written for the agent - the model can include it verbatim in its reply to the user. The user clicks the link, authenticates, and the agent's next attempt at the same tool succeeds.
Callback URLs
Without a callback URL, the user lands on a generic "authentication complete" page after auth. With one, they're redirected back to your app.
Set up:
- Register the origin. At Settings → API Keys → Allowed callback origins, add the origin (scheme + host) of your callback URL. Origins must be HTTPS for web; custom schemes (
myapp://,tauri://) are allowed for mobile and desktop apps. - Pass
callback_urlwhen minting the token.
json
POST /api/registered-users/{id}/link-token
{
"connector_slug": "linear",
"callback_url": "https://myapp.com/integrations/done"
}After the user authenticates (or exits), the browser redirects to your callback URL with query parameters appended:
| Parameter | Always present? | Meaning |
|---|---|---|
status | Yes | success / error / exit |
code | Only with auth code exchange | The authorization code (see below) |
state | Only with auth code exchange | The state value you provided |
Don't append your own ?state= to the callback URL - Agent Handler appends it for you. Doubled-up query parameters break the flow.
Mobile and desktop deep links
Same flow, custom URL scheme. Register myapp:// as an allowed origin, pass callback_url: "myapp:///integrations/done". After authentication the browser redirects to that scheme, your OS routes it to your app, your app reads the status parameter.
Authorization code exchange (for server-to-server flows)
By default, credentials are stored the moment the user finishes authenticating in the browser. For higher-security flows - most common with mobile and desktop apps where the browser session and your backend are different processes - you can require a server-to-server step before the credential is created.
The flow:
Mint the link token with both
callback_urlandstate. The presence ofstatetriggers authorization code mode.jsonPOST /api/registered-users/{id}/link-token { "connector_slug": "linear", "callback_url": "https://myapp.com/integrations/callback", "state": "random-csrf-token-123" }The user authenticates. The callback URL is hit with
status=success&code=ah_code_xyz&state=random-csrf-token-123. The credential is not yet stored.Validate
state. It should match the value you minted with. If not, abort - this is a CSRF attempt.Exchange the code from your backend. Hit the confirmation endpoint with the code. Agent Handler creates the credential and returns the IDs.
bashcurl -X POST https://ah-api.merge.dev/api/v1/link-token/confirm/ \ -H "Authorization: Bearer $MERGE_AGENT_HANDLER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "ah_code_xyz" }'json{ "credential_id": "134e0111-0f67-44f6-98f0-597000290bb3", "connector_slug": "linear", "registered_user_id": "550e8400-e29b-41d4-a716-446655440000" }
Codes expire after 5 minutes. They're single-use. They can only be exchanged by the same organization that minted them.
The state-and-code dance is OAuth's standard CSRF protection: the redirect comes from the user's browser, but the code exchange comes from your backend. Even if an attacker hijacks the redirect, they can't redeem the code without the API key.
When to use code exchange
- Mobile and desktop apps. The browser flow and the app are separate processes; code exchange lets the app verify the auth before trusting it.
- Compliance. Some security reviews want server-to-server credential creation as a hard requirement.
- Pre-credential validation. You want to check the user's identity in your own system before the credential is created on Agent Handler's side.
For simpler web flows where the user authenticates and closes the tab, omit state - credentials are created immediately, no exchange step needed.
Errors during code exchange
| Status | Error | Meaning |
|---|---|---|
400 | invalid_authorization_code | Code doesn't exist or was already used |
400 | authorization_code_expired | Older than 5 minutes |
403 | code_does_not_belong_to_organization | Wrong API key for the org that minted the code |
Treat all of these as terminal - re-mint a fresh link token and start over.
Next
Brand the Link experience with your logo and colors via Link customization.