Base URL: https://agentstate.app/api
Backward-compatible base: https://agentstate.app. /v1/* is not a general alias for
/api/v1/* — it's a frozen legacy surface mounted for exactly four resources: conversations,
AI features (title/follow-ups), tags, and public analytics (packages/api/src/index.ts). It
predates projects and the state/lease/claim primitives, so /v1/projects, /v1/states, /v1/keys,
etc. were never mounted and never will be — always use /api/v1/* for anything outside those
four resources.
The API provides improved performance, consistency, and developer experience:
- Snake_case field names: All request and response fields use snake_case
- Timestamps in milliseconds: All timestamps are Unix milliseconds (
Date.now()format) - Cursor-based pagination: Efficient pagination with
next_cursorandtotalcount - Field selection: Messages are included by default on Get Conversation; use
?fields=to select specific fields or?fields=!messagesto exclude the messages array - HTTP method semantics: PUT to replace a conversation's title/metadata, PATCH for partial updates elsewhere (e.g. projects), proper status codes
- Field names: snake_case in all request and response bodies.
- IDs: nanoid, 21 characters (e.g.,
V1StGXR8_Z5jdHi6B-myT). - Timestamps: Unix milliseconds (
Date.now()format), stored as integers. - Pagination: Cursor-based. Never offset-based. Pass
cursorto get the next page; whennext_cursorisnull, there are no more results. Includestotalcount in pagination metadata. - Metadata: Arbitrary JSON object. Stored as serialized TEXT, parsed on read.
Nearly every endpoint requires authentication. Conversations, tags, messages, search, analytics,
AI features, webhooks, API keys, states, leases, claims, and capability tokens all require a
valid API key (optionally scoped — see Permissions & scopes). Projects,
organizations, custom domains, and dashboard-only analytics/traces instead require a Clerk
session (they're called from the dashboard frontend, not with an API key). The only unauthenticated
routes are the health check, static agent-readable content (llms.txt, agents.md, openapi.json),
OAuth discovery, and domain verification.
Pass the key as a Bearer token:
Authorization: Bearer as_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Key format: as_live_ prefix followed by 40 base62 characters.
Only the SHA-256 hash of the key is stored. The raw key is shown once at creation time.
Requests with a missing, malformed, or revoked key receive:
{ "error": { "code": "UNAUTHORIZED", "message": "Invalid API key" } }Status: 401
All authenticated endpoints enforce a fixed-window rate limit per API key.
| Parameter | Value |
|---|---|
| Limit | 100 requests per minute |
| Window | 60 seconds (UTC-minute boundary) |
Every response includes rate-limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Max requests per window (100) |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Unix timestamp (seconds) when the window resets |
Retry-After |
Seconds until the window resets (only on 429) |
When exceeded:
{ "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded. Maximum 100 requests per minute. Retry after N seconds." } }Status: 429
Every response includes an X-Request-Id header. You can pass your own X-Request-Id on the request (alphanumeric, hyphens, underscores, 1-64 chars); otherwise the server generates one. Use this value when reporting issues.
All errors follow a consistent structure:
{
"error": {
"code": "MACHINE_CODE",
"message": "Human-readable description"
}
}| Code | HTTP Status | Description |
|---|---|---|
BAD_REQUEST |
400 | Invalid JSON body or validation failure |
UNAUTHORIZED |
401 | Missing or invalid API key |
FORBIDDEN |
403 | Key valid but not authorized for this resource |
NOT_FOUND |
404 | Resource does not exist |
CONFLICT |
409 | Duplicate resource (e.g., external_id already exists) |
RATE_LIMITED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Unexpected server error |
Several endpoints use caching to improve performance and reduce database load. Cached responses may be served for a period of time after the initial request.
API key lookups are cached for 60 seconds. This reduces database load on frequently-used keys while maintaining near-real-time updates when keys are created or revoked.
Analytics endpoints use aggressive caching with time-based TTLs:
| Time Range | Cache TTL |
|---|---|
| 1-7 days | 60 seconds |
| 8-30 days | 180 seconds |
| 30+ days | 300 seconds |
This applies to:
GET /v1/analytics/summaryGET /v1/analytics/timeseriesGET /v1/analytics/tagsGET /v1/conversations/:id/analyticsGET /api/v1/projects/:id/analytics
Cached responses may reflect data from up to the TTL ago. For most use cases, this delay is acceptable and provides significant performance benefits:
- Reduced database load: Fewer queries for expensive aggregations
- Faster responses: Cached data is served directly from memory
- Lower latency: Especially beneficial for analytics over large time ranges
If you need absolutely real-time data, consider:
- Using shorter time ranges for analytics queries (lower TTL)
- Making individual conversation fetches instead of aggregate analytics
- Field names: snake_case in all request and response bodies.
- IDs: nanoid, 21 characters (e.g.,
V1StGXR8_Z5jdHi6B-myT). - Timestamps: Unix milliseconds (
Date.now()format), stored as integers. - Pagination: Cursor-based. Never offset-based. Pass
cursorto get the next page; whennext_cursorisnull, there are no more results. - Metadata: Arbitrary JSON object. Stored as serialized TEXT, parsed on read.
Endpoints are located at /api/v1/*. Most require API key authentication; dashboard-management
routes (projects, organizations, domains, dashboard analytics/traces) require a Clerk session
instead. See Authentication above for the full breakdown.
Pass the key as a Bearer token:
Authorization: Bearer as_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Key format: as_live_ prefix followed by 40 base62 characters.
Only the SHA-256 hash of the key is stored. The raw key is shown once at creation time.
All endpoints enforce a fixed-window rate limit per API key.
| Parameter | Value |
|---|---|
| Limit | 100 requests per minute |
| Window | 60 seconds (UTC-minute boundary) |
Every response includes rate-limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Max requests per window (100) |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Unix timestamp (seconds) when the window resets |
Retry-After |
Seconds until the window resets (only on 429) |
All errors follow a consistent structure:
{
"error": {
"code": "MACHINE_CODE",
"message": "Human-readable description"
}
}| Code | HTTP Status | Description |
|---|---|---|
BAD_REQUEST |
400 | Invalid JSON body or validation failure |
INVALID_CURSOR |
400 | Invalid cursor parameter |
UNAUTHORIZED |
401 | Missing or invalid API key |
FORBIDDEN |
403 | Key valid but not authorized for this resource |
NOT_FOUND |
404 | Resource does not exist |
CONFLICT |
409 | Duplicate resource (e.g., external_id already exists) |
RATE_LIMITED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Unexpected server error |
All conversation endpoints require API key authentication.
POST /api/v1/conversations
Creates a new conversation, optionally with initial messages.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
external_id |
string | No | Caller-provided identifier. Must be unique within the project. |
title |
string | No | Conversation title. |
metadata |
object | No | Arbitrary key-value pairs. |
messages |
array | No | Initial messages to attach. |
Each message object:
| Field | Type | Required | Description |
|---|---|---|---|
role |
string | Yes | One of: system, user, assistant, tool. |
content |
string | Yes | Message content (min 1 character). |
metadata |
object | No | Arbitrary key-value pairs. |
token_count |
integer | No | Non-negative integer. Defaults to 0. |
Response: 201 Created
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": null,
"metadata": null,
"message_count": 1,
"token_count": 0,
"total_cost_microdollars": 0,
"total_tokens": 0,
"created_at": 1710000000000,
"updated_at": 1710000000000,
"messages": [
{
"id": "xYz789_AbCdEfGhIjKlM",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 0,
"created_at": 1710000000000
}
]
}The messages array reflects whatever was passed in the request body (empty if none were sent).
Errors:
400 BAD_REQUEST-- Invalid body or validation failure.409 CONFLICT-- A conversation with the givenexternal_idalready exists.
GET /api/v1/conversations
Returns conversations for the authenticated project, ordered by updated_at.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 | Results per page (1-100). |
cursor |
string | -- | updated_at timestamp from next_cursor of a previous response. |
order |
string | desc |
Sort direction: asc or desc. |
tag |
string | -- | Filter to conversations that have this tag. |
Response: 200 OK
{
"data": [
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": "My conversation",
"metadata": null,
"message_count": 5,
"token_count": 120,
"total_cost_microdollars": 0,
"total_tokens": 120,
"created_at": 1710000000000,
"updated_at": 1710000000000
}
],
"pagination": {
"limit": 50,
"next_cursor": "1710000000000",
"total": 150
}
}V2 Changes:
- Includes
totalcount in pagination metadata
next_cursor is null when there are no more pages.
GET /api/v1/conversations/:id
Returns a single conversation. Messages are included by default.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
fields |
string | all | Comma-separated field names to include. Special value !messages excludes the messages array. |
Valid fields: id, project_id, external_id, title, metadata, message_count, token_count, total_cost_microdollars, total_tokens, created_at, updated_at, messages
Examples:
# Get all fields, including messages (default)
GET /api/v1/conversations/:id
# Get only conversation metadata (no messages)
GET /api/v1/conversations/:id?fields=id,title,updated_at
# Exclude messages array, keep everything else
GET /api/v1/conversations/:id?fields=!messagesResponse: 200 OK
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": "My conversation",
"metadata": null,
"message_count": 2,
"token_count": 50,
"total_cost_microdollars": 0,
"total_tokens": 50,
"created_at": 1710000000000,
"updated_at": 1710000000000,
"messages": [
{
"id": "msg_001",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 5,
"created_at": 1710000000000
}
]
}Errors:
404 NOT_FOUND-- Conversation not found.
GET /api/v1/conversations/by-external-id/:externalId
Find a conversation by the caller-provided external_id. Returns the conversation with all messages, same as Get Conversation (supports the same fields parameter).
Response shape is identical to Get Conversation.
Errors: 404 NOT_FOUND
PUT /api/v1/conversations/:id
Update a conversation's title and/or metadata.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | No | New title. |
metadata |
object | No | Replaces existing metadata entirely. |
Response: 200 OK
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": "Updated title",
"metadata": { "key": "value" },
"message_count": 2,
"token_count": 50,
"total_cost_microdollars": 0,
"total_tokens": 50,
"created_at": 1710000000000,
"updated_at": 1710000080000
}Errors: 404 NOT_FOUND
DELETE /api/v1/conversations/:id
Deletes a conversation and all its messages.
Response: 204 No Content
Errors: 404 NOT_FOUND
Manage messages within an existing conversation. All endpoints require API key authentication.
POST /api/v1/conversations/:id/messages
Add one or more messages to an existing conversation. Automatically updates the conversation's message_count, token_count, and updated_at.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
messages |
array | Yes | At least 1 message. Same schema as Create Conversation messages. |
Response: 201 Created
{
"messages": [
{
"id": "xYz789_AbCdEfGhIjKlM",
"role": "assistant",
"content": "Hi there!",
"metadata": null,
"token_count": 10,
"created_at": 1710000000000
}
]
}Errors: 404 NOT_FOUND -- Conversation does not exist.
GET /api/v1/conversations/:id/messages
Paginate through messages in chronological order.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 100 | Results per page (1-500). |
after |
string | -- | Message ID to start after (cursor). |
Response: 200 OK
{
"data": [
{
"id": "msg_001",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 5,
"created_at": 1710000000000
}
],
"pagination": {
"limit": 100,
"next_cursor": "msg_001"
}
}next_cursor is the ID of the last message in the page. Pass it as the after parameter for the next page. null when there are no more results.
Errors: 404 NOT_FOUND -- Conversation does not exist.
Analytics endpoints for project-wide statistics. All endpoints require API key authentication and use caching for performance.
GET /api/v1/analytics/summary
Returns summary statistics for a project, including total conversations, messages, tokens, and averages over a specified time period. Supports tag filtering.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
start |
integer | — | Start timestamp (unix ms, inclusive). Defaults to 30 days ago. |
end |
integer | — | End timestamp (unix ms, exclusive). Defaults to now. |
tag |
string | — | Filter to conversations with this tag (can specify multiple times). |
Response: 200 OK
{
"project_id": "proj_abc123",
"total_conversations": 150,
"total_messages": 2250,
"total_tokens": 180000,
"avg_messages_per_conversation": 15.0,
"avg_tokens_per_conversation": 1200.0,
"period": {
"start": 1704067200000,
"end": 1704153600000
}
}V2 Changes:
- Includes
project_idin response - No
project_idquery parameter required (derived from API key)
Caching:
- 1-7 day ranges: 60 seconds
- 8-30 day ranges: 180 seconds
- 30+ day ranges: 300 seconds
GET /api/v1/analytics/timeseries
Returns time-series data for conversations, messages, or tokens grouped by the specified granularity.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
metric |
string | "conversations" | Metric to aggregate: conversations, messages, or tokens. |
granularity |
string | "day" | Time grouping: day, week, or month. |
start |
integer | — | Start timestamp (unix ms, inclusive). Defaults to 30 days ago. |
end |
integer | — | End timestamp (unix ms, exclusive). Defaults to now. |
tag |
string | — | Filter to conversations with this tag (can specify multiple times). |
Response: 200 OK
{
"project_id": "proj_abc123",
"metric": "conversations",
"granularity": "day",
"period": {
"start": 1704067200000,
"end": 1704153600000
},
"data": [
{"bucket": "2024-01-01", "value": 15},
{"bucket": "2024-01-02", "value": 18},
{"bucket": "2024-01-03", "value": 12}
]
}V2 Changes:
- Includes
project_idin response - No
project_idquery parameter required
Caching: Same as summary endpoint.
GET /api/v1/analytics/tags
Returns usage statistics for tags within a specified time period.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
start |
integer | — | Start timestamp (unix ms, inclusive). Defaults to 30 days ago. |
end |
integer | — | End timestamp (unix ms, exclusive). Defaults to now. |
limit |
integer | 50 | Maximum number of tags to return (1-200). |
Response: 200 OK
{
"project_id": "proj_abc123",
"period": {
"start": 1704067200000,
"end": 1704153600000
},
"data": [
{
"tag": "support",
"conversation_count": 45,
"message_count": 675,
"token_count": 54000
},
{
"tag": "sales",
"conversation_count": 32,
"message_count": 480,
"token_count": 38400
}
]
}V2 Changes:
- Includes
project_idin response conversation_countinstead ofcount(clearer naming)
Caching: Same as summary endpoint.
Project management endpoints. All endpoints require API key authentication.
POST /api/v1/projects
Creates a new project and auto-generates a default API key.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Project display name. |
slug |
string | Yes | URL-safe identifier. Lowercase alphanumeric and hyphens only. Must be unique within the organization. |
org_id |
string | No | Clerk organization ID. Defaults to "default". |
Response: 201 Created
{
"project": {
"project_id": "proj_abc123",
"org_id": "org_xyz",
"name": "My Project",
"slug": "my-project",
"created_at": 1710000000000,
"updated_at": 1710000000000
},
"api_key": {
"id": "key_abc123",
"name": "Default",
"key_prefix": "as_live_xxxx",
"key": "as_live_full_key_shown_only_once",
"created_at": 1710000000000
}
}V2 Changes:
- Includes
updated_atin project response - Field names use snake_case (
project_id,org_id)
Errors:
400 BAD_REQUEST-- Validation failure.409 CONFLICT-- Slug already taken in this organization.
GET /api/v1/projects
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
org_id |
string | "default" |
Clerk organization ID to filter by. |
limit |
integer | 50 | Results per page (1-100). |
cursor |
string | -- | created_at timestamp from next_cursor of a previous response. |
Response: 200 OK
{
"data": [
{
"project_id": "proj_abc123",
"org_id": "org_xyz",
"name": "My Project",
"slug": "my-project",
"created_at": 1710000000000,
"key_count": 2
}
],
"pagination": {
"limit": 50,
"next_cursor": "1710000000000",
"total": 10
}
}V2 Changes:
- Includes
totalcount in pagination - Snake_case field names
GET /api/v1/projects/:id
Returns the project with its API keys.
Response: 200 OK
{
"project_id": "proj_abc123",
"org_id": "org_xyz",
"name": "My Project",
"slug": "my-project",
"created_at": 1710000000000,
"api_keys": [
{
"id": "key_abc123",
"name": "Default",
"key_prefix": "as_live_xxxx",
"created_at": 1710000000000,
"last_used_at": 1710000000000,
"revoked_at": null
}
]
}V2 Changes:
api_keysinstead ofapiKeys- Snake_case field names
Errors: 404 NOT_FOUND
PATCH /api/v1/projects/:id
Update a project's name.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | New project name. |
Response: 200 OK
{
"project_id": "proj_abc123",
"org_id": "org_xyz",
"name": "Updated Project Name",
"slug": "my-project",
"created_at": 1710000000000,
"updated_at": 1710000080000,
"api_keys": [...]
}V2 Changes:
- Uses
PATCHinstead ofPUT - Includes
updated_atin response
Errors: 404 NOT_FOUND
DELETE /api/v1/projects/:id
Permanently deletes a project and all associated data, including:
- All conversations in the project
- All messages in those conversations
- All API keys for the project
- All tags on conversations
Caution: This operation is irreversible. All data will be permanently deleted.
Response: 204 No Content
Errors:
404 NOT_FOUND— Project not found
API key management endpoints. All endpoints require API key authentication.
POST /api/v1/keys
Creates a new API key for the authenticated project. The project is taken from the authenticating key.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Key display name (1-255 characters). |
scopes |
string[] | No | Permission scopes for the new key. Omit for full access. Must be a subset of the authenticating key's scopes. See Permissions & scopes. |
Response: 201 Created
{
"key_id": "key_abc123",
"project_id": "proj_abc123",
"name": "Production",
"key_prefix": "as_live_xxxx",
"key": "as_live_full_key_shown_only_once",
"scopes": ["conversations:read", "conversations:write"],
"created_at": 1710000000000,
"last_used_at": null,
"revoked_at": null
}The key field contains the full API key and is only returned at creation time. Store it securely.
The scopes field is null for full-access keys (created without a scopes array, and any legacy key).
V2 Changes:
key_idinstead ofid- Includes
project_idin response
Errors:
400 BAD_REQUEST-- Validation failure.403 FORBIDDEN-- Requestedscopesare not a subset of the authenticating key's scopes.
Errors:
400 BAD_REQUEST-- Validation failure.
GET /api/v1/keys
Lists all API keys for the authenticated project (including revoked ones). The project is taken from the authenticating key.
Response: 200 OK
{
"data": [
{
"key_id": "key_abc123",
"project_id": "proj_abc123",
"name": "Default",
"key_prefix": "as_live_xxxx",
"scopes": null,
"created_at": 1710000000000,
"last_used_at": 1710000000000,
"revoked_at": null
}
]
}The full key is never returned after creation. scopes is null for full-access keys.
V2 Changes:
key_idinstead ofid- Includes
project_idin each key object - Includes
scopes(string[]ornull)
DELETE /api/v1/keys/:id
Soft-deletes an API key by setting revoked_at. The key immediately becomes invalid for authentication.
Response: 204 No Content
Errors:
404 NOT_FOUND-- API key not found.
API keys, capability tokens, and OAuth access tokens carry scopes that limit which
endpoints they can call. A key created without a scopes array — and any legacy key — has
full access (scopes is null).
Available scopes: conversations:read, conversations:write, state:read, state:write,
state:watch, lease:write, claim:write, analytics:read, webhooks:write,
domains:write, keys:read, keys:write. The * wildcard grants full access and
per-resource wildcards like state:* cover all actions on a resource.
A key can only mint child keys whose scopes are a subset of its own. Out-of-scope requests
return 403 FORBIDDEN. Dashboard key creation (Clerk session) may grant any scopes.
See the Permissions & Scopes guide for the full taxonomy, the delegation rule, and examples.
There is no "V2" API and no live migration in progress — /api/v1/* (with a deprecated /v1/*
alias for conversations, tags, and public analytics) is the only API and always has been. A
parallel "V2" convention set was drafted at one point (a PATCH update method, ?include=messages,
pagination.total everywhere, etc.) but most of those proposed changes were never adopted. See
docs/v2-migration.md for the full historical record of what was proposed
versus what actually shipped.
Deprecated: Legacy V1 endpoints (
/v1/*without/api/prefix) are deprecated and will be removed on 2027-01-01. Please use the current/api/v1/*endpoints documented above.
All conversation endpoints require API key authentication.
POST /v1/conversations
Creates a new conversation, optionally with initial messages.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
external_id |
string | No | Caller-provided identifier. Must be unique within the project. |
title |
string | No | Conversation title. |
metadata |
object | No | Arbitrary key-value pairs. |
messages |
array | No | Initial messages to attach. |
Each message object:
| Field | Type | Required | Description |
|---|---|---|---|
role |
string | Yes | One of: system, user, assistant, tool. |
content |
string | Yes | Message content (min 1 character). |
metadata |
object | No | Arbitrary key-value pairs. |
token_count |
integer | No | Non-negative integer. Defaults to 0. |
Response: 201 Created
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": null,
"metadata": null,
"message_count": 1,
"token_count": 0,
"total_cost_microdollars": 0,
"total_tokens": 0,
"created_at": 1710000000000,
"updated_at": 1710000000000,
"messages": [
{
"id": "xYz789_AbCdEfGhIjKlM",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 0,
"created_at": 1710000000000
}
]
}Errors:
400 BAD_REQUEST-- Invalid body or validation failure.409 CONFLICT-- A conversation with the givenexternal_idalready exists.
GET /v1/conversations
Returns conversations for the authenticated project, ordered by updated_at.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 | Results per page (1-100). |
cursor |
string | -- | updated_at timestamp from next_cursor of a previous response. |
order |
string | desc |
Sort direction: asc or desc. |
tag |
string | -- | Filter to conversations that have this tag. |
Response: 200 OK
{
"data": [
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": "My conversation",
"metadata": null,
"message_count": 5,
"token_count": 120,
"total_cost_microdollars": 0,
"total_tokens": 120,
"created_at": 1710000000000,
"updated_at": 1710000000000
}
],
"pagination": {
"limit": 50,
"next_cursor": "1710000000000"
}
}next_cursor is null when there are no more pages.
GET /v1/conversations/:id
Returns a single conversation with all its messages.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
fields |
string | all | Comma-separated field names to include. Special: !messages excludes messages array. |
Valid fields: id, project_id, external_id, title, metadata, message_count, token_count, total_cost_microdollars, total_tokens, created_at, updated_at, messages
Examples:
# Get only conversation metadata (no messages)
GET /v1/conversations/:id?fields=id,title,updated_at
# Exclude messages array
GET /v1/conversations/:id?fields=!messages
# Get all fields (default behavior)
GET /v1/conversations/:idPerformance: When messages is excluded, the database query for messages is skipped entirely, significantly reducing response time and bandwidth for conversations with many messages.
Response: 200 OK
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": "My conversation",
"metadata": null,
"message_count": 2,
"token_count": 50,
"total_cost_microdollars": 0,
"total_tokens": 50,
"created_at": 1710000000000,
"updated_at": 1710000000000,
"messages": [
{
"id": "msg_001",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 5,
"created_at": 1710000000000
}
]
}Errors:
400 BAD_REQUEST-- Invalid field name specified.404 NOT_FOUND-- Conversation not found.
GET /v1/conversations/by-external-id/:externalId
Find a conversation by the caller-provided external_id. Returns the conversation with all messages.
Response shape is identical to Get Conversation.
Errors: 404 NOT_FOUND
GET /v1/conversations/search
Full-text search across message content. Returns matching conversations with a text snippet showing the match context.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
q |
string | -- | Required. Search query. |
limit |
integer | 20 | Results per page (1-100). |
cursor |
string | -- | updated_at timestamp from next_cursor of a previous response. |
Response: 200 OK
{
"data": [
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"title": "My conversation",
"snippet": "...matching text around the query...",
"message_count": 5,
"created_at": 1710000000000,
"updated_at": 1710000000000
}
],
"next_cursor": null
}Errors: 400 BAD_REQUEST -- Missing or empty q parameter.
GET /v1/conversations/:id/analytics
Returns analytics data for a specific conversation, including message counts, token counts, role breakdown, tag usage, and duration.
Response: 200 OK
{
"conversation_id": "V1StGXR8_Z5jdHi6B-myT",
"title": "My conversation",
"message_count": 15,
"token_count": 1250,
"tags": ["support", "sales"],
"duration_ms": 1800000,
"messages_by_role": {
"user": {
"count": 10,
"tokens": 850
},
"assistant": {
"count": 5,
"tokens": 400
}
},
"created_at": 1710000000000,
"updated_at": 1710086400000
}Fields:
| Field | Type | Description |
|---|---|---|
conversation_id |
string | Conversation ID |
title |
string | Conversation title |
message_count |
integer | Total number of messages |
token_count |
integer | Total tokens across all messages |
tags |
array of strings | Tags attached to this conversation |
duration_ms |
integer | Conversation duration in milliseconds |
messages_by_role |
object | Breakdown of messages and tokens by role |
created_at |
integer | Creation timestamp (unix ms) |
updated_at |
integer | Last update timestamp (unix ms) |
Errors:
404 NOT_FOUND— Conversation not found
PUT /v1/conversations/:id
Update a conversation's title and/or metadata.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | No | New title. |
metadata |
object | No | Replaces existing metadata entirely. |
Response: 200 OK
Returns the updated conversation object (same shape as list items, without messages).
Errors: 404 NOT_FOUND
DELETE /v1/conversations/:id
Deletes a conversation and all its messages.
Response: 204 No Content
Errors: 404 NOT_FOUND
POST /v1/conversations/bulk-delete
Delete multiple conversations and their messages in a single request.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] | Yes | Conversation IDs to delete (1-100 items). |
Response: 200 OK
{ "deleted": 3 }The deleted count reflects only conversations that existed and belonged to the authenticated project.
POST /v1/conversations/export
Export conversations with their full message history.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] | No | Specific conversation IDs to export (max 100). Omit to export the most recent 100. |
Response: 200 OK
{
"data": [
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": "My conversation",
"metadata": null,
"message_count": 2,
"token_count": 50,
"total_cost_microdollars": 0,
"total_tokens": 50,
"created_at": 1710000000000,
"updated_at": 1710000000000,
"messages": [
{
"id": "msg_001",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 5,
"created_at": 1710000000000
}
]
}
],
"count": 1
}Messages are capped at 5000 total across all exported conversations.
Manage messages within an existing conversation. All endpoints require API key authentication.
POST /v1/conversations/:id/messages
Add one or more messages to an existing conversation. Automatically updates the conversation's message_count, token_count, and updated_at.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
messages |
array | Yes | At least 1 message. Same schema as Create Conversation messages. |
Response: 201 Created
{
"messages": [
{
"id": "xYz789_AbCdEfGhIjKlM",
"role": "assistant",
"content": "Hi there!",
"metadata": null,
"token_count": 10,
"created_at": 1710000000000
}
]
}Errors: 404 NOT_FOUND -- Conversation does not exist.
GET /v1/conversations/:id/messages
Paginate through messages in chronological order.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 100 | Results per page (1-500). |
after |
string | -- | Message ID to start after (cursor). |
Response: 200 OK
{
"data": [
{
"id": "msg_001",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 5,
"created_at": 1710000000000
}
],
"pagination": {
"limit": 100,
"next_cursor": "msg_001"
}
}next_cursor is the ID of the last message in the page. Pass it as the after parameter for the next page. null when there are no more results.
Errors: 404 NOT_FOUND -- Conversation does not exist.
All tag endpoints require API key authentication.
GET /v1/tags
Returns all unique tags used across the authenticated project's conversations.
Response: 200 OK
{
"data": {
"tags": ["bug", "feature", "support"]
}
}GET /v1/conversations/:id/tags
Returns tags attached to a specific conversation.
Response: 200 OK
{
"data": {
"tags": ["bug", "urgent"]
}
}Errors: 404 NOT_FOUND
POST /v1/conversations/:id/tags
Add tags to a conversation. Duplicate tags are silently ignored.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
tags |
string[] | Yes | Tag names (1-50 items, each 1-64 characters). |
Response: 201 Created
Returns the full list of tags now on the conversation (including previously existing ones):
{
"data": {
"tags": ["bug", "feature", "urgent"]
}
}Errors: 404 NOT_FOUND
DELETE /v1/conversations/:id/tags/:tag
Remove a single tag from a conversation. No error if the tag was not present.
Response: 204 No Content
Errors: 404 NOT_FOUND -- Conversation does not exist.
Public analytics endpoints for project-wide statistics. All endpoints require API key authentication.
GET /v1/analytics/summary
Returns summary statistics for a project, including total conversations, messages, tokens, and averages over a specified time period. Supports tag filtering.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
project_id |
string | — | Required. Project ID (from API key) |
start |
integer | — | Required. Start timestamp (unix ms, inclusive) |
end |
integer | — | Required. End timestamp (unix ms, exclusive) |
tag |
string | — | Filter to conversations with this tag (can specify multiple times) |
Response: 200 OK
{
"total_conversations": 150,
"total_messages": 2250,
"total_tokens": 180000,
"avg_messages_per_conversation": 15.0,
"avg_tokens_per_conversation": 1200.0,
"period": {
"start": 1704067200000,
"end": 1704153600000
}
}GET /v1/analytics/timeseries
Returns time-series data for conversations, messages, or tokens grouped by the specified granularity.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
project_id |
string | — | Required. Project ID (from API key) |
metric |
string | "conversations" | Metric to aggregate: conversations, messages, or tokens |
granularity |
string | "day" | Time grouping: day, week, or month |
start |
integer | — | Required. Start timestamp (unix ms, inclusive) |
end |
integer | — | Required. End timestamp (unix ms, exclusive) |
tag |
string | — | Filter to conversations with this tag (can specify multiple times) |
Response: 200 OK
{
"metric": "conversations",
"granularity": "day",
"period": {
"start": 1704067200000,
"end": 1704153600000
},
"data": [
{"bucket": "2024-01-01", "value": 15},
{"bucket": "2024-01-02", "value": 18},
{"bucket": "2024-01-03", "value": 12}
]
}GET /v1/analytics/tags
Returns usage statistics for tags within a specified time period.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
project_id |
string | — | Required. Project ID (from API key) |
start |
integer | — | Required. Start timestamp (unix ms, inclusive) |
end |
integer | — | Required. End timestamp (unix ms, exclusive) |
limit |
integer | 50 | Maximum number of tags to return (1-200) |
Response: 200 OK
{
"period": {
"start": 1704067200000,
"end": 1704153600000
},
"data": [
{"tag": "support", "conversation_count": 45, "message_count": 675, "token_count": 54000},
{"tag": "sales", "conversation_count": 32, "message_count": 480, "token_count": 38400},
{"tag": "feedback", "conversation_count": 18, "message_count": 270, "token_count": 21600}
]
}Caching
These endpoints use aggressive caching to reduce database load. Cached responses are served for:
- 1-7 day ranges: 60 seconds
- 8-30 day ranges: 180 seconds
- 30+ day ranges: 300 seconds
AI-powered endpoints using Cloudflare Workers AI. Require API key authentication.
POST /v1/conversations/:id/generate-title
Generates a title from the conversation's first 20 messages and saves it to the conversation.
Request body: None.
Response: 200 OK
{ "title": "Discussion about API design" }Errors: 404 NOT_FOUND
POST /v1/conversations/:id/follow-ups
Generates suggested follow-up questions based on the conversation's last 20 messages.
Request body: None.
Response: 200 OK
{ "questions": ["What are the performance implications?", "How does this handle errors?"] }Errors: 404 NOT_FOUND
Dashboard-internal endpoints for project management. These routes do not require API key authentication (they are called from the dashboard frontend, authenticated via Clerk session).
POST /api/v1/projects
Creates a new project and auto-generates a default API key.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Project display name. |
slug |
string | Yes | URL-safe identifier. Lowercase alphanumeric and hyphens only. Must be unique within the organization. |
org_id |
string | No | Clerk organization ID. Defaults to "default". |
Response: 201 Created
{
"project": {
"id": "proj_abc123",
"org_id": "org_xyz",
"name": "My Project",
"slug": "my-project",
"created_at": 1710000000000
},
"api_key": {
"id": "key_abc123",
"name": "Default",
"key_prefix": "as_live_xxxx",
"key": "as_live_full_key_shown_only_once",
"created_at": 1710000000000
}
}Errors:
400 BAD_REQUEST-- Validation failure.409 CONFLICT-- Slug already taken in this organization.
GET /api/v1/projects
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
org_id |
string | "default" |
Clerk organization ID to filter by. |
Response: 200 OK
{
"data": [
{
"id": "proj_abc123",
"org_id": "org_xyz",
"name": "My Project",
"slug": "my-project",
"created_at": 1710000000000,
"key_count": 2
}
]
}GET /api/v1/projects/:id
Returns the project with its API keys.
Response: 200 OK
{
"id": "proj_abc123",
"org_id": "org_xyz",
"name": "My Project",
"slug": "my-project",
"created_at": 1710000000000,
"api_keys": [
{
"id": "key_abc123",
"name": "Default",
"key_prefix": "as_live_xxxx",
"created_at": 1710000000000,
"last_used_at": 1710000000000,
"revoked_at": null
}
]
}Errors: 404 NOT_FOUND
GET /api/v1/projects/by-slug/:slug
Same response shape as Get Project by ID.
Errors: 404 NOT_FOUND
GET /api/v1/projects/:id/conversations
Returns conversations for a specific project (for dashboard display).
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 | Results per page (1-100). |
Response: 200 OK
Same shape as List Conversations data array, wrapped in { "data": [...] }.
GET /api/v1/projects/:id/conversations/:convId/messages
Returns up to 500 messages for a conversation within a project. Returns an empty array if the conversation is not found (does not 404).
Response: 200 OK
{
"data": [
{
"id": "msg_001",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 5,
"created_at": 1710000000000
}
]
}GET /api/v1/projects/:id/analytics
Usage analytics for a project.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
range |
string | "30d" |
Time range: 7d, 30d, or 90d. |
Response: 200 OK
{
"summary": {
"total_conversations": 150,
"total_messages": 3200,
"total_tokens": 450000,
"active_api_keys": 2
},
"conversations_per_day": [
{ "date": "2026-03-15", "count": 12 }
],
"messages_per_day": [
{ "date": "2026-03-15", "count": 45 }
],
"tokens_per_day": [
{ "date": "2026-03-15", "total": 6800 }
],
"recent_conversations": [
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"title": "Latest chat",
"message_count": 10,
"token_count": 1500,
"updated_at": 1710000000000
}
]
}DELETE /api/v1/projects/:id
Permanently deletes a project and all associated data, including:
- All conversations in the project
- All messages in those conversations
- All API keys for the project
- All tags on conversations
Caution: This operation is irreversible. All data will be permanently deleted.
Response: 204 No Content
Errors:
404 NOT_FOUND— Project not found
Cascading Deletes:
- When a project is deleted, all conversations are deleted
- When conversations are deleted, all their messages are deleted
- API keys and tags are also deleted
- Foreign key constraints handle the cleanup automatically
State CRUD, queries, tokens, and claims are live under /api/v1/*. The TypeScript and Python SDKs expose these as first-class methods.
State CRUD, queries, tokens, and claims use project API key authentication. Lease renew/release and state event watch reads may use scoped capability tokens.
| Method | Path | Description |
|---|---|---|
PUT |
/api/v1/states/:stateKey |
Create or replace state |
GET |
/api/v1/states/:stateKey |
Read latest or historical state |
POST |
/api/v1/states/query |
Query state records |
DELETE |
/api/v1/states/:stateKey |
Delete state |
State records use:
| Field | Type | Description |
|---|---|---|
state_key |
string | Caller-defined state key |
agent_id |
string | Agent that last wrote the state |
data |
object | State payload |
metadata |
object | null | Optional metadata |
tags |
string[] | Query tags |
latest_sequence |
integer | Last state event sequence |
created_at, updated_at |
integer | Unix milliseconds timestamps |
deleted_at |
integer | null | Soft-delete timestamp |
Upsert body:
{
"agent_id": "assistant",
"data": { "step": "planning" },
"metadata": { "owner": "worker-c" },
"tags": ["session"],
"lease_id": "lease_abc123"
}Idempotency-Key may be supplied on mutations. Query body supports agent_id, tags, updated_after, updated_before, json_path, json_equals, predicates, at_sequence, at_time, limit, and cursor. Responses use:
{
"data": [],
"pagination": { "limit": 50, "next_cursor": null, "total": 0 }
}| Method | Path | Description |
|---|---|---|
GET |
/api/v1/states/:stateKey/events |
List events after a sequence |
POST |
/api/v1/states/:stateKey/lease |
Create a write lease |
POST |
/api/v1/leases/:id/renew |
Renew an active lease |
DELETE |
/api/v1/leases/:id |
Release an active lease |
State events use event_type values upsert and delete:
{
"sequence": 42,
"id": "evt_abc123",
"state_key": "assistant/session-123",
"agent_id": "assistant",
"event_type": "upsert",
"data": { "step": "planning" },
"metadata": null,
"tags": ["session"],
"idempotency_key": "session-123-step-1",
"created_at": 1710000000000
}Lease create body:
{
"holder": "worker-c",
"ttl_ms": 30000
}| Method | Path | Description |
|---|---|---|
POST |
/api/v1/capability-tokens |
Create a scoped state token |
GET |
/api/v1/capability-tokens |
List capability tokens |
DELETE |
/api/v1/capability-tokens/:id |
Revoke a capability token |
POST |
/api/v1/claims |
Create a deterministic claim |
GET |
/api/v1/claims |
List claims |
GET |
/api/v1/claims/:id |
Read claim with evidence |
POST |
/api/v1/claims/:id/verify |
Run deterministic verification |
Capability scopes are state:read, state:write, state:watch, lease:write, and claim:write. The raw token is returned only on create.
Claim create body:
{
"subject_type": "state",
"subject_id": "assistant/session-123",
"statement": "Session reached planning step",
"evidence": [
{
"kind": "json_value",
"source": "assistant/session-123",
"data": { "step": "planning" },
"json_path": "$.step",
"expected_value": "planning"
}
]
}Evidence kinds are state_event, text_hash, and json_value.
Two sets of key management endpoints exist:
- Authenticated (
/api/projects/:projectId/keys) -- requires a valid API key. The caller can only manage keys for the project their key belongs to. - Dashboard (
/api/v1/projects/:id/keys) -- used by the dashboard frontend. Authenticated via Clerk session, not API key.
Both sets share the same request/response shapes.
POST /api/projects/:projectId/keys # Authenticated
POST /api/v1/projects/:id/keys # Dashboard
Creates a new API key for the project.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Key display name (1-255 characters). |
scopes |
string[] | No | Permission scopes for the new key. Omit for full access. See Permissions & scopes. On the authenticated route the scopes must be a subset of the authenticating key's; the dashboard route (Clerk session) may grant any scopes. |
Response: 201 Created
{
"id": "key_abc123",
"name": "Production",
"key_prefix": "as_live_xxxx",
"key": "as_live_full_key_shown_only_once",
"scopes": ["conversations:read"],
"created_at": 1710000000000,
"last_used_at": null,
"revoked_at": null
}The key field contains the full API key and is only returned at creation time. Store it securely. scopes is null for full-access keys.
Errors:
403 FORBIDDEN-- Attempting to create a key for a different project (authenticated route only).404 NOT_FOUND-- Project does not exist (dashboard route only).
GET /api/projects/:projectId/keys # Authenticated
Lists all API keys for the project (including revoked ones).
Response: 200 OK
{
"data": [
{
"id": "key_abc123",
"name": "Default",
"key_prefix": "as_live_xxxx",
"scopes": null,
"created_at": 1710000000000,
"last_used_at": 1710000000000,
"revoked_at": null
}
]
}The full key is never returned after creation. scopes is null for full-access keys.
Errors:
403 FORBIDDEN-- Attempting to list keys for a different project.
DELETE /api/projects/:projectId/keys/:keyId # Authenticated
DELETE /api/v1/projects/:id/keys/:keyId # Dashboard
Soft-deletes an API key by setting revoked_at. The key immediately becomes invalid for authentication.
Response: 204 No Content
Errors:
403 FORBIDDEN-- Attempting to revoke a key for a different project (authenticated route only).
GET /api
No authentication required.
Response: 200 OK
{ "name": "agentstate", "version": "0.1.0", "status": "ok" }POST /api/mcp
A hosted MCP (Model Context Protocol) server over Streamable HTTP. Authenticate with
Authorization: Bearer <token>, where the token is an API key (as_live_...), a capability
token (as_cap_...), or an OAuth access token. Supports the standard MCP methods
(initialize, tools/list, tools/call, ping); each tool requires a scope. A 401
returns WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource".
See the MCP guide and OAuth 2.1 guide.
No authentication required:
GET /.well-known/oauth-protected-resource # RFC 9728 resource metadata
GET /.well-known/oauth-authorization-server # RFC 8414 authorization server metadata
POST /api/oauth/register # RFC 7591 Dynamic Client Registration
GET /api/oauth/authorize # Authorization-code + PKCE flow → consent screen
POST /api/oauth/token # Token exchange and refresh-token rotation
GET /llms.txt # LLM-readable project summary
GET /agents.md # Agent-readable documentation
GET /openapi.json # OpenAPI 3.x specification
No authentication required. These endpoints return plain text, markdown, and JSON respectively.