openai
The official TypeScript and JavaScript client for the OpenAI REST API, generated from OpenAI's published OpenAPI spec. It covers the Responses API (now the primary interface), Chat Completions, files, fine-tuning, webhooks, and the Realtime API over WebSocket, with typed requests and responses, automatic retries with backoff, server-sent-event streaming, auto-pagination, and dedicated support for Azure OpenAI and Amazon Bedrock's OpenAI-compatible endpoint.
If you call the OpenAI API from JavaScript, use this client; it is official, typed, and updated in step with the API. Add a provider-abstraction layer on top only when you genuinely expect to swap models.
Use it if
- You call OpenAI models from Node.js 22+, Deno, Bun, Cloudflare Workers, or the Vercel Edge Runtime
- You want typed request and response objects, built-in retries, and SSE streaming without writing fetch plumbing yourself
- You need Azure OpenAI or Bedrock's OpenAI-compatible API through the same client with the same call shapes
- You receive OpenAI webhooks and want signature verification handled by client.webhooks.unwrap
- You are building a browser-only app: the client refuses to run in browsers unless you set dangerouslyAllowBrowser, and doing that ships your API key to every visitor
- You want one abstraction over several model providers; a layer like the Vercel AI SDK swaps providers without rewriting call sites
- You are on React Native, which the SDK explicitly does not support
- You are pinned to Node 20 or older; the SDK requires Node 22+ and Node 20 reached end of life in April 2026
Setup reality
npm install openai plus an OPENAI_API_KEY env var and you are making calls in minutes; the client reads the key by default. The friction is elsewhere: OpenAI moved its primary interface from Chat Completions to the Responses API, so examples online mix two styles, and multi-turn Responses conversations fail confusingly if you filter reasoning or tool-call items out of the history you send back. SemVer is also loose here: type-level and low-impact breaking changes are allowed in minor releases, so pin and read changelogs.
Patterns
Generate text with the Responses APIgenerate-text
import OpenAI from 'openai';
const client = new OpenAI(); // reads OPENAI_API_KEY
const response = await client.responses.create({
model: 'gpt-5.5',
instructions: 'You are a concise assistant',
input: 'Are semicolons optional in JavaScript?',
});
console.log(response.output_text);Responses is the primary API now; output_text is the convenience accessor for the generated text.
Chat Completions (legacy but supported)chat-completion
const completion = await client.chat.completions.create({
model: 'gpt-5.5',
messages: [
{ role: 'developer', content: 'Talk like a pirate.' },
{ role: 'user', content: 'Are semicolons optional in JavaScript?' },
],
});
console.log(completion.choices[0].message.content);Chat Completions is supported indefinitely; note the developer role where older examples show system.
Stream a response with SSEstream-response
const stream = await client.responses.create({
model: 'gpt-5.5',
input: 'Write a haiku about build tools',
stream: true,
});
for await (const event of stream) {
if (event.type === 'response.output_text.delta') {
process.stdout.write(event.delta);
}
}You iterate typed events, not raw text chunks; filter on event.type to get just the text deltas.
Continue a conversation across turnsmulti-turn-conversation
const first = await client.responses.create({
model: 'gpt-5.5',
input: 'My name is Sam.',
});
const second = await client.responses.create({
model: 'gpt-5.5',
previous_response_id: first.id,
input: 'What is my name?',
});If you manage history manually instead, keep all output items in order; filtering to just messages drops reasoning and tool-call items and breaks the next request. The toResponseInputItems() helper normalizes them.
Ask about an imagevision-image-input
const response = await client.responses.create({
model: 'gpt-5.5',
input: [
{
role: 'user',
content: [
{ type: 'input_text', text: 'What is in this image?' },
{ type: 'input_image', image_url: 'https://example.com/photo.jpg' },
],
},
],
});Content is an array mixing input_text and input_image blocks; a bare string only works for text-only input.
Upload a fileupload-file
import fs from 'fs';
import OpenAI, { toFile } from 'openai';
const client = new OpenAI();
await client.files.create({
file: fs.createReadStream('input.jsonl'),
purpose: 'fine-tune',
});
// from raw bytes
await client.files.create({
file: await toFile(Buffer.from('my bytes'), 'input.jsonl'),
purpose: 'fine-tune',
});toFile() buffers whole streams in memory; use toStreamingFile() for large streams you want sent without buffering.
Handle API errors by typehandle-errors
import OpenAI from 'openai';
try {
await client.responses.create({ model: 'gpt-5.5', input: 'hi' });
} catch (err) {
if (err instanceof OpenAI.APIError) {
console.log(err.status); // e.g. 429
console.log(err.name); // e.g. RateLimitError
console.log(err.request_id); // for OpenAI support
} else {
throw err;
}
}Connection errors, 408, 409, 429, and 5xx are already retried twice by default before you ever see them.
Configure automatic retriesconfigure-retries
// default for all requests
const client = new OpenAI({ maxRetries: 0 }); // default is 2
// or per request
await client.responses.create(
{ model: 'gpt-5.5', input: 'hello' },
{ maxRetries: 5 },
);Retries use short exponential backoff; set maxRetries: 0 when your caller already has its own retry loop.
Set request timeoutsconfigure-timeout
const client = new OpenAI({
timeout: 20 * 1000, // 20 seconds, default is 10 minutes
});
await client.responses.create(
{ model: 'gpt-5.5', input: 'hello' },
{ timeout: 5 * 1000 },
);The default is a generous 10 minutes, and requests that time out are still retried twice by default.
Iterate a paginated listauto-pagination
const jobs = [];
for await (const job of client.fineTuning.jobs.list({ limit: 20 })) {
jobs.push(job); // fetches more pages automatically
}
// or page by page
let page = await client.fineTuning.jobs.list({ limit: 20 });
while (page.hasNextPage()) {
page = await page.getNextPage();
}for await crosses page boundaries silently; on huge lists that means many hidden API calls.
Verify and parse a webhookverify-webhook
const client = new OpenAI({
webhookSecret: process.env.OPENAI_WEBHOOK_SECRET,
});
export async function webhook(request: Request) {
const body = await request.text();
try {
const event = client.webhooks.unwrap(body, request.headers);
if (event.type === 'response.completed') {
console.log('done:', event.data);
}
return Response.json({ message: 'ok' });
} catch {
return new Response('Invalid signature', { status: 400 });
}
}Pass the raw body string to unwrap(); parsing the JSON first breaks signature verification.
Use Azure OpenAIazure-openai
import { AzureOpenAI } from 'openai';
import { getBearerTokenProvider, DefaultAzureCredential } from '@azure/identity';
const credential = new DefaultAzureCredential();
const azureADTokenProvider = getBearerTokenProvider(
credential,
'https://cognitiveservices.azure.com/.default',
);
const openai = new AzureOpenAI({
azureADTokenProvider,
apiVersion: '2024-10-01-preview',
});The Azure API shape differs slightly from the core API, so the static types will not always match what Azure returns.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @anthropic-ai/sdk | npm | The equivalent official client if you are on Claude models |
| ai | npm | Vercel AI SDK when you want provider-agnostic calls and UI streaming hooks |
| @google/genai | npm | The official SDK if you are on Gemini models |