> ## Documentation Index
> Fetch the complete documentation index at: https://docs.standardagents.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# API

> Create custom API endpoints that operate on specific threads

## What Are Thread API Endpoints?

Thread API endpoints are custom routes that operate on specific conversation threads. They allow you to:

* **Access thread data**: Query messages, logs, and metadata
* **Perform operations**: Export conversations, generate summaries, update state
* **Extend the API**: Add domain-specific functionality beyond built-in routes

> [!NOTE]
>
> Thread API endpoints are file-based and auto-discovered. Create a TypeScript file in `agents/api/` and export your endpoint definition—no manual registration needed.

## Quick Start

Create your first endpoint in `agents/api/status.ts`:

```typescript theme={null}
import { defineThreadEndpoint } from '@standardagents/spec';

export default defineThreadEndpoint(async (req, state) => {
  const { messages, total } = await state.getMessages();

  return Response.json({
    threadId: state.threadId,
    agentId: state.agentId,
    messageCount: total,
  });
});
```

> [!TIP]
>
> Your endpoint is now available at `GET /api/threads/:threadId/status`. The framework auto-discovers it from the `agents/api/` directory.

## Guarding User Endpoints with Application Roles

Hosted projects assign application roles in **Platform → project Settings →
Users**. Platform pushes assignments to the Standard Agents Builder instance; applications
read the local identity without making a Platform request. These roles are
separate from the built-in `admin`, `user`, and `api` access levels and the
`permissions` field. Existing sessions and personal API keys resolve the user's
current replicated assignment.

Use a user endpoint when access belongs to the authenticated user:

```typescript theme={null}
// agents/users/private-report.get.ts
import { defineUserEndpoint } from '@standardagents/spec';

export default defineUserEndpoint(async (_req, user) => {
  const { roles = [] } = await user.identity();
  if (!roles.includes('reports:read')) {
    return Response.json({ error: 'Forbidden' }, { status: 403 });
  }
  return Response.json({ report: await user.getValue('private_report') });
});
```

Role strings are opaque and case-sensitive. Your application can choose a
convention such as allowing either `releases:canary` or `releases:*`, but must
implement that check explicitly. There is no automatic wildcard matching or
administrative bypass. Missing roles grant no application access.

Tools and thread endpoints can inspect `(await state.user())?.roles`; that
identity belongs to the **thread owner**, which may differ from an administrator
addressing the thread. Choose the identity that owns the protected operation.
Check authorization in each request or execution. Identity is memoized within
an execution, so pushed changes take effect on the next request or execution;
they do not cancel work already in flight. Enforce checks on the server rather
than relying on hidden UI controls or client-supplied role lists.

## File-Based Routing

API files map directly to routes based on their filename:

| File | HTTP Method | Route |
| - | - | - |
| `status.ts` | GET | `/api/threads/:threadId/status` |
| `status.get.ts` | GET | `/api/threads/:threadId/status` |
| `export.post.ts` | POST | `/api/threads/:threadId/export` |
| `metadata.put.ts` | PUT | `/api/threads/:threadId/metadata` |
| `archive.delete.ts` | DELETE | `/api/threads/:threadId/archive` |
| `index.ts` | GET | `/api/threads/:threadId` |
| `items/[id].ts` | GET | `/api/threads/:threadId/items/:id` |
| `files/[*].ts` | GET | `/api/threads/:threadId/files/*` |

### Method Suffixes

Append the HTTP method to the filename to specify which method the endpoint handles:

* `.get.ts` — GET requests (default if no suffix)
* `.post.ts` — POST requests
* `.put.ts` — PUT requests
* `.delete.ts` — DELETE requests
* `.patch.ts` — PATCH requests

Files without a method suffix default to GET.

Method suffixes are case-insensitive. Dynamic route segments use `[name]` and are passed to the handler as `params.name`. Catch-all segments use `[*]` and are passed as `params['*']`; the catch-all value may contain `/` when it matches nested paths. Static routes are matched before dynamic routes, and dynamic routes are matched before catch-all routes.

### Nested Routes

Create subdirectories for nested route paths:

```
agents/api/
├── status.ts                    # GET /api/threads/:threadId/status
├── export.post.ts               # POST /api/threads/:threadId/export
└── messages/
    └── count.ts                 # GET /api/threads/:threadId/messages/count
```

## Handler Function

`defineThreadEndpoint` marks the original handler.
The marked value keeps the `(req, state, params, auth)` signature.
A runtime adapter performs these steps:

1. Extracts the thread ID from the URL
2. Looks up the thread.
3. Creates a ThreadState.
4. Handles errors (400 for missing ID, 404 for not found, 500 for exceptions).

### Signature

```typescript theme={null}
declare function defineThreadEndpoint(
  handler: (
    req: Request,
    state: ThreadState,
    params: Record<string, string>,
    auth: ThreadEndpointAuth
  ) => Response | Promise<Response>,
  options?: EndpointOptions
): MarkedThreadEndpoint;
```

### Parameters

**`req`**: Standard Web API [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object

**`state`**: ThreadState exposes thread data and capabilities.
`state.execution` is `null` for endpoint calls.

**`params`**: Route parameters captured from `[name]` and `[*]` path segments.

**`auth`**: `auth.caller` contains the resolved caller or `null`.
`auth.threadAccess` states whether the runtime grants access to this thread.

## ThreadState in Endpoints

The `state` parameter provides access to the thread's data:

### Identity

Access thread identity directly:

```typescript theme={null}
state.threadId     // Thread UUID
state.agentId      // Agent name
state.userId       // Optional user identifier (or null)
state.createdAt    // Creation timestamp
state.arguments    // Invocation or subagent creation arguments
```

### getMessages

Retrieve message history from the thread:

```typescript theme={null}
const { messages, total, hasMore } = await state.getMessages({
  limit: 100,         // Max messages
  offset: 0,          // Skip count
  order: 'desc',      // 'asc' or 'desc'
  includeSilent: false, // Include UI-only messages
  maxDepth: 0         // Max nesting depth (0 = top-level only)
});
```

### File System

Access the thread's file system:

```typescript theme={null}
// Read/write files
const data = await state.readFile('/data/config.json');
await state.writeFile('/output/result.json', JSON.stringify(data), 'application/json');

// List and search
const { entries } = await state.readdirFile('/data');
const { paths } = await state.findFiles('**/*.json');
const matches = await state.grepFiles('error');
```

### Resource Loading

Load agent, prompt, or model definitions:

```typescript theme={null}
const agent = await state.loadAgent('my-agent');
const prompt = await state.loadPrompt('my-prompt');
const model = await state.loadModel('gpt-5.4');
```

## Request Handling

### Reading Request Body

```typescript theme={null}
export default defineThreadEndpoint(async (req, state) => {
  // JSON
  const body = await req.json();

  // Form data
  const formData = await req.formData();

  // Plain text
  const text = await req.text();

  // ...
});
```

### Query Parameters

```typescript theme={null}
export default defineThreadEndpoint(async (req, state) => {
  const url = new URL(req.url);
  const limit = parseInt(url.searchParams.get('limit') || '10');
  const format = url.searchParams.get('format') || 'json';

  const { messages } = await state.getMessages({ limit });
  // ...
});
```

### Headers

```typescript theme={null}
export default defineThreadEndpoint(async (req, state) => {
  const authHeader = req.headers.get('Authorization');

  if (!authHeader) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // ...
});
```

## Error Handling

Standard Agents Builder returns JSON for every failed request under `/api/`.
The response preserves the HTTP status.
The body uses `{ "error": { "code": "internal_error", "message": "..." }, "requestId": "..." }`.
The `X-Request-ID` header matches `requestId`.
Failed responses use `Cache-Control: no-store`.

An uncaught exception returns HTTP 500 with code `internal_error`.
The server log contains the exception and request ID.
The response message omits exception details.
Authentication infrastructure failures return HTTP 503 with code `auth_unavailable`.
Platform network failures, unexpected HTTP statuses, and invalid payloads return HTTP 502 with code `platform_unreachable`.
Check `PLATFORM_ENDPOINT` and platform availability after that error.
An explicit platform credential or membership denial uses code `platform_auth_rejected` during local bootstrap.
Unknown API paths return HTTP 404 with code `not_found`.

Read `error.message` to display a failure.
Include `requestId` in a support report.
Returned legacy JSON errors retain their extra fields and response headers.
The router converts their `error` field to the structured envelope.
A legacy string error with a separate message retains its code in the envelope.
Its separate message becomes the envelope message.
WebSocket upgrades and successful responses retain their existing behavior.
This error contract is a Standard Agents Builder extension.

Handlers can return domain errors with an appropriate HTTP status:

```typescript theme={null}
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
```

Let unexpected exceptions reach the runtime boundary.

## Best Practices

> [accordion] Keep API Handlers Fast
>
> API handlers run in the request path. Keep operations quick:
>
> ```typescript theme={null}
> // Good - paginated query
> const { messages } = await state.getMessages({ limit: 50 });
>
> // Avoid - loading entire history for large threads
> const { messages } = await state.getMessages({ limit: 10000 });
> ```

> [accordion] Handle Errors Gracefully
>
> Always wrap risky operations:
>
> ```typescript theme={null}
> try {
>   const data = await req.json();
>   // Process data...
> } catch (error) {
>   return Response.json(
>     { error: 'Invalid JSON body' },
>     { status: 400 }
>   );
> }
> ```

> [accordion] Validate Input Data
>
> Check request data before processing:
>
> ```typescript theme={null}
> const { format, limit } = await req.json();
>
> if (!['json', 'markdown', 'csv'].includes(format)) {
>   return Response.json(
>     { error: 'Invalid format. Use: json, markdown, or csv' },
>     { status: 400 }
>   );
> }
>
> if (limit && (limit < 1 || limit > 1000)) {
>   return Response.json(
>     { error: 'Limit must be between 1 and 1000' },
>     { status: 400 }
>   );
> }
> ```

> [accordion] Use Descriptive File Names
>
> Name files clearly to indicate their purpose:
>
> ```
> agents/api/
> ├── summary.get.ts      # Clear: GET summary
> ├── export.post.ts      # Clear: POST to export
> ├── analytics.get.ts    # Clear: GET analytics
> └── data.ts             # Unclear: what does this do?
> ```

## Next Steps

> [card] [Thread](https://docs.standardagents.ai/core-concepts/thread.md)
>
> Add custom methods to the Thread class

> [card] [Tools](https://docs.standardagents.ai/core-concepts/tools.md)
>
> Create custom tools that endpoints can leverage

> [card] [Hooks](https://docs.standardagents.ai/core-concepts/hooks.md)
>
> Intercept and modify data at lifecycle points

> [card] [REST API](https://docs.standardagents.ai/api-reference/rest/get-messages.md)
>
> Explore the built-in API endpoints


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.