Skip to main content

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:
FieldTypeDescription
externalIdstringUnique session ID from the originating tool. Used for dedup.
sourcestringPlugin identifier (e.g., “opencode”, “claude-code”, “codex-cli”, “cursor”)
promptTokensnumberTotal input tokens
completionTokensnumberTotal output tokens
costnumberEstimated USD cost
Optional fields:
FieldTypeDescription
titlestringSession title
modelstringModel name
providerstringModel provider
projectPathstringAbsolute project path
projectNamestringProject directory name
totalTokensnumberSum of prompt + completion
durationMsnumberSession duration in ms
messageCountnumberNumber 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:
FieldTypeDescription
sessionExternalIdstringThe externalId of the parent session
externalIdstringUnique message ID for dedup
rolestringOne of: “user”, “assistant”, “system”, “tool”, “unknown”
Optional fields:
FieldTypeDescription
partsarrayArray of message part objects
modelstringModel used for this message
promptTokensnumberInput tokens for this message
completionTokensnumberOutput tokens for this message
durationMsnumberTime to generate the response
Part types:
TypeFieldsDescription
textcontentPlain text content
tool-calltoolName, argsA tool/function call
tool-resulttoolName, contentResult 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:
ParamTypeDefaultDescription
limitnumber50Max sessions to return (1-200)
cursorstringnullPagination cursor from previous response
sourcestringallFilter 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:
ParamTypeDescription
formatstringRequired. One of: deepeval, openai, text
sessionIdsstringComma-separated session IDs. If omitted, exports all eval-ready sessions.
statusstringFilter by eval status: golden, correct, incorrect, needs_review
limitnumberMax 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:
ParamTypeDescription
qstringNatural language query
limitnumberMax 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:
CodeMeaning
200Success
400Bad request (invalid parameters)
401Unauthorized (missing or invalid API key)
404Resource not found
429Rate limited
500Server error

Rate limits

The API enforces rate limits per API key. Current limits:
EndpointLimit
/sync/*100 requests/minute
/api/*60 requests/minute
/search30 requests/minute
If you hit a rate limit, the response includes a Retry-After header with the number of seconds to wait.