Skip to content

Proxy requests to external APIs

When a sandbox needs to call an external API, you might pass credentials directly into the sandbox process. That approach works, but it means the sandbox holds a live credential that any code running inside it can read, copy, or misuse.

The proxy pattern removes that risk. Your Worker issues a short-lived JWT token to the sandbox. The sandbox uses that token for all API requests, which go to your Worker first. The Worker validates the JWT and injects the real credential before forwarding the request. Real credentials never enter the sandbox.

For a complete multi-service implementation covering GitHub, Anthropic, and R2, refer to the authentication example.

How it works

Sandbox (short-lived JWT) → Worker proxy (validates JWT, injects real credential) → External API

The proxy framework routes requests to named services. Each service is a ServiceConfig object with three fields:

  • target — Base URL of the external API to proxy to
  • validate — Extracts the JWT from the incoming request (returns null to reject)
  • transform — Injects the real credential into the forwarded request (or returns a Response to short-circuit)

You define only the service-specific logic. The framework handles JWT verification, routing, and error responses.

When to use this pattern

Use the proxy pattern when you need to:

  • Call external APIs from sandboxes without exposing credentials
  • Rotate credentials without reconfiguring sandboxes
  • Restrict what a sandbox can do (for example, limit to specific paths or methods)
  • Share one credential across many sandboxes without each holding a copy

For short-lived or low-risk credentials, environment variables may be simpler.

Prerequisites

  • A Worker with a Sandbox binding (refer to Get started)
  • The jose package installed in your Worker project for JWT signing

1. Set up secrets

Store the API credential and a secret for signing JWT tokens in your Worker:

Terminal window
wrangler secret put MY_API_KEY
wrangler secret put PROXY_JWT_SECRET

Generate a strong random value for PROXY_JWT_SECRET:

Terminal window
openssl rand -hex 32

2. Install dependencies

Terminal window
npm i jose

3. Copy the proxy framework

The proxy framework is a self-contained module you copy into your project. Download the src/proxy/ directory from the authentication example and place it at src/proxy/ in your Worker project.

The framework exports:

  • createProxyHandler — Creates the Worker request handler that routes and validates proxy requests
  • createProxyToken — Issues a signed JWT for a sandbox
  • ServiceConfig — The interface your service definitions implement

4. Define a service

Create a ServiceConfig for each external API you want to proxy. This example proxies a generic HTTP API that expects a Bearer token:

JavaScript
export const myApi = {
// All requests to /proxy/myapi/* are forwarded to this base URL
target: "https://api.example.com",
// Extract the JWT from the Authorization header
validate: (req) =>
req.headers.get("Authorization")?.replace("Bearer ", "") ?? null,
// Replace the JWT with the real API key before forwarding
transform: async (req, ctx) => {
req.headers.set("Authorization", `Bearer ${ctx.env.MY_API_KEY}`);
return req;
},
};

The transform function receives the outgoing request and a context object containing ctx.env (your Worker environment) and ctx.jwt (the verified token payload, including sandboxId). Return the modified request to forward it, or return a Response to short-circuit with an error.

5. Wire up the Worker

Register your services with createProxyHandler and issue tokens to sandboxes using createProxyToken:

JavaScript
import { getSandbox } from "@cloudflare/sandbox";
import { createProxyHandler, createProxyToken } from "./proxy";
import { myApi } from "./services/myapi";
export { Sandbox } from "@cloudflare/sandbox";
const proxyHandler = createProxyHandler({
mountPath: "/proxy",
jwtSecret: (env) => env.PROXY_JWT_SECRET,
services: { myapi: myApi },
});
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Route all /proxy/* requests through the proxy handler
if (url.pathname.startsWith("/proxy/")) {
return proxyHandler(request, env);
}
// Create a sandbox and issue it a short-lived token
const sandboxId = "my-sandbox";
const sandbox = getSandbox(env.Sandbox, sandboxId);
const token = await createProxyToken({
secret: env.PROXY_JWT_SECRET,
sandboxId,
expiresIn: "15m",
});
const proxyBase = `https://${url.hostname}`;
// Pass the token and proxy base URL to the sandbox
await sandbox.setEnvVars({
PROXY_TOKEN: token,
PROXY_BASE: proxyBase,
});
return Response.json({ message: "Sandbox ready" });
},
};

The mountPath (/proxy) and service name (myapi) together form the proxy route. A request to /proxy/myapi/some/path is validated and forwarded to https://api.example.com/some/path.

6. Call the proxy from the sandbox

Inside the sandbox, use the PROXY_TOKEN and PROXY_BASE environment variables to call the proxy. The JWT takes the place of the real credential:

Terminal window
curl "$PROXY_BASE/proxy/myapi/v1/endpoint" \
-H "Authorization: Bearer $PROXY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"input": "hello"}'

Or from Python running inside the sandbox:

Python
import os
import requests
response = requests.post(
f"{os.environ['PROXY_BASE']}/proxy/myapi/v1/endpoint",
headers={"Authorization": f"Bearer {os.environ['PROXY_TOKEN']}"},
json={"input": "hello"}
)

The real MY_API_KEY is never present in the sandbox. The Worker substitutes it transparently.

Adding more services

To proxy additional APIs, define another ServiceConfig and add it to createProxyHandler:

JavaScript
export const anotherApi = {
target: "https://api.another-service.com",
validate: (req) =>
req.headers.get("Authorization")?.replace("Bearer ", "") ?? null,
transform: async (req, ctx) => {
req.headers.set("Authorization", `Bearer ${ctx.env.ANOTHER_API_KEY}`);
return req;
},
};
// In your Worker:
const proxyHandler = createProxyHandler({
mountPath: "/proxy",
jwtSecret: (env) => env.PROXY_JWT_SECRET,
services: { myapi: myApi, another: anotherApi },
});

Each service is reachable at /proxy/<service-name>/*. The sandbox uses the same JWT token for all of them.

Troubleshooting

Proxy returns 401

The JWT is missing, expired, or signed with the wrong secret. Verify that:

  • The sandbox is using the token returned by createProxyToken, not a hardcoded value
  • The same PROXY_JWT_SECRET value is used to create and verify tokens
  • The token has not expired — the default is 15 minutes

To issue a fresh token and pass it to the sandbox:

JavaScript
const freshToken = await createProxyToken({
secret: env.PROXY_JWT_SECRET,
sandboxId,
expiresIn: "15m",
});
await sandbox.setEnvVars({ PROXY_TOKEN: freshToken });

Proxy returns 404 for the service

The service name in the URL must match the key in the services object. A request to /proxy/myapi/... requires services: { myapi: ... }.

transform returns unexpected results

Log the request URL in transform to confirm the path is being rewritten correctly:

JavaScript
transform: async (req, ctx) => {
console.log("Proxying to:", req.url);
req.headers.set("Authorization", `Bearer ${ctx.env.MY_API_KEY}`);
return req;
};