In this article
August 27, 2026
August 27, 2026

Three ways to let an AI agent call third-party APIs on behalf of a user

Store the token yourself, fetch it at runtime, or never hold it at all. Where the credential ends up in each pattern, what each one costs, and how to pick without guessing.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

Your agent needs to read a user's Linear issues, post to their Slack, or pull their HubSpot contacts. The user has an account at each of those places. You do not. So the agent has to act on the user's behalf, using the user's authority, which means somewhere in your system there is an OAuth credential belonging to a person.

Where that credential lives is the entire design decision. Everything else, the scopes, the refresh logic, the error handling, follows from it.

There are three patterns in practice. Here they are, in the order most teams encounter them.

The short answer

Pick by where the code runs, not by which one sounds cleanest.

  • The code runs in an untrusted or agent-controlled runtime. Use a proxy so the token never arrives. The agent sends its request through WorkOS, which injects the credential server side.
  • The code runs on trusted infrastructure and needs throughput, large payloads, or a provider outside a proxy's allowlist. Fetch the token at runtime and call the provider directly.
  • You are storing long-lived provider tokens in your own database. This is the pattern to migrate off, not toward.

The rest of this post is what each one actually involves, and the specific conditions that should push you from one to the next.

Pattern 1: Store the credential yourself

You run the OAuth dance, get an access token and a refresh token, and put both in your database. When the agent needs to call the provider, it reads the token from your store and makes the request.

This is where nearly everyone starts, because it is what the provider's quickstart tells you to do, and it works on the first day.

What it actually commits you to is a small piece of security infrastructure per provider. You own encryption at rest for the tokens, key rotation, refresh scheduling before expiry, retry behavior when refresh fails, revocation detection when a user removes your app on the provider's side, and a reauthorization flow when any of that goes wrong. Every provider expires tokens differently, rotates refresh tokens differently, and signals revocation differently. You will discover each of these behaviors in production.

Then multiply that by the number of providers you support, and note that none of it is your product.

There is one honest reason to choose this pattern anyway: you need a provider nothing else supports, and you need it now. That is a real situation. It is just worth knowing you are taking on a maintenance surface, not shipping a feature.

Pattern 2: Fetch the credential at runtime

Instead of storing the token, you ask for it when you need it. Pipes access tokens work this way: your backend calls the vend credentials endpoint with a user ID, gets a fresh provider token back, and calls the provider directly.

  
const { accessToken, error } = await workos.pipes.getAccessToken({
  provider: 'github',
  userId: userId,
  organizationId: organizationId,
});
  

Refresh is handled for you, so the token you receive is current. The response also tells you when something is wrong in a way you can act on, including a missingScopes array, which is the sort of thing that is obvious in hindsight and painful to build yourself. A user who has no usable connection comes back as a 200 with {"active": false} rather than an exception, so the not-connected case is a branch rather than a surprise.

You have now deleted the credential storage problem. What you have not changed is where the token ends up: your code holds a live provider token, in your process, for the duration of the call. On trusted infrastructure that is fine, and it is the right pattern for a lot of real work. It is also the pattern that gives you the provider's full API with no intermediary, at whatever request rate and payload size the provider allows.

Pattern 3: Never hold the credential at all

The third option is to stop asking for the token and instead describe the call you want made. Pipes Relay, currently in early access, is a forward HTTP proxy for exactly this. You send your request to WorkOS with the target URL in a header and the user named in another, and WorkOS attaches that user's credential on the way out and streams the provider's response back.

  
curl --request GET https://api.workos.com/relay \
  --header "Authorization: Bearer sk_example_123456789" \
  --header "X-Relay-URL: https://api.github.com/user/repos?per_page=5" \
  --header "X-Relay-User: user_01EHZNVPK3SFK441A1RGBFSHRT"
  

Your Authorization header carries your WorkOS API key rather than the user's token, and it is stripped before the request is forwarded. The provider is resolved from the target URL's host. Method, body, and content headers pass through unchanged, so converting a direct call into a proxied one is a header edit rather than a rewrite.

The reason this matters for agents specifically is that an agent runtime is a poor place to keep a secret. It reads attacker-controlled text, it logs its own tool calls, it persists state between steps, and it acts on conclusions it reached itself. A token that enters that environment can be copied into a context window, a log line, an error payload, or an outbound request, none of which require a bug. Removing the token removes that whole class of outcome. A compromised agent can still make provider calls it should not make, but it cannot walk away with a durable credential.

How the three compare

Criterion Store it yourself Fetch at runtime Relay
You receive Nothing, you already have it The provider access token The provider's API response
Who calls the provider Your code Your code WorkOS
Token in your environment Yes, permanently Yes, per call Never
Who handles refresh You WorkOS WorkOS
Missing connection looks like Whatever you build 200 with {"active": false} 402 with an authorization_url
Payload and timeout limits The provider's The provider's 5 MB body, 30 second upstream timeout
Reachable providers Anything you integrate Anything Pipes supports Supported providers, on allowlisted hosts
Best for Nothing, if you have an alternative Trusted infrastructure and long-running syncs Agents, sandboxes, and untrusted runtimes

The last three rows are where the real decision lives, and they are the rows a vendor comparison usually leaves out.

When Relay is the wrong choice

A proxy sits in the middle of every request, which means its limits become your limits.

  • Large payloads. Relay forwards request bodies up to 5 MB. If you are uploading files to Box or Dropbox at any real size, you want a direct call.
  • Long operations. The upstream timeout is 30 seconds, and a provider that does not respond in time yields a 502. Report generation, bulk exports, and anything else that legitimately takes a minute do not fit.
  • Providers outside the allowlist. Requests can only target a supported provider's allowed hosts, and anything else is rejected before it is sent. That is a deliberate security property rather than an oversight, since it is what stops an injected credential from being sent somewhere unexpected. It also means an unsupported provider is simply not reachable this way.
  • High-volume syncs. Nightly jobs pulling every record a user has are throughput work on infrastructure you already trust. Adding a hop buys you nothing there, which is presumably why the docs point that case at access tokens.
  • Anything that follows redirects. Relay does not follow them, and returns redirect responses as-is. If your provider call depends on following a 302, you handle it yourself.

None of these are arguments against the pattern. They are the reason both patterns exist, and a team shipping a real product will probably use both: Relay for the agent, access tokens for the sync job.

A rule you can apply without thinking

Ask one question about the code making the call. Would I be comfortable if this code printed everything in its memory to a log I do not control?

If yes, it is trusted infrastructure, and fetching the token at runtime is fine. If no, and for an agent runtime the answer is no, the token should not be there in the first place.

That framing also makes the migration path obvious. You do not have to move everything. You move the code that runs in the untrusted places, which is usually the newest and smallest part of your system, and leave the sync jobs alone.

Where this leaves MCP

Worth noting because it is the same question wearing different clothes.

Most MCP servers today handle their own OAuth and keep their own tokens, which means every server you run is an instance of pattern 1, with its own storage, its own refresh logic, and its own blast radius. Pointing MCP tool implementations at a proxy instead means the tokens live in one place and each server holds nothing worth stealing. The interesting part is that this changes almost nothing about the tools themselves. It is a swap of where the request goes.

What none of this decides

All three patterns answer where the credential lives. None of them answer what the agent is allowed to do with it.

OAuth scopes are the only lever you get from the provider, and they were designed for applications rather than agents, so they tend to be coarse. A token that can read the repositories an agent needs can generally read every repository the user can see. A proxy is a natural place to eventually enforce something finer, because every call already passes through it, but a chokepoint existing is not the same as a policy existing.

So treat this as the first of two decisions. Getting the credential out of the agent's reach is the one with a clean answer today. Deciding what the agent may do once it can act is still open, and anyone telling you otherwise is selling something.