The Responses API is now the main way to call OpenAI models from Python. It handles text, images, files, tools and multi-turn chats through one endpoint.
This tutorial walks you through it step by step with the current GPT-6 models. You will go from an API key to streaming, structured outputs, function calling, caching, batches and error handling.
- Install:
pip install openai, setOPENAI_API_KEY, then callclient.responses.create(). - Models:
gpt-6-astra(flagship),gpt-6.1-sol(balanced),gpt-6.1-terra(new on October 26, 2026) andgpt-6-luna(cheapest). - Structured outputs: pass a Pydantic class to
client.responses.parse()and readoutput_parsed. - Built-in tools:
web_searchandfile_searchrun on OpenAI’s side. - Save money: caching is automatic from 1,024 tokens, and the Batch API costs 50% less.
- Errors: catch
RateLimitErrorand retry with backoff. A 429 can also mean your credits ran out.
Step 1: Get an API key
The API is billed separately from ChatGPT. A ChatGPT Plus or Pro plan does not include API usage by default, so you need credits on the platform.
- Sign in to the platform. Go to
platform.openai.comand log in or create an account. - Add billing. Open the billing settings and buy prepaid credits. Without credits, calls fail with a 429 quota error.
- Create a key. Open API keys, click Create new secret key, and pick the project it belongs to.
- Copy it once. The full key is shown only at creation, so store it in a password manager.
- Set an environment variable. Never paste the key into your code or a Git repo.
# macOS / Linux
export OPENAI_API_KEY="sk-proj-your-key"
# Windows PowerShell
$env:OPENAI_API_KEY="sk-proj-your-key"
Note: Since October 28, 2026, eligible ChatGPT Pro and Business subscribers can claim up to $500 per month in API credits. Check your platform billing page to see if you qualify.
Step 2: Install the openai SDK
Use a virtual environment so the SDK stays separate from other projects. Then install the official package.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install openai pydantic
The client reads OPENAI_API_KEY from your environment automatically. You can create it with no arguments:
from openai import OpenAI
client = OpenAI()
Which model to use
OpenAI’s model page lists four main GPT-6 models. They all have a 1.05M token context window and up to 128K output tokens.
| Model ID | Best for | Price per 1M tokens (input / output) | Reasoning levels |
|---|---|---|---|
gpt-6-astra |
Hard reasoning and coding; OpenAI’s suggested starting point | $10 / $50 | low to max |
gpt-6.1-sol |
Balance of quality and cost | $2 / $10 | low to max |
gpt-6.1-terra |
Cheaper mid-tier work, visual reasoning | $1 / $5 | none to max |
gpt-6-luna |
High-volume, cost-sensitive tasks | $0.10 / $0.50 | none to max |
GPT-5.x models are still on the API for older code, but new projects should start on GPT-6. For every rate, including cached input and tools, see my OpenAI API pricing guide.
Tip: Build and test with gpt-6-luna first. It is 100 times cheaper than Astra on input, so bugs in your loop cost almost nothing.
Your first text request
A Responses call needs only a model and an input. The output_text property joins all the text the model returned.
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-6-luna",
instructions="You are a friendly Python tutor. Keep answers short.",
input="Explain what a Python list comprehension is.",
)
print(response.output_text)
print(response.usage)
Here is what each part does:
- instructions: the system-level guidance for this call.
- input: a string, or a list of messages and content parts.
- usage: input, output and cached token counts, so you can track cost.
To control how hard the model thinks, add a reasoning setting. Lower effort is faster and cheaper:
response = client.responses.create(
model="gpt-6.1-sol",
reasoning={"effort": "low"},
input="Give me three names for a coffee shop.",
)
Multi-turn conversations
The Responses API can keep the chat history for you. Pass the previous response’s ID and only send the new message.
first = client.responses.create(
model="gpt-6-luna",
input="My name is Priya and I love hiking.",
)
second = client.responses.create(
model="gpt-6-luna",
previous_response_id=first.id,
input="What hobby did I mention?",
)
print(second.output_text)
You can also manage history yourself by sending a list of messages each time. That gives you full control, which helps when you trim old turns.
Streaming responses
Streaming shows text as it is generated, which makes chat apps feel fast. Set stream=True and loop over the events.
stream = client.responses.create(
model="gpt-6-luna",
input="Write a short poem about the sea.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
print()
print(event.response.usage)
Each event has a type. You mostly care about the text deltas and the final completed event, which carries usage data.
Structured outputs with Pydantic
Structured outputs force the model to return JSON that matches your schema. This is the cleanest way to extract data.
from pydantic import BaseModel
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
response = client.responses.parse(
model="gpt-6-luna",
input=[
{"role": "system", "content": "Extract the event information."},
{"role": "user", "content": "Alice and Bob are going to a science fair on Friday."},
],
text_format=CalendarEvent,
)
event = response.output_parsed
print(event.name, event.date, event.participants)
Without Pydantic, pass a raw JSON schema in text={"format": {"type": "json_schema", ...}} with "strict": True. Safety refusals are detectable in code, so check that a parsed result exists before you use it.
Function calling
Function calling lets the model ask your code to run a function. You describe the tool, the model returns a call with arguments, and you send back the result.
import json
tools = [{
"type": "function",
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
"strict": True,
}]
def get_weather(city):
return {"city": city, "temp_c": 21, "sky": "sunny"} # call a real API here
input_list = [{"role": "user", "content": "What's the weather in Lisbon?"}]
response = client.responses.create(model="gpt-6-luna", tools=tools, input=input_list)
input_list += response.output
for item in response.output:
if item.type == "function_call" and item.name == "get_weather":
args = json.loads(item.arguments)
input_list.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(get_weather(**args)),
})
final = client.responses.create(model="gpt-6-luna", tools=tools, input=input_list)
print(final.output_text)
The model can return zero, one or several calls in one turn, so always loop over response.output. The call_id links each result to its call.
Vision and file inputs
Send an image
Mix text and image parts in one user message. Use a public URL, or a base64 data URL for a local file.
import base64
with open("receipt.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
response = client.responses.create(
model="gpt-6-luna",
input=[{
"role": "user",
"content": [
{"type": "input_text", "text": "What is the total on this receipt?"},
{"type": "input_image", "image_url": f"data:image/png;base64,{b64}"},
],
}],
)
print(response.output_text)
Send a PDF
Upload the file once, then refer to it by ID. You can also pass a file_url for a public PDF.
uploaded = client.files.create(file=open("report.pdf", "rb"), purpose="user_data")
response = client.responses.create(
model="gpt-6-luna",
input=[{
"role": "user",
"content": [
{"type": "input_file", "file_id": uploaded.id},
{"type": "input_text", "text": "Summarize this report in five bullets."},
],
}],
)
Built-in tools: web search and file search
Built-in tools run on OpenAI’s servers, so you write no tool code. Just add them to tools.
Web search
response = client.responses.create(
model="gpt-6-luna",
tools=[{"type": "web_search"}],
input="What did OpenAI release in the API this week?",
)
print(response.output_text)
File search over your documents
File search needs a vector store. You create it, add files, then point the tool at it.
store = client.vector_stores.create(name="knowledge_base")
with open("handbook.pdf", "rb") as f:
file = client.files.create(file=f, purpose="assistants")
client.vector_stores.files.create_and_poll(vector_store_id=store.id, file_id=file.id)
response = client.responses.create(
model="gpt-6-luna",
input="How many vacation days do new staff get?",
tools=[{"type": "file_search", "vector_store_ids": [store.id]}],
)
print(response.output_text)
Built-in tool calls are billed on top of tokens. Check the tools section of the pricing page before heavy use.
Prompt caching
Caching is on by default. When the start of your prompt repeats, OpenAI reuses the processed prefix and charges far less for it.
- Minimum size: on GPT-5.6 and newer, the cacheable prefix needs 1,024 visible input tokens.
- Cached read price: 0.1x the input rate, or 0.05x on GPT-6.1 Sol and GPT-6.1 Terra.
- Write price: 1.25x input for the default 30-minute cache, or 2.5x for the 12-hour option added on October 20, 2026.
- Lifetime setting: use
prompt_cache_options={"ttl": "12h"}on GPT-5.6 and newer. - Per-user accounting: add an optional
prompt_cache_key, such as a customer ID.
response = client.responses.create(
model="gpt-6.1-sol",
instructions=LONG_SYSTEM_PROMPT, # keep the stable part first
input=user_question,
prompt_cache_key="customer-42",
)
details = response.usage.input_tokens_details
print("cached:", details.cached_tokens)
Put stable content such as instructions, tools and reference docs first, and the changing question last. My prompt caching guide compares OpenAI with Claude and Gemini.
The Batch API
If you do not need answers right away, the Batch API gives a 50% discount. Jobs finish within a 24-hour window.
- Write a JSONL file. Each line has a unique
custom_id,"method": "POST","url": "/v1/responses"and abody. - Upload it. Use
client.files.create()withpurpose="batch". - Create the batch. Pass the file ID, the endpoint and
completion_window="24h". - Poll the status. Call
client.batches.retrieve()until it iscompleted. - Download results. Read
client.files.content(batch.output_file_id).
import json
rows = [
{"custom_id": f"q{i}", "method": "POST", "url": "/v1/responses",
"body": {"model": "gpt-6-luna", "input": q}}
for i, q in enumerate(["Define API.", "Define SDK.", "Define JSON."])
]
with open("batch.jsonl", "w") as f:
for r in rows:
f.write(json.dumps(r) + "\n")
batch_file = client.files.create(file=open("batch.jsonl", "rb"), purpose="batch")
batch = client.batches.create(
input_file_id=batch_file.id,
endpoint="/v1/responses",
completion_window="24h",
)
print(batch.id, batch.status)
A batch can hold up to 50,000 requests and a 200 MB file, and each file can target only one model. Unfinished work after 24 hours is marked expired, and you pay only for completed requests.
Errors and retries
The SDK raises a typed exception for each kind of failure. Catch the ones you can recover from and fail loudly on the rest.
import time
import openai
def ask(prompt, attempts=5):
for i in range(attempts):
try:
return client.responses.create(model="gpt-6-luna", input=prompt)
except openai.RateLimitError as e:
if "quota" in str(e).lower() or "credit" in str(e).lower():
raise # retrying will not add credits
time.sleep(2 ** i)
except (openai.APIConnectionError, openai.APITimeoutError, openai.InternalServerError):
time.sleep(2 ** i)
raise RuntimeError("Gave up after retries")
The SDK also retries some errors on its own. You can raise that with OpenAI(max_retries=5) or set a timeout with OpenAI(timeout=60).
| Status | Python exception | Usual cause | Fix |
|---|---|---|---|
| 400 | BadRequestError |
Bad parameter or schema | Read the message and fix the request |
| 401 | AuthenticationError |
Wrong, revoked or mistyped key | Check the key and project, or make a new one |
| 403 | PermissionDeniedError |
Unsupported region or no access | Check supported countries and key permissions |
| 404 | NotFoundError |
Wrong model name or file ID | Check the ID spelling |
| 429 | RateLimitError |
Too fast, or no credits or spend limit hit | Back off, or add credits and raise limits |
| 500 / 503 | InternalServerError |
Server issue or overload | Retry with longer delays, check the status page |
Warning: A 429 with credit_balance_exhausted or a spend limit code will never fix itself with retries. Your loop will just burn time until you add credits or raise the limit.
Troubleshooting common problems
“The api_key client option must be set”
The SDK cannot find OPENAI_API_KEY. Set it in the same terminal that runs Python, or load it from a .env file with python-dotenv.
“Model not found” with a GPT-6 name
Check the exact ID, such as gpt-6.1-sol with the dot. Also check that your project has access to that model, and update the SDK with pip install -U openai.
Rate limits hit far too early
- Check your tier: on October 6, 2026, OpenAI cut usage tiers from five to three: Build, Launch and Grow. Your limits depend on your tier.
- Ramp slowly: OpenAI suggests raising traffic gradually, not in one jump.
- Shared usage: other apps in the same organization share the limit.
cached_tokens is always 0
Your prompt may be under 1,024 tokens, or the start of it changes each call. Move dates, user names and other variable text to the end.
Structured output fails to parse
Every field must be listed in required, and additionalProperties must be false in strict mode. With Pydantic, avoid unsupported types and keep models simple. For other API comparisons, see my AI API pricing comparison.
Frequently Asked Questions
Should I use the Responses API or Chat Completions?
Use the Responses API for new projects. It supports built-in tools, stored conversations and the latest features, and GPT-6 Astra needs it for tool calling.
What is the cheapest OpenAI model for Python projects?
GPT-6 Luna (gpt-6-luna) costs $0.10 per million input tokens and $0.50 per million output tokens. It is the best choice for high-volume, simple tasks.
Does ChatGPT Plus include API access?
No. The API is billed separately through platform credits. Eligible Pro and Business subscribers can claim up to $500 per month in API credits.
How do I stream responses in Python?
Pass stream=True to client.responses.create() and print each response.output_text.delta event’s delta.
How do I get JSON back reliably?
Use structured outputs with client.responses.parse() and a Pydantic model. The result is in response.output_parsed.
How much does the Batch API save?
Batch requests cost 50% less than normal calls. Results arrive within 24 hours, with up to 50,000 requests per batch.
Is prompt caching automatic?
Yes. Prompts of 1,024 tokens or more are cached automatically on GPT-5.6 and newer, and you can see hits in usage.input_tokens_details.cached_tokens.




