How to Use the OpenAI API: A Developer Walkthrough
Using the OpenAI API comes down to four steps: get an API key, send an HTTP request with your prompt, handle the response (or stream it), and manage errors and costs as you scale. The rest of this guide walks through each step with working code, plus the details that trip people up the first time — authentication headers, rate limits, streaming, and how to keep your bill predictable.
If you already have a key and just want to make your first call, skip to the "Making Your First Request" section below. If you're building something that needs to run in production — with usage tracking, team access, or a simpler billing model — the last section covers an alternative worth knowing about.
Setting Up Access
Before writing any code, you need three things:
- An API key from your provider's dashboard
- A way to store it securely (never hardcode it)
- An HTTP client — curl,
fetch, or an official SDK
Store your key as an environment variable:
export OPENAI_API_KEY="sk-..."
Every request needs this key in an Authorization header. If you're using a framework like Next.js or Express, load it via .env files and a package like dotenv — never commit it to git, and never expose it in client-side JavaScript. The API key should only ever be used from a server.
Making Your First Request
The core of the OpenAI API is the chat completions endpoint. Here's a minimal request with curl:
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [
{"role": "user", "content": "Explain what a REST API is in two sentences."}
]
}'
The response is a JSON object containing a choices array. The generated text lives at choices[0].message.content. In JavaScript:
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [
{ role: "user", content: "Explain what a REST API is in two sentences." }
]
})
});
const data = await response.json();
console.log(data.choices[0].message.content);
The messages array is how you build conversation context. Each message has a role — system, user, or assistant — and content. A system message sets behavior ("You are a concise technical writer"), and you append new user/assistant pairs as the conversation continues. The API is stateless: you must send the full conversation history with every request, since the model has no memory between calls.
Streaming Responses
For anything user-facing, streaming matters. Without it, users stare at a blank screen until the entire response finishes generating. With streaming, tokens appear as they're produced, which feels dramatically faster.
Enable it by setting stream: true and reading the response as server-sent events:
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Write a haiku about databases." }],
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value));
}
Each chunk arrives as a data: {...} line you need to parse and concatenate. Most SDKs handle this parsing for you, but understanding the raw format helps when debugging.
Using Tools and Function Calling
Modern models can call functions you define — useful for looking up data, running calculations, or triggering actions in your app. You describe the function in a tools array, and the model responds with a structured call instead of plain text when it decides a function is needed. Your code executes the function and sends the result back as a new message, letting the model use that result in its final answer. This pattern is the backbone of most agent-style applications.
Handling Errors and Rate Limits
Production code needs to handle a few predictable failure modes:
- 429 Too Many Requests — you've hit a rate limit; back off and retry with exponential delay
- 401 Unauthorized — the API key is missing, wrong, or revoked
- 400 Bad Request — usually a malformed payload (wrong model name, invalid JSON)
- Timeouts — long generations can exceed default client timeouts; increase them or use streaming
A simple retry wrapper with exponential backoff covers most transient failures:
async function callWithRetry(fn, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 500 * 2 ** i));
}
}
}
Managing Cost as You Scale
Every request costs money based on input and output tokens, and costs add up fast once you're beyond a prototype. A few practical habits:
- Use a smaller, cheaper model for simple tasks and reserve larger models for tasks that need them
- Cap
max_tokensso runaway generations don't inflate your bill - Log token usage per request so you can spot expensive call patterns early
- Cache repeated prompts where the output doesn't need to be regenerated
If you're already paying for a Claude subscription and want API-style access without juggling a second provider's billing and key management, SubToAPI turns that subscription into a standard HTTPS API — application keys, streaming, tool use, and usage metadata in one dashboard. It's not a drop-in replacement for the OpenAI API, but if your stack already uses Claude, it's worth a look. Get started at /signup, see /pricing, or check the /docs/quickstart to see the request format.
Questions
Do I need a paid plan to use the API? Yes. API access is billed separately from any consumer subscription and typically requires adding a payment method before you can send requests, even at low volume.
Can I call the API directly from a browser app? No — doing so exposes your API key to anyone who inspects network traffic. Always proxy requests through your own backend server.
What's the difference between the chat and streaming endpoints? There's no separate endpoint — streaming uses the same chat completions endpoint with stream: true set in the request body, returning tokens incrementally instead of one final JSON response.