Managing Responses
Every response is stored by default and can be retrieved or deleted using its id. Stored data is also subject to an automatic retention policy that expires old responses and conversations over time.
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 For example, if Turn 2 uses |
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": {...}
}
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}(returns404). -
It can no longer be used as a
previous_response_id; chaining from it fails with404 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=truereceives403(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; omittingrecovery_from_delete=truereturns400. -
Hard-deleted responses cannot be recovered; there is no row left to restore.
Automatic Retention
Beyond the manual DELETE above, stored data is expired automatically by a retention policy. In deployments backed by a database, a background job runs on a schedule and applies the policy to every response and conversation once it is old enough; you don’t need to call DELETE yourself for routine cleanup.
The lifecycle reuses the same soft delete and hard delete mechanics described above:
-
When a record is older than the retention period (measured from its
created_at), the configured expiry action runs. -
With the default
soft-deleteaction, the record is stamped withdeleted_at(it disappears from normal reads but stays in the database). -
If enabled, the record is then hard-deleted once the grace period after soft deletion elapses, physically removing it.
Expiry cascades to descendants exactly like a manual delete.
Default policy
The global defaults are set via environment variables (prefix RETENTION_):
| Setting | Env var | Default | Description |
|---|---|---|---|
Retention period |
|
|
Age after |
Expiry action |
|
|
|
Grace period |
|
|
Delay between soft deletion and permanent removal. |
Hard-delete after grace |
|
|
Whether soft-deleted records are hard-deleted once the grace period elapses. |
Schedule |
|
|
Cron expression controlling when retention processing runs (default: daily at midnight). |
|
With the defaults above, a response is soft-deleted 30 days after creation and permanently removed 7 days later. To keep data indefinitely, raise |
Admin: retention policies
Admins can override the global default with a per-application policy through the admin API. Application-level policies take precedence over the global environment defaults, so different applications can have different retention rules.
Access rules mirror the other admin operations:
-
Super-admins (no
x-application-id) may target any application via the path/bodyapplication_id. -
Application-admins (with an
x-application-id) may only manage their own application; targeting a different one returns403.
Policy fields (durations are strings like 90d):
| Field | Type | Description |
|---|---|---|
|
string | null |
Target application. |
|
string |
Age before the expiry action runs. Admin-set policies are validated against a 1-day minimum (this floor is not applied to the |
|
string |
|
|
string |
Delay before hard deletion (default |
|
boolean |
Hard-delete after grace (default |
|
string |
Cron expression (default |
Create a policy
-
curl
curl -X POST $BASE_URL/admin/retention-policies \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AA_ADMIN_TOKEN" \
-d '{
"application_id": "legal-agent",
"retention_period": "90d",
"expiry_action": "soft-delete",
"grace_period": "7d",
"hard_delete_after_grace": true
}'
Response (201 Created):
{
"application_id": "legal-agent",
"retention_period": "90d",
"expiry_action": "soft-delete",
"grace_period": "7d",
"hard_delete_after_grace": true,
"schedule": "0 0 * * *",
"created_at": "2026-07-07T00:00:00Z",
"updated_at": "2026-07-07T00:00:00Z"
}
Creating a policy for an application that already has one returns 409.
Retrieve the effective policy
Returns the policy currently in effect for an application, including its source (application_admin, super_admin, or global_default). Pass detail=true to also get the full precedence breakdown.
-
curl
curl "$BASE_URL/admin/retention-policies/legal-agent?detail=true" \
-H "Authorization: Bearer $AA_ADMIN_TOKEN"
Update a policy
A PUT applies a partial update; only the fields you send are changed.
-
curl
curl -X PUT $BASE_URL/admin/retention-policies/legal-agent \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AA_ADMIN_TOKEN" \
-d '{"retention_period": "180d"}'
Updating a non-existent policy returns 404.
Delete a policy
Removes the application-level policy; the application falls back to the global default.
-
curl
curl -X DELETE $BASE_URL/admin/retention-policies/legal-agent \
-H "Authorization: Bearer $AA_ADMIN_TOKEN"
|
A separate, feature-flagged admin router ( |
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:
-
Explicitly, create a conversation up front, then reference it in requests via the
conversationfield. -
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 |
|---|---|---|---|
|
integer |
|
Maximum conversations to return (max 100) |
|
integer |
|
Number of conversations to skip |
|
string |
|
Sort by |
|
string |
|
Return only conversations with matching |
|
boolean |
|
Admin only. Include soft-deleted conversations in the list. Non-admin callers passing |
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 |
|---|---|---|---|
|
string |
|
Sort by |
|
boolean |
|
Admin only. Include soft-deleted responses in the list. Non-admin callers passing |
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 asprevious_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=truereceives403. -
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.