CoreMesh

Responses API previous_response_id: Chain Turns in Python

Chain turns in the OpenAI Responses API with previous_response_id instead of resending history. Python example, 30-day retention, billing and the gotchas.

Responses API previous_response_id: Chain Turns in Python

previous_response_id is a parameter on the Responses API create call. Set it to the id of an earlier response, and the model treats that response, plus everything chained behind it, as the conversation so far. You send only the new user message. OpenAI's servers supply the history.

That replaces the Chat Completions habit of rebuilding a full messages array on every turn. Your application keeps one string per conversation, the latest response id, and passes it forward. OpenAI's conversation state guide describes this as chaining responses to create a threaded conversation. Below is the Python code, what it does to your bill, and the places people get bitten.

What previous_response_id actually does#

The Responses API is stateful. When you create a response with storage on, which is the default, OpenAI saves the response object server-side. As of October 19, 2026, the conversation state guide says response objects are saved for 30 days by default, can be viewed in the dashboard logs page, and can be retrieved via the API.

Because the object lives on the server, a later request can point at it by id. The new input is appended after the referenced thread, and the model answers with the whole thread in view. Each new response gets its own id, so a chain is a series of ids, each pointing one step back.

Two consequences that aren't obvious from the parameter name:

  • You always pass the id of the most recent response, not the first one. Pass a stale id and the model never sees the turns that came after it.
  • The structure is a tree, not a list. Two follow-ups can reference the same earlier id, which gives you a fork. The guide's WebSocket section says this directly: previous_response_id controls lineage, and a new lane can fork from a response on another lane.

The smallest working Python example#

Install the official openai package inside a Python virtual environment, export OPENAI_API_KEY, and run this. It's the same shape as the example in OpenAI's docs.

python
from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-6-astra",
    input="tell me a joke",
)
print(response.output_text)

second_response = client.responses.create(
    model="gpt-6-astra",
    previous_response_id=response.id,
    input=[{"role": "user", "content": "explain why this is funny."}],
)
print(second_response.output_text)

Look at what the second call leaves out: the joke, the original prompt, any assistant message. The model still resolves "this" because the server attached the first response as context.

input takes either a plain string or a list of role and content dicts. Both forms appear in the official examples, and the two calls above use one of each on purpose. If you haven't used the Python SDK before, the basics are covered in how to call the OpenAI API from Python.

On the model: gpt-6-astra is what the docs use. As of October 19, 2026, OpenAI's models page lists gpt-6-astra as the flagship, gpt-6.1-sol as the balanced option, and gpt-6-luna for cost-sensitive, high-volume work. For hammering on a chaining prototype, I'd use gpt-6-luna and switch up later.

Chaining a longer conversation in a loop#

In a real app the turns arrive one at a time, so the natural shape is a loop that carries the last id forward.

python
from openai import OpenAI

client = OpenAI()

turns = [
    "I'm planning three days in Lisbon in November. Sketch an itinerary.",
    "Swap day two for something indoors in case it rains.",
    "Compress the whole plan into a bulleted list I can paste into Notes.",
]

previous_id = None
for user_text in turns:
    response = client.responses.create(
        model="gpt-6-astra",
        input=user_text,
        previous_response_id=previous_id,
    )
    previous_id = response.id
    print(f"--- {response.id}\n{response.output_text}\n")

Passing None on the first turn starts a fresh thread. After that, the only conversation state your code holds is previous_id. Put it in your session store or database keyed by user, and a request from any of your servers can continue the thread.

The simulation below shows the lineage rules without touching the network. It stands in for the server's store, walks a chain back to its root, forks from an earlier id, and shows what happens when an id doesn't exist.

python
import itertools

store = {}
counter = itertools.count(1)


def create_response(user_text, previous_response_id=None):
    """Stand-in for client.responses.create(): saves the turn, returns an id."""
    if previous_response_id is not None and previous_response_id not in store:
        raise ValueError(f"unknown previous_response_id: {previous_response_id}")
    rid = f"fake-{next(counter):03d}"
    store[rid] = {
        "previous_response_id": previous_response_id,
        "user": user_text,
        "assistant": f"(answer to: {user_text})",
    }
    return rid


def thread(response_id):
    """What the server sees as context: walk the chain back to the root."""
    turns = []
    while response_id is not None:
        item = store[response_id]
        turns.append((response_id, item["user"]))
        response_id = item["previous_response_id"]
    return list(reversed(turns))


a = create_response("tell me a joke")
b = create_response("explain why this is funny", previous_response_id=a)
c = create_response("make it shorter", previous_response_id=b)
fork = create_response("tell a different joke instead", previous_response_id=a)

for label, rid in [("main chain", c), ("fork from the first response", fork)]:
    print(label)
    for step, (step_id, text) in enumerate(thread(rid), 1):
        print(f"  {step}. {step_id}: {text}")

try:
    create_response("hello?", previous_response_id="fake-999")
except ValueError as err:
    print("error:", err)

If you drive many conversations at once, the same loop works with the SDK's async client; see Python async await with runnable examples for the pattern.

How to check the response id and what came back#

The object returned by client.responses.create() is the API response. The two fields you'll touch most are id, which you store for the next turn, and output_text, a convenience that joins the text the model produced.

python
response = client.responses.create(model="gpt-6-astra", input="tell me a joke")

print(response.id)           # store this for previous_response_id
print(response.output_text)  # the assistant's text
print(response.output)       # full list of output items

output is the raw list of output items. A message is one kind of item, and each message carries content parts, with the text living in output text parts. The official Java example walks exactly that path: output, then message, then content, then output text. In Python, output_text does that walk for you, so you only need output when you want tool calls or other non-text items.

To check a stored response outside your code, open the logs page in the OpenAI dashboard. Stored responses appear there for the retention period, and the id you see is the one you'd pass as previous_response_id.

What chaining costs in tokens#

Here's the gotcha the parameter name hides. The conversation state guide is explicit: even when using previous_response_id, all previous input tokens for responses in the chain are billed as input tokens. Chaining saves you from shipping the history over the wire and from writing bookkeeping code. It does not shrink the prompt the model reads, and the earlier answers are part of what it reads too.

So the cost per turn grows with the thread, the same as if you'd resent everything yourself. The rough model below shows the shape of that growth. The numbers are placeholders, not measurements.

python
# Rough model of billed input per turn when chaining with previous_response_id.
# Each turn's context = everything the model has already read or written.
turns = [
    ("ask for an itinerary", 60, 700),
    ("swap day two", 20, 650),
    ("compress to bullets", 15, 300),
    ("add restaurant picks", 12, 500),
]

context = 0
total_billed_input = 0
print(f"{'turn':<22}{'new input':>10}{'billed input':>14}{'output':>8}")
for name, new_in, out in turns:
    billed = context + new_in
    total_billed_input += billed
    print(f"{name:<22}{new_in:>10}{billed:>14}{out:>8}")
    context = billed + out

print()
print("new input tokens you actually typed:", sum(t[1] for t in turns))
print("input tokens billed across the chain:", total_billed_input)

The context window applies as well. The guide defines it as the maximum number of tokens in a single request, counting input, output and reasoning tokens, and warns that tokens beyond the limit may be truncated. As of October 19, 2026, the models page lists a 1.05M context window and 128K max output for gpt-6-astra, gpt-6.1-sol and gpt-6-luna. Long agent threads can still get there. For those cases the guide points to server-side compaction with context_management and compact_threshold on the responses endpoint, and to a standalone compact endpoint.

OpenAI's pricing page lists separate rates for cached input and cache writes on the GPT-6 models, so a stable thread prefix matters for cost. The changelog also lists Prompt Cache Diagnostics in the Responses API, which compares cache reuse against a previous response and reports why a cache miss happened.

previous_response_id vs the Conversations API#

OpenAI offers two ways to avoid resending history, and they suit different jobs.

previous_response_id Conversations API
What you keep The latest response id A conversation id
What you pass each turn previous_response_id=<last id> conversation=<conversation id>
Retention (as of October 19, 2026) Response objects kept 30 days by default Items not subject to the 30-day TTL
Shape A chain you can fork from any earlier id One long-running object you keep appending to
Fits Single sessions, branching, experiments Threads that span sessions, devices or jobs

The Conversations version looks like this:

python
from openai import OpenAI

client = OpenAI()

conversation = client.conversations.create()

response = client.responses.create(
    model="gpt-6-astra",
    input=[{"role": "user", "content": "What are the 5 Ds of dodgeball?"}],
    conversation=conversation.id,
)
print(response.output_text)

Every later turn passes the same conversation.id, and the guide says any response attached to a conversation has its items persisted with no 30-day TTL. My rule of thumb: previous_response_id for anything a user finishes in one sitting, Conversations for anything they come back to next week.

WebSocket mode works with the same parameter. According to the guide, continuation over a socket uses the same previous_response_id semantics as HTTP, with repeated response.create events and a connection-local cache of recent responses for low-latency continuation. If an uncached id can't be resolved, the guide's instruction is to send a new turn with previous_response_id set to null and pass the full input context. OpenAI's changelog dates WebSocket mode for the Responses API to February 23, 2026.

Common mistakes with previous_response_id#

Passing the first id every turn. The thread quietly forks. Turn three references turn one, so the model never sees turn two's answer, and your user wonders why it forgot. Always overwrite the stored id with the newest response.id.

Chaining with store set to false. The guide says setting store to false disables saving the response. A later previous_response_id needs a saved response to point at, so leave storage on for any turn you intend to continue. If a response can't be stored for policy reasons, pass the full input yourself on the next turn.

Sending the history and the id. Teams migrating from Chat Completions sometimes keep appending the old messages array to input while also passing previous_response_id. Now the model reads every turn twice and you pay for both copies. Send only the new user message.

Expecting a smaller bill. Covered above, but it's the most common surprise. The guide says all previous input tokens in the chain are billed as input tokens. Budget as if you were resending.

Holding ids past the retention window. Response objects are kept 30 days by default as of October 19, 2026. An id older than that won't resolve. Catch the error, start a fresh chain with previous_response_id=None, and pass whatever context you have as full input, which is the same recovery the guide prescribes for unresolvable ids over WebSockets. For threads that must outlive 30 days, use the Conversations API from the start.

Treating the chain as unlimited memory. The context window still caps a single request, and the guide warns output may be truncated past it. Watch thread length and plan for compaction on long-running agents.

Checking the wrong field. output_text is text only. If a turn produced a tool call and no prose, output_text can be empty while output has the item you need. Inspect output before concluding the model returned nothing.

Quick reference#

  • Create the first response with no previous_response_id, or pass None.
  • After every turn, store response.id. Overwrite it, don't collect a list.
  • Next turn: pass previous_response_id=<latest id> and only the new message in input.
  • Keep store on for anything you plan to chain.
  • Budget tokens as though resending the whole thread. Chaining is a convenience, not a discount.
  • Response objects last 30 days by default as of October 19, 2026. Use the Conversations API for threads that need to live longer.
  • If an id won't resolve, start a new chain with full input context.
  • Read output_text for text, output for everything else, and use the dashboard logs page to look at stored responses.

Quick answers

What is previous_response_id in the OpenAI Responses API?

It's a create-call parameter that takes the id of an earlier response. The server attaches that response and its chain as context, so you send only the new user message instead of the full history.

Do I still pay for earlier turns when I use previous_response_id?

Yes. OpenAI's conversation state guide says all previous input tokens for responses in the chain are billed as input tokens. Chaining saves bandwidth and code, not tokens.

How do I check an API response's id in Python?

Print response.id on the object returned by client.responses.create(). Stored responses also appear in the OpenAI dashboard logs page, where the same id is shown.

How long does a response id stay usable?

As of October 19, 2026, response objects are saved for 30 days by default. Responses attached to a Conversations API conversation have their items persisted with no 30-day TTL.

What's the difference between a response id and a conversation id?

A response id identifies one turn, and you pass the newest one each time to extend a chain. A conversation id identifies a long-running object you pass unchanged on every turn.

Did this solve it?

One tap. No account.

Saved you an hour? Send a tip.

USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.

Base

Network

0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
Open in wallet · 5 USDC

On a computer? Scan the code with your phone wallet, or copy the address.

Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.

Discussion

Start the discussion

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS