Skip to content

Conversations

The Responses API is stateful. Every response belongs to a stored thread, and Urai adds the earlier turns for you. You send the new message only.

store is always true on this surface. The thread is the state that makes previous_response_id work, so there is no unstored mode.

Send the id of the last response as previous_response_id.

Terminal window
curl https://chat.app.urai.dev/api/agents/$AGENT_ID/v1/responses \
-H "Authorization: Bearer $URAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "agent",
"previous_response_id": "resp_9f2c1e40-1b7c-4c65-9a30-51a6b0f2c3d1",
"input": "And when does it arrive?"
}'
const first = await client.responses.create({
model: "agent",
input: "Where is order 4821?",
});
const second = await client.responses.create({
model: "agent",
previous_response_id: first.id,
input: "And when does it arrive?",
});

A response id has the form resp_<uuid>. Urai also accepts the bare UUID, for a client that stores the id without its prefix.

An id that is not a UUID returns 400 with the code invalid_response_id. An id that names no response you can read returns 404 with the code response_not_found.

Every response carries the thread in a header:

x-thread-id: 0723cee0-3912-434e-8450-2c701e6f9713

Send that header back to continue the same thread. This suits a client that tracks the conversation rather than the last response.

Terminal window
curl https://chat.app.urai.dev/api/agents/$AGENT_ID/v1/responses \
-H "Authorization: Bearer $URAI_API_KEY" \
-H "Content-Type: application/json" \
-H "x-thread-id: 0723cee0-3912-434e-8450-2c701e6f9713" \
-d '{ "model": "agent", "input": "And when does it arrive?" }'

The x-thread-id header is a Urai extension, and it has the same name and meaning on the Chat Completions API.

A header that is not a UUID returns 400 with the code invalid_thread_id. Urai does not start a new thread instead, because a conversation that is lost without a message is worse than a request that fails.

When you send neither previous_response_id nor x-thread-id, Urai starts a new thread.

Set user to a stable identifier when one API key serves several end users.

"user": "customer-4821"

A thread belongs to the exact triple of API key, user value, and agent. To continue a thread, send the same user value that created it. A different value returns 404, so one end user cannot learn that another user’s conversation exists.

When you omit user, every such call shares one bucket for that key. A request made by a workflow run defaults to the principal wf:<run_id>, so each run keeps its own thread.

The GET, DELETE, and cancel routes carry no body. Send the principal as a query parameter on those:

GET /responses/resp_9f2c1e40-.../input_items?user=customer-4821

input takes a string, or a list of items.

{
"model": "agent",
"input": [
{ "role": "user", "content": "I ordered a lamp last week." },
{ "role": "assistant", "content": "Thanks. What is the order number?" },
{ "role": "user", "content": "It is 4821." }
]
}

The last user message drives the turn. Everything before it is written into the thread as history first. Use this to seed a new thread with a conversation that happened somewhere else.

Four rules apply:

  • The list must hold at least one user message. An empty list, or a list with no user message, returns 400 with the code invalid_input.
  • The last user message must carry text or an attachment.
  • system and developer messages are not stored. Send instructions in the instructions field instead, on each request that needs them.
  • An item with a type other than message is skipped. Tools run on the server, so a function call item that you echo back is already in the thread.

An unknown role returns 400 with the code invalid_input.

instructions goes after the agent’s own system instructions. It does not replace them.

{
"model": "agent",
"instructions": "Answer in Portuguese. Keep it to two sentences.",
"input": "Where is order 4821?"
}

The value applies to that request only. Urai does not store it on the thread.

A response echoes back your instructions and never the agent’s own. The instructions that the author of the agent wrote stay on the server.

vars carries context for the turn. Your UraiJS tools read it as meta.vars, layered over the vars set on the agent. Use it for the identity of the caller, the current page, a tenant id, or anything else the tools need.

{
"model": "agent",
"vars": { "customer_id": "4821", "plan": "business" },
"input": "What is my delivery window?"
}

Urai writes the vars to the thread before the turn starts, so the tools of that same turn see them.

metadata is an alias for vars. Use it when your SDK exposes metadata as a typed field and gives you no way to send an unknown one. When both set the same key, vars wins.

Three rules apply:

  • The value must be a JSON object. Anything else returns 400 with the code invalid_vars.
  • The merged object holds at most 32 keys.
  • A request that sends neither field leaves the vars of the thread as they are. To clear a key, send the field again without it.

The metadata field of a response reports the vars of the thread.

Attachments reach the last user message. Two ways to send them.

Upload first with POST /api/openai/v1/files, then send the ids:

{
"model": "agent",
"attachments": ["9d4c1f7e-1a2b-4c3d-8e5f-6a7b8c9d0e1f"],
"input": "What does this invoice total?"
}

Or send the bytes inline as content parts:

{
"model": "agent",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "What does this invoice total?" },
{
"type": "input_file",
"filename": "invoice-2041.pdf",
"file_data": "data:application/pdf;base64,JVBERi0xLjQK..."
}
]
}
]
}

An image works the same way with input_image:

{ "type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo..." }

Or by id, when you uploaded it first:

{ "type": "input_image", "file_id": "9d4c1f7e-1a2b-4c3d-8e5f-6a7b8c9d0e1f" }

Four rules apply:

  • image_url must be a base64 data: URL. Urai does not fetch remote URLs.
  • One request carries at most 16 attachments.
  • One attachment holds at most 25 MiB after decoding.
  • A file id belongs to the API key and the user value that uploaded it.

The file rules and the error codes are the same as on the Chat Completions API. See Send files and images.

Terminal window
curl https://chat.app.urai.dev/api/agents/$AGENT_ID/v1/responses/$RESPONSE_ID \
-H "Authorization: Bearer $URAI_API_KEY"

You get the same response object. A turn that is still running reports "status": "in_progress" and holds the text that arrived so far.

instructions and user come back as null here. Urai does not store the per-request values, and it never echoes the instructions of the agent.

Terminal window
curl "https://chat.app.urai.dev/api/agents/$AGENT_ID/v1/responses/$RESPONSE_ID/input_items" \
-H "Authorization: Bearer $URAI_API_KEY"
{
"object": "list",
"data": [
{
"id": "msg_1c9a...",
"type": "message",
"role": "user",
"content": [{ "type": "input_text", "text": "Where is order 4821?" }]
}
],
"first_id": "msg_1c9a...",
"last_id": "msg_1c9a...",
"has_more": false
}

The list holds the user and assistant messages of the thread up to that response, and it is never paged.

Terminal window
curl -X POST https://chat.app.urai.dev/api/agents/$AGENT_ID/v1/responses/$RESPONSE_ID/cancel \
-H "Authorization: Bearer $URAI_API_KEY"

The call returns the response with "status": "cancelled". The agent stops where it is, and Urai keeps the text that arrived before the stop.

A later GET of the same response reports completed, because what is stored is a finished message that holds partial text. A turn that already ended returns its response unchanged.

A thread runs one turn at a time, so a cancel stops that one turn.

Terminal window
curl -X DELETE https://chat.app.urai.dev/api/agents/$AGENT_ID/v1/responses/$RESPONSE_ID \
-H "Authorization: Bearer $URAI_API_KEY"
{ "id": "resp_9f2c1e40-...", "object": "response", "deleted": true }

The delete removes the assistant turn and the user turn that prompted it. The rest of the thread stays, so the responses before and after it still resolve.

The delete also removes the usage record of that turn, and it removes the two messages from search. Deleting a response deletes what it cost.