API Endpoints
OpenSync exposes a REST API through Convex HTTP endpoints. All endpoints are authenticated with API keys.
Base URL
For the hosted version:
https://polished-penguin-622.convex.site
For self-hosted deployments, replace the base URL with your Convex deployment URL, substituting .convex.cloud with .convex.site:
https://your-deployment.convex.site
Authentication
All endpoints (except /health) require an API key in the Authorization header:
Authorization: Bearer osk_your_api_key_here
Generate API keys from the Settings page in the dashboard. Keys start with osk_ and are tied to your user account.
API keys provide full access to your account data. Do not commit them to version control or share them publicly.
Sync endpoints
These endpoints are used by sync plugins to push session and message data.
POST /sync/session
Create or update a session. Uses upsert logic based on externalId, so calling this multiple times with the same externalId is safe.
Request body:
{
"externalId": "unique-session-id-from-your-tool",
"source": "opencode",
"title": "Fix the login redirect bug",
"model": "claude-sonnet-4-20250514",
"provider": "anthropic",
"projectPath": "/Users/me/projects/my-app",
"projectName": "my-app",
"promptTokens": 3200,
"completionTokens": 1800,
"totalTokens": 5000,
"cost": 0.037,
"durationMs": 45000,
"messageCount": 8
}
Required fields:
| Field | Type | Description |
|---|
externalId | string | Unique session ID from the originating tool. Used for dedup. |
source | string | Plugin identifier (e.g., “opencode”, “claude-code”, “codex-cli”, “cursor”) |
promptTokens | number | Total input tokens |
completionTokens | number | Total output tokens |
cost | number | Estimated USD cost |
Optional fields:
| Field | Type | Description |
|---|
title | string | Session title |
model | string | Model name |
provider | string | Model provider |
projectPath | string | Absolute project path |
projectName | string | Project directory name |
totalTokens | number | Sum of prompt + completion |
durationMs | number | Session duration in ms |
messageCount | number | Number of messages |
Response:
{
"ok": true,
"sessionId": "j57a..."
}
POST /sync/message
Create or update a message within a session. The session is identified by sessionExternalId and will be auto-created if it does not exist.
Request body:
{
"sessionExternalId": "unique-session-id-from-your-tool",
"externalId": "unique-message-id",
"role": "assistant",
"parts": [
{
"type": "text",
"content": "Here's how to fix the redirect issue..."
},
{
"type": "tool-call",
"toolName": "edit_file",
"args": "{\"path\": \"src/auth.ts\", \"content\": \"...\"}"
}
],
"model": "claude-sonnet-4-20250514",
"promptTokens": 0,
"completionTokens": 450,
"durationMs": 3200
}
Required fields:
| Field | Type | Description |
|---|
sessionExternalId | string | The externalId of the parent session |
externalId | string | Unique message ID for dedup |
role | string | One of: “user”, “assistant”, “system”, “tool”, “unknown” |
Optional fields:
| Field | Type | Description |
|---|
parts | array | Array of message part objects |
model | string | Model used for this message |
promptTokens | number | Input tokens for this message |
completionTokens | number | Output tokens for this message |
durationMs | number | Time to generate the response |
Part types:
| Type | Fields | Description |
|---|
text | content | Plain text content |
tool-call | toolName, args | A tool/function call |
tool-result | toolName, content | Result returned by a tool |
POST /sync/batch
Sync multiple sessions and messages in a single request. Preferred for bulk operations to reduce write conflicts.
Request body:
{
"sessions": [
{ "externalId": "s1", "source": "opencode", "promptTokens": 100, "completionTokens": 50, "cost": 0.001 }
],
"messages": [
{ "sessionExternalId": "s1", "externalId": "m1", "role": "user", "parts": [{"type": "text", "content": "Hello"}] }
]
}
Response:
{
"ok": true,
"sessions": { "inserted": 1, "updated": 0 },
"messages": { "inserted": 1, "updated": 0 }
}
Query endpoints
GET /api/sessions
List sessions for the authenticated user.
Query parameters:
| Param | Type | Default | Description |
|---|
limit | number | 50 | Max sessions to return (1-200) |
cursor | string | null | Pagination cursor from previous response |
source | string | all | Filter by source plugin |
Response:
{
"sessions": [
{
"_id": "j57a...",
"title": "Fix login redirect",
"source": "opencode",
"model": "claude-sonnet-4-20250514",
"messageCount": 12,
"promptTokens": 3200,
"completionTokens": 1800,
"totalTokens": 5000,
"cost": 0.037,
"createdAt": 1706140800000
}
],
"cursor": "eyJ..."
}
GET /api/sessions/:id
Get a single session with all its messages.
Response:
{
"_id": "j57a...",
"title": "Fix login redirect",
"source": "opencode",
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "user",
"textContent": "I'm seeing a redirect loop...",
"promptTokens": 150,
"completionTokens": 0,
"createdAt": 1706140800000
},
{
"role": "assistant",
"textContent": "The issue is in your AuthProvider...",
"promptTokens": 0,
"completionTokens": 450,
"createdAt": 1706140803000
}
]
}
Search endpoints
POST /search
Search across all sessions using full-text or semantic search.
Request body (full-text):
{
"query": "redirect loop authentication",
"type": "fulltext",
"limit": 10
}
Full-text search matches against sessions.searchableText, which contains the session title and all message content. Results are ranked by relevance.
Request body (semantic):
{
"query": "How do I handle OAuth callback errors?",
"type": "semantic",
"limit": 10
}
Semantic search converts the query to a 1536-dimension embedding using OpenAI’s text-embedding-3-small model and performs a vector similarity search against sessionEmbeddings. Results are ranked by cosine similarity.
Response:
{
"results": [
{
"sessionId": "j57a...",
"title": "Fix OAuth redirect",
"score": 0.87,
"snippet": "...the callback URL needs to match..."
}
]
}
Export endpoints
GET /api/export
Export sessions in evaluation framework formats.
Query parameters:
| Param | Type | Description |
|---|
format | string | Required. One of: deepeval, openai, text |
sessionIds | string | Comma-separated session IDs. If omitted, exports all eval-ready sessions. |
status | string | Filter by eval status: golden, correct, incorrect, needs_review |
limit | number | Max sessions to export |
Response: The response is the exported file content with the appropriate Content-Type header (application/json for DeepEval/OpenAI, text/plain for text format).
Context endpoint
GET /api/context
Retrieve relevant session context for RAG (Retrieval-Augmented Generation) pipelines. Returns the most relevant session snippets for a given query.
Query parameters:
| Param | Type | Description |
|---|
q | string | Natural language query |
limit | number | Max results (default: 5) |
Response:
{
"context": [
{
"sessionId": "j57a...",
"title": "Set up Convex auth",
"content": "The relevant portion of the conversation...",
"score": 0.91
}
]
}
Use this endpoint to inject past session knowledge into your AI prompts.
Health endpoint
GET /health
Public endpoint (no auth required). Returns the API status.
Response:
{
"status": "ok",
"version": "1.0.0"
}
Error responses
All endpoints return errors in this format:
{
"error": "Unauthorized",
"message": "Invalid or missing API key"
}
Common HTTP status codes:
| Code | Meaning |
|---|
| 200 | Success |
| 400 | Bad request (invalid parameters) |
| 401 | Unauthorized (missing or invalid API key) |
| 404 | Resource not found |
| 429 | Rate limited |
| 500 | Server error |
Rate limits
The API enforces rate limits per API key. Current limits:
| Endpoint | Limit |
|---|
/sync/* | 100 requests/minute |
/api/* | 60 requests/minute |
/search | 30 requests/minute |
If you hit a rate limit, the response includes a Retry-After header with the number of seconds to wait.