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 ↗.
Sandbox (short-lived JWT) → Worker proxy (validates JWT, injects real credential) → External APIThe 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 tovalidate— Extracts the JWT from the incoming request (returnsnullto reject)transform— Injects the real credential into the forwarded request (or returns aResponseto short-circuit)
You define only the service-specific logic. The framework handles JWT verification, routing, and error responses.
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.
- A Worker with a Sandbox binding (refer to Get started)
- The
jose↗ package installed in your Worker project for JWT signing
Store the API credential and a secret for signing JWT tokens in your Worker:
wrangler secret put MY_API_KEYwrangler secret put PROXY_JWT_SECRETGenerate a strong random value for PROXY_JWT_SECRET:
openssl rand -hex 32npm i joseyarn add josepnpm add joseThe 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 requestscreateProxyToken— Issues a signed JWT for a sandboxServiceConfig— The interface your service definitions implement
Create a ServiceConfig for each external API you want to proxy. This example proxies a generic HTTP API that expects a Bearer token:
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; },};import type { ServiceConfig } from '../proxy';
interface Env { MY_API_KEY: string; PROXY_JWT_SECRET: string;}
export const myApi: ServiceConfig<Env> = { // 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.
Register your services with createProxyHandler and issue tokens to sandboxes using createProxyToken:
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" }); },};import { getSandbox } from '@cloudflare/sandbox';import { createProxyHandler, createProxyToken } from './proxy';import { myApi } from './services/myapi';
export { Sandbox } from '@cloudflare/sandbox';
interface Env { Sandbox: DurableObjectNamespace; MY_API_KEY: string; PROXY_JWT_SECRET: string;}
const proxyHandler = createProxyHandler<Env>({ mountPath: '/proxy', jwtSecret: (env) => env.PROXY_JWT_SECRET, services: { myapi: myApi }});
export default { async fetch(request: Request, env: Env): Promise<Response> { 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.
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:
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:
import osimport 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.
To proxy additional APIs, define another ServiceConfig and add it to createProxyHandler:
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 },});export const anotherApi: ServiceConfig<Env> = { 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<Env>({ 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.
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_SECRETvalue 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:
const freshToken = await createProxyToken({ secret: env.PROXY_JWT_SECRET, sandboxId, expiresIn: "15m",});await sandbox.setEnvVars({ PROXY_TOKEN: freshToken });const freshToken = await createProxyToken({ secret: env.PROXY_JWT_SECRET, sandboxId, expiresIn: '15m'});await sandbox.setEnvVars({ PROXY_TOKEN: freshToken });The service name in the URL must match the key in the services object. A request to /proxy/myapi/... requires services: { myapi: ... }.
Log the request URL in transform to confirm the path is being rewritten correctly:
transform: async (req, ctx) => { console.log("Proxying to:", req.url); req.headers.set("Authorization", `Bearer ${ctx.env.MY_API_KEY}`); return req;};transform: async (req, ctx) => { console.log('Proxying to:', req.url); req.headers.set('Authorization', `Bearer ${ctx.env.MY_API_KEY}`); return req;}- Authentication example ↗ — complete multi-service implementation with GitHub, Anthropic, and R2
- Security model — how the Sandbox SDK approaches security
- Work with Git — Git operations in sandboxes
- Environment variables — simpler alternative for lower-risk credentials