---
title: Build an agent with OpenAI Agents API on Vercel
description: Build and deploy an agent with OpenAI Agents API, Vercel Functions, Queues, and Sandbox for isolated code execution.
url: /kb/guide/openai-agents-api-vercel
canonical_url: "https://vercel.com/kb/guide/openai-agents-api-vercel"
published: 2026-09-10
last_updated: 2026-09-10
authors: Allen Zhou
related:
  - /docs/queues
  - /docs/sandbox
  - /docs/cli
  - /docs/sandbox/concepts/runtimes
  - /docs/workflow
  - /kb/guide/building-an-agent-with-openai-agents-sdk-and-vercel-sandbox
  - /docs/workflows
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

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](https://vercel.com/docs/queues) handles lifecycle events, while [Vercel Sandbox](https://vercel.com/docs/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.

## What 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.

## Prerequisites

Before you begin, you need:

- An OpenAI project with access to the Agents API
  
- A Vercel account with [Sandbox](https://vercel.com/docs/sandbox) and [Queues](https://vercel.com/docs/queues) access
  
- Node.js 24 or later
  
- The [Vercel CLI](https://vercel.com/docs/cli)
  
- Permission to create an application key and an environment key in the same OpenAI organization and project
  

## Create the OpenAI credentials

Open the [OpenAI API keys page](https://platform.openai.com/api-keys) 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 key                                                  | Environment key                                          |
| -------------- | ---------------------------------------------------------------- | -------------------------------------------------------- |
| Permissions    | `api.agents.read`, `api.agents.write`, and `api.responses.write` | Created from the **Agents** tab for executor connections |
| Where it lives | Vercel Functions environment variables                           | Passed into the Sandbox as `CODEX_API_KEY`               |
| What it does   | Creates agents and manages sessions                              | Connects `codex exec-server` to the session              |

## Set up the project

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

```bash
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:

```bash
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:

```bash
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`:

```bash
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:

| Variable                  | Where it comes from                                                  | What it does                                                |
| ------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------- |
| `OPENAI_API_KEY`          | The application key created in the OpenAI project                    | Creates agents and manages sessions from Vercel Functions   |
| `OPENAI_AGENT_ID`         | The `agent_...` value returned by the agent creation request         | Selects the reusable agent configuration for each session   |
| `OPENAI_EXECUTOR_API_KEY` | The environment key created from the OpenAI project's **Agents** tab | Lets `codex exec-server` connect the Sandbox to the session |
| `OPENAI_WEBHOOK_SECRET`   | The signing secret returned after webhook registration               | Verifies that lifecycle requests came from OpenAI           |
| `APP_PASSWORD`            | A strong value you choose                                            | Protects 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.

## Review the project structure

The walkthrough follows these files in dependency order:

```text
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.

## Create the OpenAI session client

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

```typescript
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`:

```typescript
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.

```typescript
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.

## Receive 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:

```typescript
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:

```typescript
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.

## Reconcile 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`:

```typescript
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:

```typescript
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.

## Create or reconnect the Sandbox

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

```typescript
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](https://vercel.com/docs/sandbox/concepts/runtimes). `persistent: true` preserves the session's `/workspace` filesystem if the Sandbox stops and later resumes.

Allow these outbound hosts:

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

Each host serves one purpose:

| Host                                   | Why it is needed                                             |
| -------------------------------------- | ------------------------------------------------------------ |
| `api.openai.com`                       | Registers the executor with the Agents API                   |
| `codex-cloud-environments.chatgpt.com` | Carries file and shell operations over an outbound WebSocket |
| `registry.npmjs.org`                   | Installs 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:

```typescript
await sandbox.runCommand({
  cmd: "flock",
  args: [
    "-w",
    "120",
    "/tmp/codex-setup.lock",
    "sh",
    "-c",
    "mkdir -p /workspace && chown ubuntu:ubuntu /workspace && (command -v codex || npm install -g @openai/codex@alpha)",
  ],
  sudo: true,
});
```

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.

## Connect the executor to OpenAI

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

```typescript
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.

## Configure the Queue consumer

Wrap the reconciliation function with the Queue callback handler:

```typescript
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`:

```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.

## Send 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:

```typescript
export function runSession(
  sessionId: string,
  input: string,
  idempotencyKey: string,
) {
  return client().beta.agents.sessions.stream(
    sessionId,
    { input, idempotencyKey },
    { timeout: 15 * 60_000 },
  );
}
```
```typescript
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](https://github.com/vercel-labs/openai-agents-api-vercel/blob/main/lib/sse.ts) 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.

## Add 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](https://github.com/vercel-labs/openai-agents-api-vercel/blob/main/components/demo.tsx) and [app/page.tsx](https://github.com/vercel-labs/openai-agents-api-vercel/blob/main/app/page.tsx) from the companion app. These files contain only the interface; all credentials and infrastructure operations remain in server routes.

## Run locally

Pull the project environment variables before starting the app:

```bash
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:

```bash
pnpm dev
```

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

| Command              | What it does                                                                |
| -------------------- | --------------------------------------------------------------------------- |
| `pnpm dev`           | Starts the local development server                                         |
| `pnpm test`          | Runs the test suite                                                         |
| `pnpm build`         | Verifies the production build                                               |
| `pnpm probe:sandbox` | Creates a Sandbox, installs the Codex CLI, prints its version, and stops it |

## Deploy the app

Add the variables available before webhook registration, then deploy:

```bash
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.

## Register the OpenAI webhook

Open the webhook settings for the same OpenAI project used to create the agent. 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:

```bash
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.

## Test the complete flow

Verify the project and managed image first:

```bash
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.

## Why 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](https://vercel.com/docs/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.

## How this differs from the OpenAI Agents SDK integration

The existing guide to [building an agent with the OpenAI Agents SDK and Vercel Sandbox](https://vercel.com/kb/guide/building-an-agent-with-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 guide | This guide                        |
| ----------------- | ---------------- | --------------------------------- |
| Agent loop        | Your application | OpenAI Agents API                 |
| Session state     | Your application | OpenAI                            |
| Command execution | Vercel Sandbox   | Vercel Sandbox                    |
| Sandbox lifecycle | Your application | Signed webhook and Queue consumer |

## Customize the sample app

| To change               | Edit                                            | Notes                                                                                           |
| ----------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Agent model or behavior | The saved OpenAI agent                          | Run `pnpm create:agent` after changing `scripts/create-agent.ts`, then update `OPENAI_AGENT_ID` |
| Authentication          | `lib/auth.ts` and `APP_PASSWORD`                | Replace the shared password with your identity provider and authorization policy                |
| Sandbox image           | `image` in `lib/sandbox.ts`                     | Bake the Codex CLI into a custom image and remove `registry.npmjs.org` from the network policy  |
| Sandbox lifetime        | `SANDBOX_TIMEOUT_MS` in `lib/config.ts`         | Set it above the longest expected turn                                                          |
| Queue region            | The webhook producer and consumer `QueueClient` | Choose the region where your Functions run to reduce delivery latency                           |

## Production 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.

## Troubleshooting

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

### The 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.

### Sessions 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`.

### The 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.

### Local 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.

## Next steps

- Explore the [source code](https://github.com/vercel-labs/openai-agents-api-vercel) for the complete implementation.
  
- Learn how [Vercel Sandbox](https://vercel.com/docs/sandbox) isolates untrusted code, and which [runtimes and images](https://vercel.com/docs/sandbox/concepts/runtimes) are available.
  
- Read the [Vercel Queues documentation](https://vercel.com/docs/queues) for topics, triggers, and more.
  
- Compare with [Vercel Workflows](https://vercel.com/docs/workflows) for flows your application owns end-to-end.
  
- See the companion guide to [building an agent with the OpenAI Agents SDK and Vercel Sandbox](https://vercel.com/kb/guide/building-an-agent-with-openai-agents-sdk-and-vercel-sandbox) for the application-hosted agent loop.