Read project runs, usage, threads and scores from a backend with an API key.
The project read API answers project wide observability and conversation reads for a backend service. Send an API key to https://backend.assistant-api.com with the Aui-User-Id and Aui-Workspace-Id headers like every API key request; the workspace header is required but does not scope these reads, which cover the whole project. Every route on this page is read only. The cloud never returns system runs such as thread title runs. See Conventions for the shared hosts and credentials, and Errors for error shapes.
The project run object
| Field | Type | Meaning |
|---|---|---|
id | string | The run identifier. |
thread_id, message_id | string or null | The thread and message associated with the run. |
created_by | string or null | The identity that created the run. |
status, outcome_type, error_code | stored values | The run status, outcome classification and error code. |
provider, model_id | string or null | The provider and model used by the run. |
environment, release | string or null | The environment and release recorded for the run. |
tags | array | Tags recorded on the run. The tag query filters for a matching tag. |
metadata, attributes | object | The same object under both names. metadata remains for SDK backward compatibility. |
root_span_id | string or null | The root span identifier. |
service_name, agent_name | string or null | Service and agent names recorded for the run. |
usage_details, cost_details | object or null | Detailed usage and cost values recorded for the run. |
input_tokens, cached_input_tokens, output_tokens, reasoning_tokens | number or null | Token counts recorded for the run. |
duration_ms, first_token_ms | number or null | Run duration and time to the first token in milliseconds. |
cost_usd | decimal string or null | The run cost in US dollars. |
total_steps | number or null | The total number of recorded steps. |
assistant_id | string or null | The assistant associated with the run. |
created_at | ISO 8601 | When the run was created. |
The project span object
| Field | Type | Meaning |
|---|---|---|
id, run_id, parent_span_id | string or null | Span, run and parent span identifiers. |
created_at, started_at, ended_at | ISO 8601 or null | Span creation, start and end times. |
trace_id, span_id | string or null | W3C trace and span identifiers when exported from your server. |
name, operation | string or null | Span name and operation. |
provider, tool_call_id | string or null | Provider and tool call associated with the span. |
status_code, error_type | stored values | Span status code and error type. |
attributes | object or null | Attributes recorded for the span. |
span_index | number | The span index used to order spans with the same start time. |
type, step_type | stored values | type is generation or tool_call; step_type is initial, tool-result, continue or sampling. |
model_id, tool_name, tool_source | string or null | Model and tool details recorded for the span. |
input_tokens, cached_input_tokens, output_tokens, reasoning_tokens | number or null | Token counts recorded for the span. |
start_ms, end_ms | number or null | Start and end times relative to the run. |
finish_reason, status, error | stored values | Completion and error values recorded for the span. |
cost_usd | decimal string or null | The span cost in US dollars. |
usage_details, cost_details | object or null | Detailed usage and cost values recorded for the span. |
input, output | stored values | The span input and output. |
List project runs
GET /v1/projects/runs| Query | Type | Default | Rules |
|---|---|---|---|
status | enum | One of the API run status values. | |
outcome_type | enum | One of the API run outcome values. | |
model_id | string | 1 to 255 characters. | |
provider | string | 1 to 255 characters. | |
created_by | string | 1 to 255 characters. | |
thread_id | string | 1 to 48 characters. | |
environment | string | 1 to 64 characters. | |
release | string | 1 to 255 characters. | |
tag | string | 1 to 64 characters. Matches a tag on the run. | |
from | ISO 8601 datetime | Include runs from this time. | |
to | ISO 8601 datetime | Include runs through this time. | |
limit | integer | 50 | 1 to 100. |
cursor | string | An opaque cursor, 1 to 512 characters. |
GET /v1/projects/runs?limit=50&from=2026-09-01T00%3A00%3A00.000Z&to=2026-09-17T00%3A00%3A00.000Zcurl "https://backend.assistant-api.com/v1/projects/runs?limit=50&from=2026-09-01T00%3A00%3A00.000Z&to=2026-09-17T00%3A00%3A00.000Z" \
-H "Authorization: Bearer $ASSISTANT_API_KEY" \
-H "Aui-User-Id: user_123" \
-H "Aui-Workspace-Id: workspace_123"{
"runs": [],
"next_cursor": null
}The cursor is a strict base64url encoding of JSON shaped like { "created_at": ISO, "id": string }. It is opaque, so pass next_cursor unchanged as the next cursor value. Results are ordered by created_at DESC, id DESC, and the response contains limit rows at most.
| Status | Body | When |
|---|---|---|
200 | A runs array and a next_cursor value. | The read completed. The array is empty when no runs match. |
400 | A schema validation error object. | A query value failed validation. |
400 | { "error": "Invalid run cursor" } | cursor is not a valid project run cursor. |
401 | An authentication error object. | The authorization header is missing, malformed or expired. |
403 | { "error": "This endpoint requires an API key" } | The credential is not an API key, or the API key cannot use this route. |
Get a project run
GET /v1/projects/runs/{run_id}| Parameter | Type | Required | Rules |
|---|---|---|---|
runId | string | yes | 1 to 48 characters. |
GET /v1/projects/runs/run_000000000000000000000000curl https://backend.assistant-api.com/v1/projects/runs/run_000000000000000000000000 \
-H "Authorization: Bearer $ASSISTANT_API_KEY" \
-H "Aui-User-Id: user_123" \
-H "Aui-Workspace-Id: workspace_123"{
"id": "run_000000000000000000000000",
"thread_id": "thread_0qzof3jPoDwr7K3agyJN3D4U",
"message_id": null,
"created_by": null,
"status": null,
"outcome_type": null,
"error_code": null,
"provider": null,
"model_id": null,
"environment": null,
"release": null,
"tags": [],
"metadata": {},
"attributes": {},
"root_span_id": null,
"service_name": null,
"agent_name": null,
"usage_details": null,
"cost_details": null,
"input_tokens": 0,
"cached_input_tokens": 0,
"output_tokens": 0,
"reasoning_tokens": 0,
"duration_ms": 0,
"first_token_ms": null,
"cost_usd": "0",
"total_steps": 0,
"assistant_id": null,
"created_at": "2026-09-17T00:00:00.000Z",
"spans": []
}The response is the project run object plus spans. Spans are ordered by start_ms ASC, span_index ASC. Only spans belonging to a non system run are returned.
| Status | Body | When |
|---|---|---|
200 | The run object with spans. | The run is visible in the authenticated project. |
400 | A schema validation error object. | runId failed validation, including a value longer than 48 characters. |
401 | An authentication error object. | The authorization header is missing, malformed or expired. |
403 | { "error": "This endpoint requires an API key" } | The request does not use an API key. |
404 | { "error": "Run not found" } | No matching non system run is visible in the project. |
The span order is part of the response contract, so clients can render a stable sequence without sorting the returned array.
Get daily project usage
GET /v1/projects/usage/daily| Query | Type | Default | Rules |
|---|---|---|---|
from | YYYY-MM-DD | 30 day window | A real UTC calendar day. If omitted, it is 29 days before to. |
to | YYYY-MM-DD | Today in UTC | A real UTC calendar day. |
group_by | enum | model, which groups rows by day, provider and model. |
The default range ends today in UTC and starts 29 days earlier. A requested range cannot have a negative span or a span greater than 366 days. Invalid calendar dates are also rejected. Without group_by, every day in the range appears, including days with zero values. With group_by=model, the response contains one row for each stored day, provider and model combination, with no zero fill; provider and model_id can be null.
GET /v1/projects/usage/daily?from=2026-09-01&to=2026-09-17&group_by=modelcurl "https://backend.assistant-api.com/v1/projects/usage/daily?from=2026-09-01&to=2026-09-17&group_by=model" \
-H "Authorization: Bearer $ASSISTANT_API_KEY" \
-H "Aui-User-Id: user_123" \
-H "Aui-Workspace-Id: workspace_123"{
"days": [
{
"day": "2026-09-17",
"provider": null,
"model_id": null,
"runs": 0,
"errors": 0,
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"cost_usd": 0
}
]
}runs counts rows whose kind is run. errors counts rows whose kind is run and whose status is error. Token and cost sums also include rows whose kind is sampling.
| Status | Body | When |
|---|---|---|
200 | A days array. | The usage range was read. |
400 | A schema validation error object. | A query value failed validation. |
400 | { "error": "Usage day range must span at most 366 days" } | The range is negative, greater than 366 days or contains a date that is not a real UTC day. |
401 | An authentication error object. | The authorization header is missing, malformed or expired. |
403 | { "error": "This endpoint requires an API key" } | The request does not use an API key. |
Get the current usage period
GET /v1/projects/usage/periodThis route has no query parameters and no request body. The period is the UTC calendar month from the first day at 00:00 through the first day of the next month at 00:00.
GET /v1/projects/usage/periodcurl https://backend.assistant-api.com/v1/projects/usage/period \
-H "Authorization: Bearer $ASSISTANT_API_KEY" \
-H "Aui-User-Id: user_123" \
-H "Aui-Workspace-Id: workspace_123"{
"plan": "free",
"period_start": "2026-09-01T00:00:00.000Z",
"period_end": "2026-10-01T00:00:00.000Z",
"active_users": 0,
"included": 200,
"cap": 200,
"capped_at": null,
"thread_titles": {
"used": 0,
"completed": 0,
"included": 500,
"cap": 500,
"capped_at": null
}
}| Field | Meaning |
|---|---|
plan | The plan identifier for the project. |
period_start, period_end | The UTC start and exclusive end of the current calendar month. |
active_users | The active user count for the period. It is the greater of the stored period count and the sum of period users by day. |
included, cap | The active user allowance and cap resolved for the project. |
capped_at | The time the active user cap was reached, or null. |
thread_titles | The title usage counters, allowance, cap and cap time for the period. |
| Status | Body | When |
|---|---|---|
200 | The full usage period object. | The project exists and the period was read. |
401 | An authentication error object. | The authorization header is missing, malformed or expired. |
403 | { "error": "This endpoint requires an API key" } | The request does not use an API key. |
404 | { "error": "Project not found" } | The project row does not exist. |
The response reports the current UTC period and does not reserve or consume allowance.
List project threads
GET /v1/projects/threads| Query | Type | Default | Rules |
|---|---|---|---|
is_archived | true or false | false | List archived or unarchived threads. A response does not mix both. |
external_id | string | Return only threads with this external id. | |
limit | integer | 20 | 1 to 100. |
after | thread id | Continue after this thread in the list order. |
The query is the same as the thread list. The route searches every workspace in the project. It returns raw thread table rows, which carry more fields than the thread object.
GET /v1/projects/threads?is_archived=false&limit=20curl "https://backend.assistant-api.com/v1/projects/threads?is_archived=false&limit=20" \
-H "Authorization: Bearer $ASSISTANT_API_KEY" \
-H "Aui-User-Id: user_123" \
-H "Aui-Workspace-Id: workspace_123"{
"threads": [
{
"id": "thread_0qzof3jPoDwr7K3agyJN3D4U",
"project_id": "proj_0ltyjcuaxpv1",
"workspace_id": "workspace_0ouwwTGaKr9lBxsYD5mPSWga",
"created_by": "usr_anon_0ouwwTGaKr9lBxsYD5mPSWga",
"created_at": "2026-09-17T02:40:30.497Z",
"updated_by": "usr_anon_0ouwwTGaKr9lBxsYD5mPSWga",
"updated_at": "2026-09-17T02:40:32.478Z",
"title": "Capital of Portugal and a pastry",
"last_message_at": "2026-09-17T02:40:31.932Z",
"is_archived": false,
"external_id": null,
"metadata": null,
"run_count": 1,
"copied": false,
"stopped": false,
"regenerated": false,
"edited": false,
"cost_usd": "0.0012",
"total_tokens": 42,
"thumbs_up": false,
"thumbs_down": false,
"feedback_positive": 1,
"feedback_negative": 0,
"topic": null,
"task": null,
"review": null,
"review_note": null
}
]
}Each raw row includes created_by, updated_by, copied, stopped, regenerated, edited, thumbs_up and thumbs_down, as well as run_count as an integer counting the conversation's runs, cost_usd as a decimal string summing those runs' cost, and total_tokens as an integer for the input plus output tokens of those runs. feedback_positive and feedback_negative are integers counting thumbs up and down ratings on the thread's messages. topic and task are the names the Intelligence analysis gave the thread, or null; a junk classification has null for both. review is good, needs_work, unclear or null, the latest review a person on the team recorded on the conversation. review_note is that review's note or null. Titles generated by the cloud are not counted in run_count, cost_usd or total_tokens.
Threads use the thread list ordering and after cursor rules. A successful response always has threads, including an empty array when nothing matches.
| Status | Body | When |
|---|---|---|
200 | A threads array. | The project wide read completed. |
400 | A schema validation error object. | A query value failed validation. |
401 | An authentication error object. | The authorization header is missing, malformed or expired. |
403 | { "error": "Project-wide thread listing requires API key authentication" } | The credential is not an API key, or API key access failed. |
List messages in a project thread
GET /v1/projects/threads/{thread_id}/messages| Parameter or query | Type | Required | Default | Rules |
|---|---|---|---|---|
threadId | string | yes | 1 to 255 characters. | |
format | string | no | Stored format | Requests a supported conversion of message content. |
limit | integer | no | 200 | 1 to 200. |
after | message id | no | Continue after this message. |
The route applies the same format conversion as the user message list. Stored aui/v0 rows can convert to ai-sdk/v5 or ai-sdk/v6, and stored ai-sdk/v5 rows can convert to ai-sdk/v6. Other formats are returned as stored. A conversion that cannot parse a row skips that row rather than failing the whole list. When attachment storage is configured, attachment URLs in the content are replaced with signed reads.
GET /v1/projects/threads/thread_0qzof3jPoDwr7K3agyJN3D4U/messages?format=ai-sdk%2Fv6&limit=200curl "https://backend.assistant-api.com/v1/projects/threads/thread_0qzof3jPoDwr7K3agyJN3D4U/messages?format=ai-sdk%2Fv6&limit=200" \
-H "Authorization: Bearer $ASSISTANT_API_KEY" \
-H "Aui-User-Id: user_123" \
-H "Aui-Workspace-Id: workspace_123"{
"messages": [
{
"id": "msg_000000000000000000000000",
"parent_id": null,
"created_at": "2026-09-17T02:40:31.000Z",
"updated_at": "2026-09-17T02:40:31.000Z",
"format": "ai-sdk/v6",
"content": {
"role": "user",
"parts": [{ "type": "text", "text": "Where is order 1042?" }]
},
"height": 0
}
]
}The response contains raw message rows with the requested format and converted content.
| Status | Body | When |
|---|---|---|
200 | A messages array. | The project wide message read completed. |
400 | A schema validation error object. | A parameter or query value failed validation. |
401 | An authentication error object. | The authorization header is missing, malformed or expired. |
403 | { "error": "Project-wide message listing requires API key authentication" } | The request does not use an API key, or API key access failed. |
Format conversion and attachment signing affect the returned representation only; they do not rewrite stored messages.
List project scores
GET /v1/projects/scores| Query | Type | Default | Rules |
|---|---|---|---|
name | string | 1 to 64 characters. | |
thread_id | string | 1 to 48 characters. | |
run_id | string | 1 to 48 characters. | |
since | ISO 8601 datetime | Include scores from this time. | |
limit | integer | 50 | 1 to 200. |
after | score id | 1 to 48 characters. Resolves the score cursor. |
There is no source query filter on this route. Scores are ordered by created_at DESC, id DESC. after resolves the referenced score and continues after its (created_at, id) position in that same order.
GET /v1/projects/scores?name=quality&limit=50curl "https://backend.assistant-api.com/v1/projects/scores?name=quality&limit=50" \
-H "Authorization: Bearer $ASSISTANT_API_KEY" \
-H "Aui-User-Id: user_123" \
-H "Aui-Workspace-Id: workspace_123"{
"scores": [
{
"id": "score_000000000000000000000000",
"thread_id": "thread_0qzof3jPoDwr7K3agyJN3D4U",
"message_id": null,
"run_id": null,
"name": "quality",
"data_type": "numeric",
"value": 0,
"string_value": null,
"source": "api",
"author_id": null,
"comment": null,
"created_at": "2026-09-17T00:00:00.000Z",
"updated_at": "2026-09-17T00:00:00.000Z"
}
]
}Each score contains id, thread_id, message_id, run_id, name, data_type, value, string_value, source, author_id, comment, created_at and updated_at. value is returned as a number or null.
| Status | Body | When |
|---|---|---|
200 | A scores array. | The score read completed. There is no next_cursor field. |
400 | A schema validation error object. | A query value failed validation. |
401 | An authentication error object. | The authorization header is missing, malformed or expired. |
403 | { "error": "Project-wide score listing requires API key authentication" } | The request does not use an API key, or API key access failed. |
List project skills
GET /v1/projects/skillsThis API key only route has no parameters or pagination. It returns only published skills in name order.
GET /v1/projects/skillscurl https://backend.assistant-api.com/v1/projects/skills \
-H "Authorization: Bearer $ASSISTANT_API_KEY" \
-H "Aui-User-Id: user_123" \
-H "Aui-Workspace-Id: workspace_123"{
"skills": [
{
"id": "task_…",
"name": "…",
"description": null,
"instructions": "…",
"task": {
"id": "task_…",
"name": "…"
},
"published_at": "2026-09-17T00:00:00.000Z",
"updated_at": "2026-09-17T00:00:00.000Z"
}
]
}Each item has an id, name, description that is a string or null, instructions, a task object with string id and name, and ISO timestamps in published_at and updated_at.
Publish a skill from the task page in the dashboard: Intelligence, open a task, Skill, then Publish. Update saves later edits and moves updated_at. Unpublish withdraws the skill. Publishing needs no LLM provider or assistant. A newer draft from the analysis never replaces the published text.
Using skills from a backend
Fetch the list at startup or on a timer, and cache it by each skill's updated_at. You can append every skill's instructions under its name to the system prompt. Alternatively, include only each skill's name and description in the prompt, then let the model load one skill's instructions with a tool when it applies.
This example uses the second pattern. The model sees the skill list from systemPrompt(), built for each request so a refresh shows up; loadSkill returns instructions only after it selects a skill.
type Skill = {
id: string;
name: string;
description: string | null;
instructions: string;
task: { id: string; name: string };
published_at: string;
updated_at: string;
};
let skills: Skill[] = [];
const updatedAtById = new Map<string, string>();
async function refreshSkills() {
const response = await fetch("https://backend.assistant-api.com/v1/projects/skills", {
headers: {
Authorization: `Bearer ${process.env.ASSISTANT_API_KEY}`,
"Aui-User-Id": "user_123",
"Aui-Workspace-Id": "workspace_123",
},
});
if (!response.ok) throw new Error("Could not load project skills");
const { skills: fetchedSkills } = (await response.json()) as { skills: Skill[] };
const isCurrent =
fetchedSkills.length === skills.length &&
fetchedSkills.every((skill) => updatedAtById.get(skill.id) === skill.updated_at);
if (isCurrent) return;
skills = fetchedSkills;
updatedAtById.clear();
for (const skill of skills) updatedAtById.set(skill.id, skill.updated_at);
}
await refreshSkills();
setInterval(() => {
refreshSkills().catch((error) => console.error("skill refresh failed", error));
}, 5 * 60_000);
function systemPrompt() {
return [
"Use a skill when it applies.",
"Available skills:",
...skills.map((skill) => `- ${skill.name}: ${skill.description ?? "No description."}`),
].join("\n");
}
const loadSkill = {
name: "load_skill",
description: "Return the published instructions for a listed skill.",
async execute({ name }: { name: string }) {
const skill = skills.find((candidate) => candidate.name === name);
if (!skill) throw new Error(`No published skill named ${name}`);
return skill.instructions;
},
};| Status | Body | When |
|---|---|---|
200 | { "skills": [...] } | The skills were read in name order. |
401 | An authentication error object. | The authorization header is missing, malformed or expired. |
403 | { "error": "Project skills require API key authentication" } | A JWT caller uses this API key only route. |