> ## Documentation Index
> Fetch the complete documentation index at: https://docs.standardagentbuilder.com/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.
</Note>

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

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

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

The `defineThreadEndpoint` function wraps your handler and automatically:

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

### Signature

```typescript theme={null}
defineThreadEndpoint(
  handler: (req: Request, state: ThreadState, params: Record<string, string>) => Response | Promise<Response>
)
```

### Parameters

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

**`state`** — ThreadState providing access to thread identity, messages, file system, and more. Note: `state.execution` is always `null` in endpoints since the thread is at rest.

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

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

Wrap operations in try/catch for robust error handling:

```typescript theme={null}
export default defineThreadEndpoint(async (req, state) => {
  try {
    const { messages } = await state.getMessages();

    // Process messages...

    return Response.json({ success: true, data: result });
  } catch (error) {
    console.error('Endpoint error:', error);

    return Response.json(
      { error: error.message || 'Internal server error' },
      { status: 500 }
    );
  }
});
```

<Info>
  The framework automatically handles common errors like missing thread IDs (400) and threads not found (404). Your handler only needs to handle domain-specific errors.
</Info>

## Best Practices

<AccordionGroup>
  <Accordion title="Keep API Handlers Fast" icon="bolt">
    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>

  <Accordion title="Handle Errors Gracefully" icon="shield">
    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>

  <Accordion title="Validate Input Data" icon="check">
    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>

  <Accordion title="Use Descriptive File Names" icon="file">
    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?
    ```
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Thread" icon="database" href="/core-concepts/thread">
    Add custom methods to the Thread class
  </Card>

  <Card title="Tools" icon="wrench" href="/core-concepts/tools">
    Create custom tools that endpoints can leverage
  </Card>

  <Card title="Hooks" icon="webhook" href="/core-concepts/hooks">
    Intercept and modify data at lifecycle points
  </Card>

  <Card title="REST API" icon="code" href="/api-reference/rest/get-messages">
    Explore the built-in API endpoints
  </Card>
</CardGroup>
