Skip to content

Overview

Urai runs an OpenAI-compatible chat completions endpoint. If your code already speaks the OpenAI Chat Completions format, you change two things to reach Urai: the base URL and the API key.

One endpoint reaches every model your organization has credentials for. The same endpoint runs your UraiJS tools, searches your knowledge collections, and reads the files you attach, all on the server. You get a finished answer back.

https://chat.app.urai.dev/api/openai/v1

Every path on this page is relative to that base URL.

Send an API key as a bearer token.

Authorization: Bearer sk-urai-...

To create a key:

  1. Sign in to Urai.
  2. Open API Keys in the sidebar. Only organization owners and admins see this page.
  3. Select Create API Key. Give the key a name. Set an expiry if you want one.
  4. Copy the key now. Urai shows the value one time and cannot show it again.

A key belongs to the organization that created it. The key reaches that organization’s models, tools, and collections. You can disable a key from the same page. Disabled keys and expired keys stop immediately.

Keep the key on your server. Anyone who has the key can make requests that your organization pays for. Do not put a key in browser code. Do not commit a key to source control.

Terminal window
curl https://chat.app.urai.dev/api/openai/v1/chat/completions \
-H "Authorization: Bearer $URAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-haiku-4-5",
"messages": [
{ "role": "user", "content": "Give me one sentence on the water cycle." }
]
}'

The same request with the OpenAI Python SDK:

from openai import OpenAI
client = OpenAI(
base_url="https://chat.app.urai.dev/api/openai/v1",
api_key="sk-urai-...",
)
response = client.chat.completions.create(
model="anthropic/claude-haiku-4-5",
messages=[{"role": "user", "content": "Give me one sentence on the water cycle."}],
)
print(response.choices[0].message.content)

And with the OpenAI Node SDK:

import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://chat.app.urai.dev/api/openai/v1",
apiKey: "sk-urai-...",
});
const response = await client.chat.completions.create({
model: "anthropic/claude-haiku-4-5",
messages: [{ role: "user", content: "Give me one sentence on the water cycle." }],
});
console.log(response.choices[0].message.content);

The model field takes the form provider/model, such as anthropic/claude-haiku-4-5 or gemini/gemini-2.5-flash.

To see what your organization can use, list the models:

Terminal window
curl https://chat.app.urai.dev/api/openai/v1/models \
-H "Authorization: Bearer $URAI_API_KEY"
{
"object": "list",
"data": [
{ "id": "anthropic/claude-haiku-4-5", "object": "model", "created": 0, "owned_by": "anthropic" },
{ "id": "gemini/gemini-2.5-flash", "object": "model", "created": 0, "owned_by": "gemini" }
]
}

Send any id from that list as your model. A model your organization has no credential for returns 400 with the code model_not_found.

To read one model, put its id in the path. The id contains a slash, and the endpoint accepts it:

Terminal window
curl https://chat.app.urai.dev/api/openai/v1/models/anthropic/claude-haiku-4-5 \
-H "Authorization: Bearer $URAI_API_KEY"
{
"id": "chatcmpl-2f1c9a0c4c1c4f5f9a2b1d3e4f5a6b7c",
"object": "chat.completion",
"created": 1710000000,
"model": "anthropic/claude-haiku-4-5",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Water evaporates, forms clouds, and falls again as rain." },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 36,
"completion_tokens": 14,
"total_tokens": 50
}
}

Every response has one choice. The endpoint does not support n greater than 1.

usage covers the whole request. If the model called tools, the totals include every round.

Set "stream": true to receive Server-Sent Events. Each event holds a chat.completion.chunk object. The stream ends with data: [DONE].

Terminal window
curl https://chat.app.urai.dev/api/openai/v1/chat/completions \
-H "Authorization: Bearer $URAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-haiku-4-5",
"stream": true,
"stream_options": { "include_usage": true },
"messages": [{ "role": "user", "content": "Count from 1 to 5." }]
}'
data: {"choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}], ...}
data: {"choices":[{"index":0,"delta":{"content":"1 2 3"},"finish_reason":null}], ...}
data: {"choices":[{"index":0,"delta":{"content":" 4 5"},"finish_reason":null}], ...}
data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}], ...}
data: {"choices":[],"usage":{"prompt_tokens":46,"completion_tokens":21,"total_tokens":67}, ...}
data: [DONE]

Three rules apply to the stream:

  • The first chunk carries {"role": "assistant"} and no content.
  • Tool rounds are silent. Urai sends no event while a tool runs. Text starts again when the model writes the answer.
  • stream_options.include_usage adds one final chunk before [DONE]. That chunk has an empty choices array and the token totals.
stream = client.chat.completions.create(
model="anthropic/claude-haiku-4-5",
messages=[{"role": "user", "content": "Count from 1 to 5."}],
stream=True,
)
for chunk in stream:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="")

Use response_format to constrain the shape of the answer.

For free-form JSON:

"response_format": { "type": "json_object" }

For a schema:

"response_format": {
"type": "json_schema",
"json_schema": {
"name": "invoice_summary",
"description": "One line for each invoice in the message.",
"schema": {
"type": "object",
"properties": {
"invoices": {
"type": "array",
"items": {
"type": "object",
"properties": {
"number": { "type": "string" },
"total": { "type": "number" }
},
"required": ["number", "total"]
}
}
},
"required": ["invoices"]
}
}
}

Three rules apply:

  • json_schema.name holds 1 to 64 characters from A-Za-z0-9_-.
  • json_schema.schema is a JSON Schema object.
  • You cannot send response_format with tool_choice: "required". A model writes a tool call or a constrained answer, not both. Urai rejects the pair with 400 and the code invalid_response_format.

Urai accepts "type": "text" and applies no constraint, because that is the default behaviour of the provider.

The endpoint is stateless by default, the same as OpenAI. You send the full messages array on each request.

Urai also stores each conversation on the server. Every response carries an x-thread-id header:

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

Send that header back to continue the conversation. Include only the new message. Urai adds the earlier turns for you.

Terminal window
curl https://chat.app.urai.dev/api/openai/v1/chat/completions \
-H "Authorization: Bearer $URAI_API_KEY" \
-H "Content-Type: application/json" \
-H "x-thread-id: 0723cee0-3912-434e-8450-2c701e6f9713" \
-d '{
"model": "anthropic/claude-haiku-4-5",
"messages": [{ "role": "user", "content": "And what did I ask before that?" }]
}'

The x-thread-id header is a Urai extension. The OpenAI specification has no equivalent. To stay stateless, omit the header and send the history yourself.

System messages are not stored. Send them on every request.

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

"user": "customer-4821"

A thread belongs to the exact pair of API key and user value. To continue a thread, send the same user value that created it. A different value returns 404 with the code thread_not_found. Urai returns 404 rather than 403, so one end user cannot learn that another user’s thread exists.

Errors use the OpenAI envelope and the matching HTTP status.

{
"error": {
"message": "The model 'openai/gpt-4o' is not available for this organization",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}

The Reference page lists every code.