<!-- llms.txt: https://workos.com/llms.txt -->

# Token proxy

## Overview

The token proxy calls third-party APIs on your users' behalf without exposing
their credentials to your application. Send the request to WorkOS, and the token
proxy attaches the user's credential server-side and streams the provider's
response back.

> **Note:** The token proxy is in early access. Contact support
> [via email](mailto:support@workos.com) or Slack to request access for your
> environment.

The token proxy is a forward HTTP proxy built for AI agents and other untrusted
runtimes. With [access tokens](https://workos.com/docs/pipes#fetching-access-tokens), your
infrastructure fetches a provider access token and calls the provider directly,
so the token exists inside the calling environment, where it can be copied,
logged, or leaked. With the token proxy, the token never leaves WorkOS: the
agent authenticates with your WorkOS API key and names the user it acts for.

> **Note:** The token proxy removes provider tokens from the calling environment, not the
> WorkOS API key. That key authenticates every WorkOS API call for the
> environment, so treat it as a secret even in a sandbox: inject it at request
> time rather than baking it into agent-visible code or prompts, and rotate it
> from the *API Keys* section of the dashboard if a runtime is compromised.

## How it works

Every proxied request follows the same lifecycle:

1. **Authenticate.** Your WorkOS API key is verified and determines the
   environment. The key is stripped from the request and is never sent upstream.
2. **Resolve the user.** The `X-Token-Proxy-User` header identifies the user the
   request acts on behalf of, scoped by `X-Token-Proxy-Organization` when the
   connection belongs to an organization.
3. **Look up the connected account.** WorkOS finds the user's Pipes connection
   for the provider you named. If the user hasn't connected, or needs to
   re-authorize, the response is a `402` with an `authorization_url` to send them
   to.
4. **Fetch the credential.** The user's access token is retrieved from the Pipes
   credential store, and refreshed automatically if it has expired.
5. **Inject and forward.** Proxy control headers, cookies, and forwarding
   metadata are stripped, the provider credential is injected into the
   `Authorization` header, and the request is forwarded to the provider over
   HTTPS.
6. **Stream the response.** The provider's status, headers, and body are streamed
   back verbatim, with an added `X-Token-Proxy-Upstream-Status` header.

## Before you begin

The token proxy builds on your existing Pipes setup. You need:

- A WorkOS API key for the environment, from the
  [API Keys](https://dashboard.workos.com/environment/api-keys) section of the
  WorkOS Dashboard.
- The target provider enabled for your environment, configured in the
  [Pipes](https://dashboard.workos.com/environment/pipes) section of the
  dashboard or through the
  [data integration API](https://workos.com/docs/reference/pipes/data-integration/create).
- A connected account for the user, created when the user authorizes the provider
  through the [Pipes widget](https://workos.com/docs/widgets/pipes) or an authorization URL from the
  [get authorization URL](https://workos.com/docs/reference/pipes/connected-account/get-authorize-url)
  endpoint.

## Making requests

The base URL is `https://api.workos.com/token-proxy`, and any HTTP method is
accepted. To convert a direct provider call into a proxied one:

1. Keep the method, body, and content headers as they are.
2. Send the request to the token proxy base URL, and move the original URL into
   the `X-Token-Proxy-URL` header.
3. Replace the provider token with your WorkOS API key in `Authorization`.
4. Name the provider in `X-Token-Proxy-Provider` and the user in
   `X-Token-Proxy-User`, adding `X-Token-Proxy-Organization` for
   organization-scoped connections.

### Request headers

| Header                 | Required          | Description                                                                                                                                                                              |
| ---------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization`        | Yes               | `Bearer` followed by your WorkOS API key. Determines the environment. Never forwarded to the provider.                                                                                    |
| `X-Token-Proxy-User`         | Yes               | The ID of the user the request acts on behalf of, for example `user_01EHZ…`. Must belong to the API key's environment.                                                                    |
| `X-Token-Proxy-Organization` | Conditional       | The organization the connection was authorized under, for example `org_01KV9…`. Required when the connection is organization-scoped, and must be omitted when it isn't.                   |
| `X-Token-Proxy-Provider`     | URL routing only  | The slug of the provider to call, for example `github`. Named explicitly rather than inferred, since one API host can back several providers.                                              |
| `X-Token-Proxy-URL`          | URL routing only  | The full upstream URL, including path and query string. Must use HTTPS.                                                                                                                   |

All other request headers, including `Content-Type`, `Accept`, and
provider-specific headers, are forwarded unchanged. See
[limits and behavior](#limits-and-behavior) for the headers that are stripped.

### URL routing

Put the complete upstream URL in `X-Token-Proxy-URL` and the provider's slug in
`X-Token-Proxy-Provider`. Your code keeps working with the provider's real URLs,
and naming the provider keeps the target unambiguous when several providers share
an API host, as Gmail, Google Calendar, and generic Google access all do on
`www.googleapis.com`.

:::code-group

```bash language="curl"
curl --request GET https://api.workos.com/token-proxy \
  --header "Authorization: Bearer sk_example_123456789" \
  --header "X-Token-Proxy-Provider: github" \
  --header "X-Token-Proxy-URL: https://api.github.com/user/repos?per_page=5" \
  --header "X-Token-Proxy-User: user_01EHZNVPK3SFK441A1RGBFSHRT" \
  --header "X-Token-Proxy-Organization: org_01EHZNVPK3SFK441A1RGBFSHRT"
```

:::

### Path routing

Alternatively, prefix the provider's API path with the token proxy base URL and
the provider's slug. The slug in the path selects the provider, so no
`X-Token-Proxy-Provider` header is needed. The path and query string are appended
to the provider's default API host, so `/token-proxy/github/user` proxies to
`https://api.github.com/user`.

:::code-group

```bash language="curl"
curl --request GET "https://api.workos.com/token-proxy/github/user/repos?per_page=5" \
  --header "Authorization: Bearer sk_example_123456789" \
  --header "X-Token-Proxy-User: user_01EHZNVPK3SFK441A1RGBFSHRT" \
  --header "X-Token-Proxy-Organization: org_01EHZNVPK3SFK441A1RGBFSHRT"
```

:::

URL routing is the better default: it can reach any of a provider's
[allowed hosts](#supported-providers) rather than only the default one, and it
keeps the provider's own URLs intact in your code and logs.

## Examples

### Query Linear

:::code-group

```bash language="curl"
curl --request POST https://api.workos.com/token-proxy \
  --header "Authorization: Bearer sk_example_123456789" \
  --header "X-Token-Proxy-Provider: linear" \
  --header "X-Token-Proxy-URL: https://api.linear.app/graphql" \
  --header "X-Token-Proxy-User: user_01EHZNVPK3SFK441A1RGBFSHRT" \
  --header "X-Token-Proxy-Organization: org_01EHZNVPK3SFK441A1RGBFSHRT" \
  --header "Content-Type: application/json" \
  --data '{"query": "{ viewer { id name email } }"}'
```

:::

### Post a Slack message

:::code-group

```bash language="curl"
curl --request POST https://api.workos.com/token-proxy \
  --header "Authorization: Bearer sk_example_123456789" \
  --header "X-Token-Proxy-Provider: slack" \
  --header "X-Token-Proxy-URL: https://slack.com/api/chat.postMessage" \
  --header "X-Token-Proxy-User: user_01EHZNVPK3SFK441A1RGBFSHRT" \
  --header "X-Token-Proxy-Organization: org_01EHZNVPK3SFK441A1RGBFSHRT" \
  --header "Content-Type: application/json" \
  --data '{"channel": "C01XXXXXXXX", "text": "Hello from the Pipes token proxy"}'
```

:::

## Organization scoping

Pipes connections are either scoped to an organization or user-only, and the
connection lookup requires an exact match on that scope:

- If the user authorized the provider under an organization, requests must
  include `X-Token-Proxy-Organization` with that organization's ID.
- If the user authorized the provider without an organization, the header must be
  omitted.

> **Note:** A scope mismatch in either direction returns the same `402`
> `token_proxy_authorization_required` response as a user who never connected at all.
> If a connected user keeps getting a `402`, check that the
> `X-Token-Proxy-Organization` header matches the organization on their connected
> account before asking them to re-authorize.

## Handling authorization

When the user has no usable connection for the provider—they never connected,
their grant was revoked, or the token can no longer be refreshed—the response is
`402 Payment Required`:

:::code-group

```json language="json"
{
  "code": "token_proxy_authorization_required",
  "connection": "github",
  "message": "User has not authorized provider \"github\"",
  "authorization_url": "https://github.com/login/oauth/authorize?client_id=..."
}
```

:::

Surface the `authorization_url` to the user, let them complete the provider's
OAuth flow, then retry the original request.

`authorization_url` is `null` when WorkOS can't build an authorization URL for
the provider, which usually means the provider's Pipes configuration is
incomplete. Fall back to your own connection flow, such as the
[Pipes widget](https://workos.com/docs/widgets/pipes), instead of assuming the field is present.

> **Note:** Authorization-required responses use `402`, not `401` or `403`, so they can't
> be confused with a WorkOS authentication failure (`401`) or a
> provider-returned `401` or `403` passed through from upstream.

## Responses

On success you receive the provider's status code, headers, and body exactly as
the provider sent them, streamed as they arrive. Provider errors are passed
through untouched, so a GitHub `404` comes back as a `404` with GitHub's own
error body.

Every proxied response carries an `X-Token-Proxy-Upstream-Status` header with the
provider's status code. Its presence means the request reached the provider, so
you can distinguish an upstream `404` from a token proxy `404` such as an unknown
provider.

## Limits and behavior

| Behavior                  | Detail                                                                                                                                                                                     |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Request body              | Forwarded byte-for-byte, up to 5 MB. `GET` and `HEAD` requests send no body.                                                                                                                |
| Upstream timeout          | 30 seconds. A provider that doesn't respond in time yields `502 token_proxy_upstream_error`.                                                                                                      |
| HTTPS only                | `X-Token-Proxy-URL` must use HTTPS. Credentials are never injected into a plaintext request.                                                                                                      |
| Host allowlist            | Requests can only target a supported provider's allowed hosts. Anything else is rejected before the request is sent.                                                                         |
| Redirects                 | Not followed. Redirect responses are returned as-is.                                                                                                                                       |
| Stripped request headers  | Hop-by-hop headers such as `Connection` and `Transfer-Encoding`, `Cookie`, forwarding metadata such as `X-Forwarded-*`, `Via`, and `X-Real-IP`, your `Authorization` header, and anything prefixed `X-Token-Proxy-` or `X-WorkOS-`. |
| Stripped response headers | Hop-by-hop headers and `Set-Cookie`. Compressed bodies are decoded in transit, so `Content-Encoding` and `Content-Length` may be removed.                                                   |

## Supported providers

The token proxy supports the following providers. With path routing, the request
goes to the provider's base API host. With URL routing, any listed host is
allowed. The provider must also be enabled for your environment.

Each provider's API domains are part of its WorkOS configuration, so this list
grows as providers are added without any change on your side.

| Provider   | Slug         | Allowed hosts                    |
| ---------- | ------------ | -------------------------------- |
| Asana      | `asana`      | `app.asana.com`                  |
| Attio      | `attio`      | `api.attio.com`                  |
| Box        | `box`        | `api.box.com`                    |
| Calendly   | `calendly`   | `api.calendly.com`               |
| Datadog    | `datadog`    | `api.datadoghq.com`              |
| Dropbox    | `dropbox`    | `api.dropboxapi.com`             |
| Front      | `front`      | `api2.frontapp.com`              |
| GitHub     | `github`     | `api.github.com`, `github.com`   |
| GitLab     | `gitlab`     | `gitlab.com`                     |
| Gong       | `gong`       | `api.gong.io`                    |
| Google     | `google`     | `www.googleapis.com`             |
| Help Scout | `helpscout`  | `api.helpscout.net`              |
| HubSpot    | `hubspot`    | `api.hubapi.com`                 |
| Intercom   | `intercom`   | `api.intercom.io`                |
| Jira       | `jira`       | `api.atlassian.com`              |
| Linear     | `linear`     | `api.linear.app`                 |
| Microsoft  | `microsoft`  | `graph.microsoft.com`            |
| Notion     | `notion`     | `api.notion.com`                 |
| npm        | `npm`        | `registry.npmjs.org`             |
| QuickBooks | `quickbooks` | `quickbooks.api.intuit.com`      |
| Salesforce | `salesforce` | `login.salesforce.com`           |
| Sentry     | `sentry`     | `sentry.io`                      |
| Slack      | `slack`      | `slack.com`, `api.slack.com`     |
| Stripe     | `stripe`     | `api.stripe.com`                 |
| Xero       | `xero`       | `api.xero.com`                   |
| Zendesk    | `zendesk`    | `api.zendesk.com`                |
| Zoom       | `zoom`       | `api.zoom.us`                    |

## Error reference

Errors generated by the token proxy are JSON objects with `code` and `message`
fields. Provider errors are passed through unchanged, so check for
`X-Token-Proxy-Upstream-Status` to tell the two apart.

| Status | Code                            | Meaning                                                                                                                                              |
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | —                               | Missing or invalid WorkOS API key.                                                                                                                   |
| `400`  | `token_proxy_missing_url`             | URL routing was used but the `X-Token-Proxy-URL` header is absent.                                                                                          |
| `400`  | `token_proxy_provider_required`        | URL routing was used but the `X-Token-Proxy-Provider` header is absent.                                                                                     |
| `400`  | `token_proxy_invalid_url`             | `X-Token-Proxy-URL` isn't a valid URL or doesn't use HTTPS, or the resolved target host isn't one of the provider's allowed hosts.                           |
| `400`  | `token_proxy_user_required`           | The `X-Token-Proxy-User` header is absent.                                                                                                                  |
| `400`  | `token_proxy_user_not_found`          | No user with that ID exists in the API key's environment.                                                                                             |
| `402`  | `token_proxy_authorization_required`  | The user has no usable connection for this provider, or the organization scope doesn't match. Includes an `authorization_url`.                        |
| `404`  | `token_proxy_provider_not_found`      | The slug doesn't match a supported provider, or the provider isn't enabled in this environment.                                                        |
| `404`  | —                               | A generic `404` with no `code` field means the token proxy isn't enabled for this environment.                                                         |
| `502`  | `token_proxy_credential_error`        | The user's credential couldn't be resolved, for a reason that re-authorization won't fix.                                                              |
| `502`  | `token_proxy_upstream_error`          | The provider was unreachable or timed out.                                                                                                            |
