Managing Responses

Every response is stored by default and can be retrieved or deleted using its id.


Opting Out of Storage

Set store=false to skip persisting a response. The response is returned normally but is not saved to history.

  • curl

  • Python (OpenAI SDK)

curl -X POST $BASE_URL/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AA_TOKEN" \
  -d '{
    "model": "qwen3-32b-tool",
    "input": "What is 2+2?",
    "store": false
  }'
response = client.responses.create(
    model="qwen3-32b-tool",
    input="What is 2+2?",
    store=False,
)
# Response is returned but not persisted
Impact on conversation chaining

A non-stored response cannot be referenced by a later previous_response_id. Any follow-up call that tries to chain from it will receive a 404 not_found_error, because there is no history entry to look up.

For example, if Turn 2 uses store=false, then Turn 3 cannot set previous_response_id to Turn 2’s ID.

This is useful for one-off queries, e.g. classification or extraction, where you don’t need conversation continuity or later retrieval.


Retrieve a Response

Fetch a stored response by its ID.

  • curl

  • Python (OpenAI SDK)

curl $BASE_URL/v1/responses/resp_abc123 \
  -H "Authorization: Bearer $AA_TOKEN"
response = client.responses.retrieve("resp_abc123")

print(response.id)           # "resp_abc123"
print(response.status)       # "completed"
print(response.output_text)  # The assistant's response text

Response:

{
  "id": "resp_abc123",
  "object": "response",
  "created_at": 1711000000,
  "model": "qwen3-32b-tool",
  "status": "completed",
  "output": [...],
  "usage": {...}
}

Retrieve as Streaming SSE

You can replay a stored response as a Server-Sent Events stream:

curl $BASE_URL/v1/responses/resp_abc123?stream=true \
  -H "Authorization: Bearer $AA_TOKEN"

Not Found

Requesting a non-existent ID returns 404:

{
  "error": {
    "message": "Response with ID 'resp_nonexistent' not found.",
    "type": "not_found_error",
    "param": null,
    "code": "response_not_found"
  }
}

Delete a Response

Remove a stored response when it’s no longer needed.

  • curl

  • Python (OpenAI SDK)

curl -X DELETE $BASE_URL/v1/responses/resp_abc123 \
  -H "Authorization: Bearer $AA_TOKEN"
client.responses.delete("resp_abc123")  # returns None on success

Response:

{
  "id": "resp_abc123",
  "object": "response",
  "deleted": true
}

Deletion Behavior

Deletion follows a retention policy built around two modes: soft delete (the default, reversible) and hard delete (admin-only, permanent). A plain DELETE is always a soft delete, and the 200 body above ("deleted": true) is returned in both modes.

Soft delete (default)

A standard DELETE marks the response as deleted by stamping a deleted_at timestamp. The row remains in the database but becomes invisible to normal access:

  • It can no longer be retrieved via GET /v1/responses/{id} (returns 404).

  • It can no longer be used as a previous_response_id; chaining from it fails with 404 not_found_error.

  • It is excluded from conversation listings.

Cascade to descendants: Soft-deleting a response also soft-deletes every subsequent response in its chain: any response whose ancestor_ids contain the deleted ID. Deleting a parent therefore removes the entire downstream subtree in one call, so you no longer need to delete chains leaf-to-root.

For example, in a chain resp_1 → resp_2 → resp_3, deleting resp_1 soft-deletes resp_1, resp_2, and resp_3 together. Deleting resp_2 removes resp_2 and resp_3 but leaves resp_1 intact.

Soft delete does not affect the parent conversation; the conversation row remains unchanged.

Hard delete (admin only)

Admins can permanently remove a response by passing hard_delete=true. This physically deletes the row(s) from the database; the data is unrecoverable.

  • curl

curl -X DELETE "$BASE_URL/v1/responses/resp_abc123?hard_delete=true" \
  -H "Authorization: Bearer $AA_ADMIN_TOKEN"
  • Hard delete also cascades to all descendants, including ones that were already soft-deleted.

  • A non-admin caller passing hard_delete=true receives 403 (permission_error / insufficient_permissions).

  • If the target response is still being processed in the background, hard delete is blocked with 425 Too Early (too_early_error / response_in_progress). Wait until processing completes or fails, then retry.

Hard delete is irreversible

Hard delete exists for compliance scenarios (e.g. GDPR erasure requests) where data must be physically purged. Once a response is hard-deleted it cannot be recovered. For routine deletions, prefer the default soft delete.

Admin: retrieve a soft-deleted response

Admins can read soft-deleted responses by passing include_deleted=true. This is intended for audit and recovery workflows and is logged.

  • curl

curl "$BASE_URL/v1/responses/resp_abc123?include_deleted=true" \
  -H "Authorization: Bearer $AA_ADMIN_TOKEN"

Non-admin callers passing include_deleted=true receive 403.

Admin: recover a soft-deleted response

Because soft delete is reversible, admins can restore a soft-deleted response with PATCH /v1/responses/{id} and recovery_from_delete=true. Recovery cascades to all soft-deleted descendants, restoring the subtree that was removed together.

  • curl

curl -X PATCH "$BASE_URL/v1/responses/resp_abc123?recovery_from_delete=true" \
  -H "Authorization: Bearer $AA_ADMIN_TOKEN"
  • Returns the recovered response object.

  • Non-admin callers receive 403; omitting recovery_from_delete=true returns 400.

  • Hard-deleted responses cannot be recovered; there is no row left to restore.


Response Metadata

You can attach key-value metadata to any response for tagging, filtering, or tracking:

  • curl

  • Python (OpenAI SDK)

curl -X POST $BASE_URL/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AA_TOKEN" \
  -d '{
    "model": "qwen3-32b-tool",
    "input": "Summarize the quarterly report.",
    "metadata": {
      "team": "finance",
      "request_source": "slack-bot"
    }
  }'
response = client.responses.create(
    model="qwen3-32b-tool",
    input="Summarize the quarterly report.",
    metadata={
        "team": "finance",
        "request_source": "slack-bot",
    },
)

print(response.metadata)  # {"team": "finance", "request_source": "slack-bot"}

Constraints:

Limit Value

Maximum keys

16

Maximum key length

64 characters

Maximum value length

512 characters

Value type

Strings only

Metadata is echoed back in the response object and persisted with the response.


Conversations API

Conversations let you group related responses together and manage them as a unit. There are two ways to use conversations:

  1. Explicitly, create a conversation up front, then reference it in requests via the conversation field.

  2. Implicitly, when a response belongs to a conversation and you chain from it with previous_response_id, the conversation ID is inherited automatically.

Assigning Responses to a Conversation

Pass the conversation field when creating a response to assign it to an existing conversation. You can pass either a bare string or an object:

  • curl

  • Python (OpenAI SDK)

# Assign a response to an existing conversation.
# Replace CONVERSATION_ID with the id of your conversation.
curl -X POST $BASE_URL/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AA_TOKEN" \
  -d '{
    "model": "qwen3-32b-tool",
    "input": "Hello!",
    "conversation": "CONVERSATION_ID"
  }'
import httpx

# Create a conversation
conv = httpx.post(
    f"{BASE_URL}/v1/conversations",
    headers={"Authorization": f"Bearer {AA_TOKEN}"},
).json()

# Assign a response to it
response = client.responses.create(
    model="qwen3-32b-tool",
    input="Hello!",
    extra_body={"conversation": conv["id"]},
)

When you chain from a response that belongs to a conversation using previous_response_id, the follow-up response automatically inherits the conversation; you don’t need to pass conversation again.

Create a Conversation

  • curl

  • Python

curl -X POST $BASE_URL/v1/conversations \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AA_TOKEN"
import httpx

conv = httpx.post(
    f"{BASE_URL}/v1/conversations",
    headers={"Authorization": f"Bearer {AA_TOKEN}"},
).json()

print(conv["id"])  # "conv_abc123"

Response:

{
  "id": "conv_abc123",
  "object": "conversation",
  "metadata": {},
  "created_at": 1711000000,
  "updated_at": 1711000000
}

You can optionally pass metadata (key-value string pairs, max 16 keys) on create or update it later via POST /v1/conversations/{id}. Metadata follows the same constraints as response metadata.

Retrieve a Conversation

  • curl

  • Python

curl $BASE_URL/v1/conversations/{conversation_id} \
  -H "Authorization: Bearer $AA_TOKEN"
import httpx

conv = httpx.get(
    f"{BASE_URL}/v1/conversations/{conversation_id}",
    headers={"Authorization": f"Bearer {AA_TOKEN}"},
).json()

List Conversations

  • curl

  • Python

curl "$BASE_URL/v1/conversations?limit=10&offset=0&order=desc" \
  -H "Authorization: Bearer $AA_TOKEN"
import httpx

result = httpx.get(
    f"{BASE_URL}/v1/conversations",
    params={"limit": 10, "offset": 0, "order": "desc"},
    headers={"Authorization": f"Bearer {AA_TOKEN}"},
).json()

for conv in result["data"]:
    print(conv["id"], conv["metadata"])

Response:

{
  "object": "list",
  "data": [
    {
      "id": "conv_abc123",
      "object": "conversation",
      "metadata": {},
      "created_at": 1711000000,
      "updated_at": 1711000000
    }
  ],
  "has_more": false,
  "first_id": "conv_abc123",
  "last_id": "conv_abc123"
}
Parameter Type Default Description

limit

integer

20

Maximum conversations to return (max 100)

offset

integer

0

Number of conversations to skip

order

string

desc

Sort by updated_at: "asc" or "desc"

metadata.application

string

none

Return only conversations with matching metadata.application

include_deleted

boolean

false

Admin only. Include soft-deleted conversations in the list. Non-admin callers passing true receive 403.

To list conversations for one app, create conversations with an application metadata value and pass the same value as metadata.application when listing.

  • curl

  • Python

curl -X POST $BASE_URL/v1/conversations \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AA_TOKEN" \
  -d '{"metadata": {"application": "legal-agent"}}'

curl "$BASE_URL/v1/conversations?metadata.application=legal-agent" \
  -H "Authorization: Bearer $AA_TOKEN"
import httpx

httpx.post(
    f"{BASE_URL}/v1/conversations",
    json={"metadata": {"application": "legal-agent"}},
    headers={"Authorization": f"Bearer {AA_TOKEN}"},
)

result = httpx.get(
    f"{BASE_URL}/v1/conversations",
    params={"metadata.application": "legal-agent"},
    headers={"Authorization": f"Bearer {AA_TOKEN}"},
).json()

List Responses in a Conversation

Fetch all responses belonging to a conversation. Each item includes the full response data plus tree metadata (ancestor_ids, depth) and the original request_input.

  • curl

  • Python

# Ascending order (default)
curl "$BASE_URL/v1/conversations/{conversation_id}/responses" \
  -H "Authorization: Bearer $AA_TOKEN"

# Descending order
curl "$BASE_URL/v1/conversations/{conversation_id}/responses?order=desc" \
  -H "Authorization: Bearer $AA_TOKEN"

# Admin only: include soft-deleted responses
curl "$BASE_URL/v1/conversations/{conversation_id}/responses?include_deleted=true" \
  -H "Authorization: Bearer $AA_ADMIN_TOKEN"
import httpx

response = httpx.get(
    f"{BASE_URL}/v1/conversations/{conversation_id}/responses",
    headers={"Authorization": f"Bearer {AA_TOKEN}"},
)

data = response.json()
for item in data["data"]:
    print(f"{item['id']} (depth={item['depth']}): {item['status']}")

Response:

{
  "object": "list",
  "data": [
    {
      "id": "resp_001",
      "object": "response",
      "status": "completed",
      "model": "qwen3-32b-tool",
      "output": [...],
      "ancestor_ids": [],
      "depth": 0,
      "request_input": [{"type": "message", "role": "user", "content": "Hello"}]
    },
    {
      "id": "resp_002",
      "object": "response",
      "status": "completed",
      "model": "qwen3-32b-tool",
      "output": [...],
      "ancestor_ids": ["resp_001"],
      "depth": 1,
      "request_input": [{"type": "message", "role": "user", "content": "Follow up"}]
    }
  ]
}
Parameter Type Default Description

order

string

asc

Sort by created_at: "asc" or "desc"

include_deleted

boolean

false

Admin only. Include soft-deleted responses in the list. Non-admin callers passing true receive 403.

Delete a Conversation

  • curl

  • Python

curl -X DELETE $BASE_URL/v1/conversations/{conversation_id} \
  -H "Authorization: Bearer $AA_TOKEN"
import httpx

httpx.delete(
    f"{BASE_URL}/v1/conversations/{conversation_id}",
    headers={"Authorization": f"Bearer {AA_TOKEN}"},
)

Response:

{
  "id": "conv_abc123",
  "object": "conversation",
  "deleted": true
}

Conversation deletion follows the same retention policy as responses, with the same options: soft delete is the default, hard_delete=true and recovery are admin-only, and admins can read soft-deleted data with include_deleted=true (see List Responses in a Conversation).

The key difference: deleting a single response only cascades to that response’s descendants (later responses in its chain), leaving its ancestors intact. Deleting a conversation instead removes every response in it, the entire response tree, including ancestors, not just one chain.

  • The conversation and all its responses are marked as deleted (cascade) but remain in the database.

  • Soft-deleted responses cannot be retrieved via GET /v1/responses/{id} or used as previous_response_id.

  • You do not need to delete individual responses first; the cascade handles it.

  • Deleting an already-deleted or non-existent conversation returns 404.

Hard delete (admin only)

Admins can permanently remove a conversation and every history entry it owns by passing hard_delete=true:

  • curl

curl -X DELETE "$BASE_URL/v1/conversations/{conversation_id}?hard_delete=true" \
  -H "Authorization: Bearer $AA_ADMIN_TOKEN"
  • The conversation row and all associated responses are physically deleted and cannot be recovered.

  • A non-admin caller passing hard_delete=true receives 403.

  • If any response in the conversation is still being processed in the background, hard delete is blocked with 425 Too Early (conversation_has_in_progress_responses).

Recovery (admin only)

A soft-deleted conversation can be restored with PATCH /v1/conversations/{id} and recovery_from_delete=true. Recovery restores the conversation and cascades to its soft-deleted responses.

  • curl

curl -X PATCH "$BASE_URL/v1/conversations/{conversation_id}?recovery_from_delete=true" \
  -H "Authorization: Bearer $AA_ADMIN_TOKEN"

Non-admin callers receive 403; hard-deleted conversations cannot be recovered.