Base URL: https://agentstate.app/api
Backward-compatible base: https://agentstate.app (routes under /v1/* mirror /api/v1/*).
The V2 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 - Conditional includes: Use
?include=messagesto fetch messages only when needed - HTTP method semantics: PATCH for partial updates, proper status codes
- Deprecation headers: V1 endpoints return
X-API-Deprecated: truewarning header
V1 endpoints remain functional but are deprecated. All V1 responses include:
X-API-Deprecated: true
X-API-Sunset: 2027-01-01
Deprecation: true
Link: </api/v2/conversations>; rel="successor-version"
Migration guide: See V2 Migration below.
- 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. V2 includestotalcount in pagination metadata. - Metadata: Arbitrary JSON object. Stored as serialized TEXT, parsed on read.
All /v1/conversations/* and /v1/tags endpoints require a valid API key.
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 /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.
All V2 endpoints require API key authentication and are located at /api/v2/*.
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 V2 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/v2/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,
"created_at": 1710000000000,
"updated_at": 1710000000000
}V2 Changes:
- Messages are NOT included in the create response for efficiency
- Use
POST /api/v2/conversations/:id/messagesto add messages separately
Errors:
400 BAD_REQUEST-- Invalid body or validation failure.409 CONFLICT-- A conversation with the givenexternal_idalready exists.
GET /api/v2/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,
"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/v2/conversations/:id
Returns a single conversation. Messages are NOT included by default.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
include |
string | -- | Comma-separated resources to include. Use messages to include messages array. |
Examples:
# Get conversation metadata only (no messages)
GET /api/v2/conversations/:id
# Include messages
GET /api/v2/conversations/:id?include=messagesResponse: 200 OK
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"project_id": "abc123",
"external_id": null,
"title": "My conversation",
"metadata": null,
"message_count": 2,
"token_count": 50,
"created_at": 1710000000000,
"updated_at": 1710000000000,
"messages": [
{
"id": "msg_001",
"role": "user",
"content": "Hello",
"metadata": null,
"token_count": 5,
"created_at": 1710000000000
}
]
}V2 Changes:
- Messages NOT included by default (use
?include=messages) - Uses
includequery param instead offields
Errors:
404 NOT_FOUND-- Conversation not found.
GET /api/v2/conversations/by-external-id/:externalId
Find a conversation by the caller-provided external_id. Messages are NOT included by default.
Response shape is identical to Get Conversation (without ?include=messages).
Errors: 404 NOT_FOUND
PATCH /api/v2/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,
"created_at": 1710000000000,
"updated_at": 1710000080000
}V2 Changes:
- Uses
PATCHinstead ofPUT(HTTP semantic improvement)
Errors: 404 NOT_FOUND
DELETE /api/v2/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/v2/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: 204 No Content
V2 Changes:
- Returns
204 No Contentinstead of201 Createdwith message list - More efficient for bulk message inserts
Errors: 404 NOT_FOUND -- Conversation does not exist.
GET /api/v2/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 | -- | Created-at timestamp (unix ms) 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": "1710000005000"
}
}next_cursor is the created_at timestamp of the last message in the page. Pass it as the after parameter for the next page. null when there are no more results.
V2 Changes:
- Cursor is now
created_attimestamp instead of message ID
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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/keys
Creates a new API key for the authenticated project.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Key display name (1-255 characters). |
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",
"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.
V2 Changes:
key_idinstead ofid- Includes
project_idin response
Errors:
400 BAD_REQUEST-- Validation failure.
GET /api/v2/keys
Lists all API keys for the authenticated project (including revoked ones).
Response: 200 OK
{
"data": [
{
"key_id": "key_abc123",
"project_id": "proj_abc123",
"name": "Default",
"key_prefix": "as_live_xxxx",
"created_at": 1710000000000,
"last_used_at": 1710000000000,
"revoked_at": null
}
]
}The full key is never returned after creation.
V2 Changes:
key_idinstead ofid- Includes
project_idin each key object
DELETE /api/v2/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.
- Base Path:
/api/v1/*→/api/v2/* - Messages in Get Conversation: V1 includes messages by default. V2 requires
?include=messages - Create Conversation Response: V1 returns created messages. V2 returns only conversation metadata
- Update Method: V1 uses
PUT. V2 usesPATCH - Append Messages Response: V1 returns created messages. V2 returns
204 No Content - Pagination: V2 includes
totalcount in metadata - Analytics: V2 derives
project_idfrom API key, no need to pass it as query parameter - Field Names: V2 consistently uses snake_case (e.g.,
key_idinstead ofid)
- Update base URLs: Change
/api/v1/to/api/v2/in your code - Update conversation fetches: Add
?include=messageswhen you need messages - Handle create response: V2 create no longer returns messages
- Update field name references: Use V2 field names (e.g.,
key_id,project_id) - Remove project_id from analytics: The API key determines the project automatically
- Update pagination handling: Use
pagination.totalfor total counts
V1:
// Create conversation
const response = await fetch('/api/v1/conversations', {
method: 'POST',
headers: { 'Authorization': 'Bearer as_live_xxx' },
body: JSON.stringify({ messages: [{ role: 'user', content: 'Hello' }] })
});
const { messages } = await response.json();
// messages is available in responseV2:
// Create conversation
const response = await fetch('/api/v2/conversations', {
method: 'POST',
headers: { 'Authorization': 'Bearer as_live_xxx' },
body: JSON.stringify({ messages: [{ role: 'user', content: 'Hello' }] })
});
const conversation = await response.json();
// messages NOT in response - conversation created efficiently
// Later, fetch messages when needed
const msgResponse = await fetch(`/api/v2/conversations/${conversation.id}/messages`);
const { data: messages } = await msgResponse.json();
⚠️ Deprecated: V1 endpoints are deprecated and will be removed on 2027-01-01. All V1 responses include deprecation headers. Please migrate to V2 API.
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,
"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,
"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, 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,
"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,
"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 /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 /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 /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 /v1/projects/by-slug/:slug
Same response shape as Get Project by ID.
Errors: 404 NOT_FOUND
GET /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 /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 /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 /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
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 (
/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 /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). |
Response: 201 Created
{
"id": "key_abc123",
"name": "Production",
"key_prefix": "as_live_xxxx",
"key": "as_live_full_key_shown_only_once",
"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.
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",
"created_at": 1710000000000,
"last_used_at": 1710000000000,
"revoked_at": null
}
]
}The full key is never returned after creation.
Errors:
403 FORBIDDEN-- Attempting to list keys for a different project.
DELETE /api/projects/:projectId/keys/:keyId # Authenticated
DELETE /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" }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.