Skip to content
Docs

Build an agent with OpenAI Agents API on Vercel

Build and deploy an agent with OpenAI Agents API, Vercel Functions, Queues, and Sandbox for isolated code execution.

Allen ZhouMember of Technical Staff

The OpenAI Agents API hosts the Codex agent harness (OpenAI's coding-agent runtime), the inference loop, and session state. Vercel hosts the application and provides the infrastructure that connects each agent session to an execution environment.

The sample Next.js app uses Vercel Functions to create sessions, submit input, stream events, and receive signed webhooks. Vercel Queues handles lifecycle events, while Vercel Sandbox provides a persistent, isolated execution environment for each session. OpenAI runs the agent, and Vercel runs the application and the infrastructure that manages its Sandbox lifecycle.

Clone the sample app, customize it, and read on to see how it all works.

OpenAI Agents API on Vercel

OpenAI agent sessions connected to isolated Vercel Sandboxes through signed webhooks and a Vercel Queue.

Learn more

Copy link to headingWhat you will build

The finished app lets a user create an OpenAI agent session, submit a task, and stream its events. When input is submitted and the executor is disconnected, OpenAI sends a connection-required webhook. The handler starts or reconnects the Sandbox, then the waiting turn proceeds.

There are two independent paths:

  • The agent path carries user input and streamed output between your app and OpenAI.
  • The infrastructure path turns an OpenAI lifecycle webhook into a running Sandbox.

During a run, the pieces connect in this order:

  1. The app creates a session with a self_hosted environment and sends input.
  2. OpenAI sends a signed webhook when the session needs an executor.
  3. The webhook places the session ID on a Queue and responds immediately.
  4. A private consumer reads the current session state and creates or resumes its Sandbox.
  5. codex exec-server connects outbound from the Sandbox to OpenAI.
  6. OpenAI runs the agent and streams session events back to the app.

The OpenAI environment ID identifies the executor connection for the life of a session. It is different from the Vercel Sandbox ID. The app uses the OpenAI session ID to deterministically name the corresponding Sandbox.

Copy link to headingPrerequisites

Before you begin, you need:

  • An OpenAI project with access to the Agents API
  • A Vercel account with Sandbox and Queues access
  • Node.js 24 or later
  • The Vercel CLI
  • Permission to create an application key and an environment key in the same OpenAI organization and project

Copy link to headingCreate the OpenAI credentials

Open the OpenAI API keys page for the project with Agents API access and create an application key. Grant it api.agents.read, api.agents.write, and api.responses.write. The Vercel app uses this key to create and manage agents and sessions.

Create a second environment key for the executor from the Agents tab in the OpenAI platform dashboard. Both keys must belong to the same organization, project, and user or service account. The environment key is the only OpenAI credential passed into the Sandbox. Keep the broader application key and webhook signing secret in Vercel Functions.

Each key has a different scope and location:

Application keyEnvironment key
Permissionsapi.agents.read, api.agents.write, api.webhooks.write and api.responses.writeCreated from the Agents tab for executor connections
Where it livesVercel Functions environment variablesPassed into the Sandbox as CODEX_API_KEY
What it doesCreates agents and manages sessionsConnects codex exec-server to the session

Copy link to headingSet up the project

Clone the sample Next.js app and install its dependencies:

git clone https://github.com/vercel-labs/openai-agents-api-vercel
cd openai-agents-api-vercel
pnpm install

The project uses the official openai@7.15.0 TypeScript SDK, @vercel/queue, and @vercel/sandbox for the server-side integration. The interface uses streamdown to render streamed Markdown as the agent responds.

Create the reusable agent with the sample's SDK script:

OPENAI_API_KEY=YOUR_APPLICATION_KEY pnpm create:agent

The command prints an ID beginning with agent_. Save it as OPENAI_AGENT_ID. The agent ID is not a model ID or API key; it identifies the reusable agent configuration that each new session loads.

Link the app to a Vercel project:

vercel link

Deployed Functions authenticate to Sandbox and Queues automatically through OIDC. You will pull a local development token after configuring and deploying the project.

Add the application settings to .env.local:

OPENAI_API_KEY=YOUR_APPLICATION_KEY
OPENAI_AGENT_ID=agent_...
OPENAI_EXECUTOR_API_KEY=YOUR_OPENAI_ENVIRONMENT_KEY
OPENAI_WEBHOOK_SECRET=pending-webhook-registration
APP_PASSWORD=YOUR_DEMO_PASSWORD

Each value comes from a different part of setup:

VariableWhere it comes fromWhat it does
OPENAI_API_KEYThe application key created in the OpenAI projectCreates agents and manages sessions from Vercel Functions
OPENAI_AGENT_IDThe agent_... value returned by the agent creation requestSelects the reusable agent configuration for each session
OPENAI_EXECUTOR_API_KEYThe environment key created from the OpenAI project's Agents tabLets codex exec-server connect the Sandbox to the session
OPENAI_WEBHOOK_SECRETThe signing secret returned after webhook registrationVerifies that lifecycle requests came from OpenAI
APP_PASSWORDA strong value you chooseProtects the sample UI and session routes

APP_PASSWORD protects the sample UI and session routes with a signed, HTTP-only cookie. Choose a strong value. Replace this shared password with your identity provider and authorization policy in a production application.

You cannot add the real webhook secret yet. OpenAI creates it after you register the deployed webhook URL later in this guide.

Copy link to headingReview the project structure

The walkthrough follows these files in dependency order:

app/api/sessions/ Create, inspect, and delete sessions
app/api/sessions/[id]/input/ Send input and stream one turn
app/api/auth/ Create and clear the signed login cookie
app/api/webhook/ Verify and enqueue OpenAI webhooks
app/api/queues/provision/ Reconcile the session and Sandbox
components/demo.tsx Stream the conversation and display session events
lib/auth.ts Password and session-cookie verification
lib/config.ts Shared configuration
lib/openai.ts OpenAI Agents API client
lib/sandbox.ts Sandbox lifecycle and executor startup
lib/sse.ts Session event stream handling
lib/webhook.ts Webhook event routing
vercel.json Queue trigger configuration

The following snippets focus on the integration boundaries. The cloned project contains the complete implementations, including shared types, error handling, tests, and the demo interface.

Copy link to headingCreate the OpenAI session client

Create lib/openai.ts and initialize the official OpenAI TypeScript SDK:

import OpenAI from "openai";
import { config } from "@/lib/config";
function client() {
return new OpenAI({ apiKey: config.appApiKey });
}

Create a self-hosted session through client.beta.agents.sessions:

export async function createSession() {
return client().beta.agents.sessions.create({
agent_id: config.agentId,
environment: { type: "self_hosted", workspace_directory: "/workspace" },
});
}

Expose that helper from app/api/sessions/route.ts. The route returns the new session to the browser.

import { isAuthenticated } from "@/lib/auth";
import { createSession } from "@/lib/openai";
export async function POST() {
if (!(await isAuthenticated())) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
try {
return Response.json(await createSession(), { status: 201 });
} catch (error) {
console.error(error);
return Response.json(
{ error: error instanceof Error ? error.message : "Could not create session" },
{ status: 502 },
);
}
}

At this point OpenAI knows that the session requires a self-hosted executor, but there is no Sandbox yet.

Copy link to headingReceive OpenAI lifecycle webhooks

Create app/api/webhook/route.ts. This is the only public infrastructure endpoint OpenAI needs to call.

Verify the signature against the unmodified request body before parsing it:

const payload = await request.text();
const verifier = new OpenAI({
apiKey: "unused",
webhookSecret: config.webhookSecret,
});
try {
await verifier.webhooks.verifySignature(payload, request.headers);
} catch {
return new Response("Invalid signature", { status: 400 });
}
const event = JSON.parse(payload) as WebhookEvent;

Handle two event types:

  • agent.session.action_required wakes the infrastructure when OpenAI needs an environment_connection.
  • agent.session.failed removes compute for a failed session.

Publish the session ID to the Queue:

const sessionId = sessionIdToReconcile(event);
if (sessionId) {
await queue.send(
QUEUE_TOPIC,
{ sessionId },
event.id ? { idempotencyKey: event.id } : undefined,
);
}
return new Response("ok");

Using the OpenAI event ID as the Queue idempotency key deduplicates repeated webhook deliveries. Provisioning runs after the webhook responds, so OpenAI does not need to wait for a Sandbox to boot.

Copy link to headingReconcile the current OpenAI session

Queue delivery is at least once. A message may be retried, duplicated, or arrive after another message already connected the executor. The consumer must retrieve the latest OpenAI session instead of treating the webhook payload as a command.

Add retrieval to lib/openai.ts:

export async function getSession(sessionId: string) {
return client().beta.agents.sessions.retrieve(sessionId, {
timeout: 30_000,
});
}
export function sandboxConnection(session: AgentSession) {
if (session.environment.type !== "self_hosted") return null;
return {
environmentId: session.environment.id,
remoteUrl: session.environment.remote_url,
};
}

Then create app/api/queues/provision/route.ts and inspect the current state:

import { NotFoundError } from "openai";
let session;
try {
session = await getSession(sessionId);
} catch (error) {
if (error instanceof NotFoundError) return;
throw error;
}
if (!isSessionForThisDemo(session)) return;
if (session.status === "failed") {
await deleteSandbox(sessionId);
return;
}
const connection = sandboxConnection(session);
if (!connection) return;

The consumer uses the current environment ID and remote URL rather than values copied from the webhook. This read-before-write pattern makes each Queue delivery a stateless reconciliation.

Copy link to headingCreate or reconnect the Sandbox

Create lib/sandbox.ts. Use the OpenAI session ID as the deterministic Sandbox name so retries resolve to one microVM:

const sandbox = await Sandbox.getOrCreate({
name: `agents-${sessionId}`,
image: "vercel/sandbox/node:24",
persistent: true,
timeout: SANDBOX_TIMEOUT_MS,
networkPolicy: {
allow: [
"api.openai.com",
"codex-cloud-environments.chatgpt.com",
"registry.npmjs.org",
],
},
});

This uses the Vercel-managed Node.js 24 image. persistent: true preserves the session's /workspace filesystem if the Sandbox stops and later resumes.

Allow these outbound hosts:

const requiredHosts = [
"api.openai.com",
"codex-cloud-environments.chatgpt.com",
"registry.npmjs.org",
];

Each host serves one purpose:

HostWhy it is needed
api.openai.comRegisters the executor with the Agents API
codex-cloud-environments.chatgpt.comCarries file and shell operations over an outbound WebSocket
registry.npmjs.orgInstalls the Codex CLI

The npm registry is only needed during CLI installation. For production, bake the CLI into a custom image and remove the registry from the allowlist.

Install the executor once under an OS lock:

const setup = await sandbox.runCommand({
cmd: "flock",
args: [
"-w",
"120",
"/tmp/codex-setup.lock",
"sh",
"-c",
[
"mkdir -p /workspace",
"chown ubuntu:ubuntu /workspace",
"(codex --version >/dev/null 2>&1 && codex exec-server --help >/dev/null 2>&1 || npm install -g @openai/
codex@alpha)",
"codex --version",
"codex exec-server --help >/dev/null",
].join(" && "),
],
sudo: true,
});
if (setup.exitCode !== 0) {
throw new Error("Codex executor setup failed");
}

The setup creates /workspace with ownership that lets the executor read and write files without elevated permissions. The lock prevents concurrent Queue consumers from changing the directory or installing the CLI twice.

Copy link to headingConnect the executor to OpenAI

Start codex exec-server with the environment ID and remote URL from the current OpenAI session:

await sandbox.runCommand({
cmd: "flock",
args: [
"-n",
"/tmp/codex-executor.lock",
"codex",
"exec-server",
"--remote",
remoteUrl,
"--environment-id",
environmentId,
],
cwd: "/workspace",
detached: true,
env: { CODEX_API_KEY: config.executorApiKey },
});

Startup uses a second non-blocking flock. This prevents concurrent deliveries from starting competing executor processes in the same Sandbox.

The executor registers over api.openai.com, then opens an outbound WebSocket to codex-cloud-environments.chatgpt.com. OpenAI sends file and shell operations over that connection and the executor returns their results. The Sandbox does not need an inbound port.

Copy link to headingConfigure the Queue consumer

Wrap the reconciliation function with the Queue callback handler:

import { QueueClient } from "@vercel/queue";
import { NotFoundError } from "openai";
import { getSession, isSessionForThisDemo, sandboxConnection } from "@/lib/openai";
import { connectSandbox, deleteSandbox } from "@/lib/sandbox";
const queue = new QueueClient({ region: "iad1" });
export const maxDuration = 180;
export const POST = queue.handleCallback<{ sessionId: string }>(
async ({ sessionId }) => {
let session;
try {
session = await getSession(sessionId);
} catch (error) {
if (error instanceof NotFoundError) return;
throw error;
}
if (!isSessionForThisDemo(session)) return;
if (session.status === "failed") {
await deleteSandbox(sessionId);
return;
}
const connection = sandboxConnection(session);
if (!connection) return;
const { sandbox } = await connectSandbox(
sessionId,
connection.environmentId,
connection.remoteUrl,
);
console.log(
JSON.stringify({
session_id: sessionId,
sandbox_name: sandbox.name,
action: "connected",
}),
);
},
{ visibilityTimeoutSeconds: 240 },
);

Map the sandbox-wakeup topic to the Function in vercel.json:

{
"functions": {
"app/api/queues/provision/route.ts": {
"experimentalTriggers": [
{ "type": "queue/v2beta", "topic": "sandbox-wakeup" }
]
}
}
}

handleCallback() processes an incoming Queue delivery, but it does not register the route as a consumer. The queue/v2beta trigger creates that deploy-time subscription and makes the Function private. Without it, messages can enter the Queue but this Function will not receive them.

The Function duration remains beside the consumer implementation rather than in vercel.json. The producer uses iad1, and the callback derives the delivery region from the Queue request. You do not need to pin every Function in the project with a top-level regions setting.

Copy link to headingSend input and stream one turn

Create app/api/sessions/[id]/input/route.ts. The SDK's session stream helper subscribes before sending input and follows the resulting turn until it ends:

export function runSession(
sessionId: string,
input: string,
idempotencyKey: string,
) {
return client().beta.agents.sessions.stream(
sessionId,
{ input, idempotencyKey },
{ timeout: 15 * 60_000 },
);
}
import { randomUUID } from "node:crypto";
import { runSession } from "@/lib/openai";
import { createBrowserEventStream } from "@/lib/sse";
const events = runSession(id, input.trim(), randomUUID());
return new Response(createBrowserEventStream(events), {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});

The helper prevents a short turn from finishing before the app starts listening and stops after the selected turn ends and the reusable OpenAI session becomes idle. createBrowserEventStream() serializes the SDK events for the browser. The sample implementation contains the complete parser and termination logic.

The Sandbox does not stream output directly to the browser. It returns command results to OpenAI, OpenAI continues the agent loop, and the OpenAI session emits the output events consumed here.

Copy link to headingAdd the demo interface

The remaining presentation layer is a client component that:

  1. Accepts a task.
  2. Calls POST /api/sessions once.
  3. Calls POST /api/sessions/:id/input for the first task and follow-ups.
  4. Records each returned SSE block in the session event list, but appends only

    agent.session.turn.output_text.delta events to the assistant message so completed content is not rendered a second time.

  5. Groups text deltas by content part and preserves paragraph boundaries between

    parts.

  6. Renders assistant output as streaming Markdown with streamdown.
  7. Calls DELETE /api/sessions/:id to remove both the session and Sandbox.

Copy components/demo.tsx and app/page.tsx from the companion app. These files contain only the interface; all credentials and infrastructure operations remain in server routes.

Copy link to headingRun locally

Pull the project environment variables before starting the app:

vercel env pull .env.local

This also writes a short-lived OIDC token to .env.local. The token gives the Sandbox SDK your project's identity during local development. Pull the environment again when that token expires.

Run the app to verify the interface:

pnpm dev

Session creation and streaming can run locally. Use a deployed webhook for the complete managed Queue delivery and Sandbox lifecycle.

CommandWhat it does
pnpm devStarts the local development server
pnpm testRuns the test suite
pnpm buildVerifies the production build
pnpm probe:sandboxCreates a Sandbox, installs the Codex CLI, prints its version, and stops it

Copy link to headingDeploy the app

Add the variables available before webhook registration, then deploy:

vercel env add OPENAI_API_KEY
vercel env add OPENAI_AGENT_ID
vercel env add OPENAI_EXECUTOR_API_KEY
vercel env add APP_PASSWORD
vercel deploy --prod

Vercel supplies Sandbox and Queue authentication through OIDC. Do not configure a separate Vercel access token in the deployed app.

Copy link to headingRegister the OpenAI webhook

Open the webhook settings for the same OpenAI project used to create the agent. The application key must include the api.webhooks.write permission. Register https://YOUR_PROJECT.vercel.app/api/webhook and subscribe to:

  • agent.session.action_required
  • agent.session.failed

Copy the resulting signing secret into the Vercel project and redeploy:

vercel env add OPENAI_WEBHOOK_SECRET
vercel deploy --prod

Until the real signing secret is configured, the sample endpoint responds with 503. OpenAI must be able to reach the webhook without a Vercel login. The OpenAI signature, rather than a bearer token in the URL, authenticates each delivery.

If Vercel Deployment Protection is enabled, create an automation bypass secret and register the webhook URL with ?x-vercel-protection-bypass=YOUR_BYPASS_SECRET. This lets OpenAI reach only that URL without disabling protection for the deployment.

Copy link to headingTest the complete flow

Verify the project and managed image first:

pnpm test
pnpm build
pnpm probe:sandbox

The probe creates a real Sandbox, installs the Codex CLI, prints its version, and stops it.

Open the production app and ask the agent to create a file in /workspace, run it, and report the result. You should see:

  1. An environment-pending event while OpenAI waits for the executor.
  2. An environment-connected event after the Queue consumer starts the Sandbox.
  3. Turn and output events as the agent works.
  4. An idle event when the turn completes.

Send a follow-up in the same session and ask the agent to read the file. This confirms that the session still owns the same persistent workspace. Use Delete session to remove the OpenAI session and its Sandbox.

Copy link to headingWhy use a Queue instead of Workflow?

The Queue does not run the agent or store user prompts. It only makes delivery of the infrastructure wake-up signal durable.

Sandbox provisioning can take longer than a webhook should stay open, and either OpenAI or Vercel can retry a request. A Queue absorbs those bursts, retries transient failures, and invokes a private consumer.

Four safeguards make at-least-once delivery safe:

  1. The OpenAI webhook event ID deduplicates Queue messages.
  2. The consumer reads the latest OpenAI session before acting.
  3. The session ID deterministically identifies one Sandbox.
  4. OS locks allow only one setup and executor process inside that Sandbox.

Vercel Workflow is useful when your app owns a long-running sequence of steps, waits, or human approvals. Here, OpenAI already owns the long-running session state. Adding a Workflow would create a second lifecycle without removing the need to reconcile OpenAI's state.

Copy link to headingHow this differs from the OpenAI Agents SDK integration

The existing guide to building an agent with the OpenAI Agents SDK and Vercel Sandbox runs the agent loop in your application. Your code calls the model, decides when to use tools, and manages the loop.

In this integration, OpenAI hosts that loop. Your application creates sessions, sends user input, and displays events. Its infrastructure responsibility is narrower: connect an isolated Sandbox whenever OpenAI says the session needs an executor.

Agents SDK guideThis guide
Agent loopYour applicationOpenAI Agents API
Session stateYour applicationOpenAI
Command executionVercel SandboxVercel Sandbox
Sandbox lifecycleYour applicationSigned webhook and Queue consumer

Copy link to headingCustomize the sample app

To changeEditNotes
Agent model or behaviorThe saved OpenAI agentRun pnpm create:agent after changing scripts/create-agent.ts, then update OPENAI_AGENT_ID
Authenticationlib/auth.ts and APP_PASSWORDReplace the shared password with your identity provider and authorization policy
Sandbox imageimage in lib/sandbox.tsBake the Codex CLI into a custom image and remove registry.npmjs.org from the network policy
Sandbox lifetimeSANDBOX_TIMEOUT_MS in lib/config.tsSet it above the longest expected turn
Queue regionThe webhook producer and consumer QueueClientChoose the region where your Functions run to reduce delivery latency

Copy link to headingProduction considerations

The sample cleans up when the app deletes a session or OpenAI reports a failed session. OpenAI does not send a deletion webhook, so deletion performed elsewhere does not automatically remove the Sandbox. Production apps should periodically compare retained Sandboxes with their session records and remove orphaned resources and snapshots.

For observability, record the webhook event ID, session ID, environment ID, Sandbox name, provisioning duration, and cleanup reason. Never log OpenAI API keys or webhook signatures.

Set the Sandbox timeout above the longest expected turn. Persistent Sandboxes preserve files across normal stops, but processes such as codex exec-server must be restarted when a Sandbox resumes. Rebuild custom images regularly to keep the Codex CLI and system packages current.

Copy link to headingTroubleshooting

Each item below lists a symptom, its cause, and the fix.

Copy link to headingThe webhook responds with 503

Symptom: OpenAI reports failed webhook deliveries, and requests to /api/webhook return 503.

Cause: OPENAI_WEBHOOK_SECRET isn't set, so the route can't verify signatures and refuses every delivery.

Fix: Register the webhook in the OpenAI project, add the resulting signing secret with vercel env add OPENAI_WEBHOOK_SECRET, and redeploy.

Copy link to headingSessions stall on environment-pending

Symptom: The app shows an environment-pending event, but no environment-connected event follows.

Cause: The webhook isn't reaching the app, or messages enter the Queue with no consumer. Deployment Protection can block OpenAI's deliveries, and a missing queue/v2beta trigger in vercel.json leaves the provision Function unsubscribed.

Fix: Confirm the webhook is registered against the production URL, append the protection bypass secret if Deployment Protection is enabled, and check that vercel.json maps the sandbox-wakeup topic to app/api/queues/provision/route.ts.

Copy link to headingThe executor never connects

Symptom: The Sandbox starts, but the pending environment_connection action never clears.

Cause: The Sandbox network policy blocks a host the executor needs, or the Codex CLI install failed.

Fix: Allow api.openai.com, codex-cloud-environments.chatgpt.com, and registry.npmjs.org in the network policy, then run pnpm probe:sandbox to confirm the image can install and run the CLI.

Copy link to headingLocal sessions never get an executor

Symptom: Streaming works with pnpm dev, but locally created sessions wait on the environment forever.

Cause: OpenAI delivers webhooks to the registered production URL, not to your local server, so the local app never receives the wake-up signal.

Fix: This is expected. Test the full lifecycle against the deployed app, and use local development for the session and streaming routes.

Copy link to headingNext steps

Related documentation

More Vercel Sandbox guides