# 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
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,
  });
});
```

> **Check:**
>
> 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](/docs/core-concepts/users) when access belongs to the authenticated user:

```typescript
// 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.
`state.user()` reads the owner's current local identity on each call.
A user endpoint's `user.identity()` lookup is memoized for that execution.
A role change does not cancel work in flight.
Enforce checks on the server.
Treat client-supplied role lists as untrusted input.

## 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
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.

Endpoints default to `{ access: 'authenticated' }`. The runtime verifies the caller and thread access before it calls your handler. Use `auth.caller` for the request's identity. `state.user()` identifies the thread owner.

With `{ access: 'public' }`, the runtime can call your handler for an anonymous caller. Inspect `auth.threadAccess` before you return protected thread data.

## ThreadState in Endpoints

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

### Identity

Access thread identity directly:

```typescript
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
const { messages, total, hasMore } = await state.getMessages({
  limit: 100,         // Max messages
  offset: 0,          // Skip count
  order: 'desc',      // 'asc' or 'desc'
  includeSilent: false, // Exclude messages hidden from the UI
  maxDepth: 0         // Max nesting depth (0 = top-level only)
});
```

### File System

Access the thread's file system:

```typescript
// Read/write files
const data = await state.readFile('/data/config.json');
if (data) {
  const config = JSON.parse(new TextDecoder().decode(data));
  await state.writeFile('/output/result.json', JSON.stringify(config), '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
const agent = await state.loadAgent('my-agent');
const prompt = await state.loadPrompt('my-prompt');
const model = await state.loadModel('tool-reasoning');
```

## Request Handling

### Reading Request Body

```typescript
export default defineThreadEndpoint(async (req, state) => {
  // Read the body once with the method for your content type.
  const body = await req.json();

  // For form data, use await req.formData() instead.
  // For plain text, use await req.text() instead.

  // ...
});
```

### Query Parameters

```typescript
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
export default defineThreadEndpoint(async (req, state, _params, auth) => {
  const requestId = req.headers.get('X-Request-ID');
  return Response.json({
    requestId,
    threadId: state.threadId,
    callerId: auth.caller?.id,
    threadAccess: auth.threadAccess,
  });
});
```

## 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
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
```

Let unexpected exceptions reach the runtime boundary.

## Best Practices

### Keep API Handlers Fast

API handlers run in the request path. Keep operations quick:

```typescript
// Good - paginated query
const { messages } = await state.getMessages({ limit: 50 });

// Avoid - loading entire history for large threads
const { messages } = await state.getMessages({ limit: 10000 });
```

### Handle Errors Gracefully

Always wrap risky operations:

```typescript
try {
  const data = await req.json();
  // Process data...
} catch (error) {
  return Response.json(
    { error: 'Invalid JSON body' },
    { status: 400 }
  );
}
```

### Validate Input Data

Check request data before processing:

```typescript
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 }
  );
}
```

### 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

- [Thread](/docs/core-concepts/thread)

  Add custom methods to the Thread class

* [Tools](/docs/core-concepts/tools)

  Create custom tools that endpoints can leverage

- [Hooks](/docs/core-concepts/hooks)

  Intercept and modify data at lifecycle points

* [REST API](/docs/api-reference/rest/get-messages)

  Explore the built-in API endpoints
