API Reference
Base path: /api/v1
All protected endpoints use:
Authorization: Bearer <token>
Error Shape
{
"error": "Unable to append messages: AI session recording / append message transaction - session lookup failed",
"code": "INTERNAL_ERROR"
}
Every error must include consequence, module/process, and cause.
Public And Setup
GET /health
Returns service status.
GET /setup/status
Ensures the database schema exists when DATABASE_URL is configured, then returns whether the app has been initialized. In normal Vercel and Docker deployments this is already true because deploy initialization runs during build or container start.
POST /setup/init
Fallback initializer for cases where deploy initialization did not run. It ensures the database schema exists, initializes the app, and returns the UI token once. If PCP_ADMIN_TOKEN is configured, setup stores that token hash and returns no plaintext token. If PCP_ADMIN_TOKEN is not configured, setup generates a temporary first-login token, returns it once, and prints it once in server logs so zero-config deployments can still log in. If the app is initialized but the ui_auth credential row is missing, setup creates and returns one replacement token.
Response:
{
"success": true,
"initialized": true,
"ui_token": "<shown once or null when PCP_ADMIN_TOKEN is configured>",
"env_admin_token_configured": false,
"message": "No PCP_ADMIN_TOKEN was configured, so PCP generated a first-login admin token. It is shown once here and printed once in deployment logs. Log in with it, then change it immediately in Settings so the real credential is never visible in logs."
}
GET /auth/check
Validates a UI/admin bearer token before opening the dashboard. If PCP_ADMIN_TOKEN is configured, the deployment token is reconciled into ui_auth before validation. Missing generated credentials return SETUP_REQUIRED; configured-but-unusable deployment credentials return INVALID_ADMIN_TOKEN or ADMIN_CREDENTIAL_NOT_INITIALIZED with a deployment guide URL.
PATCH /auth/token
Updates the UI/admin token after the caller authenticates with the current valid UI token. This endpoint is disabled while PCP_ADMIN_TOKEN is configured because the environment variable owns the credential.
Admin Topics
GET /topics
Lists topics with session counts.
POST /topics
Creates a topic. title is optional and free-text: omit it (or send {}) and
the server generates a unique default name (New Topic, New Topic 2, …).
Duplicate titles are auto-suffixed instead of rejected.
Body:
{
"title": "Research",
"description": "optional"
}
POST /topics/:id/rename
Renames a topic.
Body:
{
"title": "new-title"
}
POST /topics/:id/archive
Archives or restores a topic. Body (optional): { "archived": false } to
restore from trash. When omitted, the topic is archived (archived: true).
DELETE /topics/:id
Permanently deletes a topic from trash. Sessions under it move to Uncategorized.
DELETE /sessions/:id
Permanently deletes a session and all its child data (messages, tokens, compactions, events).
POST /trash/empty
Permanently deletes ALL archived sessions and topics. Returns
{ sessions_deleted, topics_deleted }.
Admin Sessions
GET /sessions
Lists every session across all topics, including uncategorized ones
(topic_id: null). Used by the dashboard to group sessions by topic.
POST /sessions
Creates a session. topic_id is optional — omit it for an uncategorized
(no-topic) session. title is optional and auto-suffixed for uniqueness within
the topic group (or the uncategorized group).
Body (all fields optional):
{
"title": "Conversation title",
"topic_id": "topic_123"
}
GET /topics/:topicId/sessions
Lists sessions inside a topic.
POST /topics/:topicId/sessions
Creates a session inside a topic. title is optional: omit it for a unique
default name (New Session, New Session 2, …) within the topic. Duplicate
titles are auto-suffixed.
Body:
{
"title": "Conversation title"
}
GET /sessions/:id
Returns session details with topic title.
PATCH /sessions/:id
Updates session metadata.
Body:
{
"title": "New title",
"topic_id": "topic_123",
"archived": false
}
At least one field is required. topic_id: null moves the session to
Uncategorized (no topic). public: true enables a read-only public view;
public: false makes it private again. archived: true removes the session
from the active list; archived: false restores it.
POST /sessions/:id/import
Human fallback import (UI token). When an agent cannot upload from its sandbox,
the admin pastes the agent’s fallback block here. Accepts the same forgiving
formats as agent ingest — a { "messages": [...] } block, an agent wrapper
object that contains a messages array, <PCP_APPEND>/<PCP_COMPACT>,
ChatML-like arrays, or a plain transcript. Messages already present (matched by
role + content) are skipped, so re-pasting merges cleanly.
Body: { "payload": "<pasted text>" } (or the raw pasted text). Response:
{ success, imported_as: "messages"|"compact", imported, skipped }.
GET /sessions/:id/review
Returns messages for review.
PATCH /sessions/:id/messages/:messageId
Admin message correction (UI token). Edits a wrongly-recorded/imported message:
{ "content": "..." }. The append-only rule applies to AI tokens only; this
human-admin path is audited (message.edited).
POST /sessions/:id/messages/delete
Admin bulk delete (UI token). Removes wrongly-recorded/imported messages:
{ "message_ids": ["msg_...", ...] } (one or many). Only ids belonging to the
session are deleted; audited (message.deleted).
GET /sessions/:id/events
Returns audit events for a session.
GET /sessions/:id/compactions
Returns the durable compaction summaries for a session (additional records that never replace raw messages). The workspace renders these as readable, system-prompt-style cards. If a session has compactions but no messages, the messages pane shows “No raw messages recorded. This session has compact summaries.”
POST /sessions/:id/archive
Archives a session.
Tokens
GET /sessions/:sessionId/tokens
Lists token metadata for the session (never the secret): name, status
(active, expired, revoked), expires_at, and last_used_at.
DELETE /sessions/:sessionId/tokens
Permanently deletes a token from the database after a 3-second undo window.
Body: { "token_id": "tok_..." }.
PATCH /sessions/:sessionId/tokens
Renames a token. Body: { "token_id": "tok_...", "name": "New name" }. Body: { "token_id": "tok_...", "name": "New name" } to rename (names are kept distinct within the session), or
{ "token_id": "tok_...", "revoke": true } to revoke. A revoked token is
rejected on every subsequent request.
POST /sessions/:sessionId/tokens
Creates a session access token and returns it once, together with the recording URL and a ready-to-paste agent instruction.
Body (all fields optional):
{
"name": "Claude session",
"can_rename_session": false,
"expires_in": "7d"
}
namedefaults to<session title> access token.expires_inis one of1h,24h,7d,30d,never. Default is7d.neverstores a NULLexpires_at; expired tokens are rejected at auth time.
Response (token shown only here):
{
"success": true,
"access_token": "<raw token, shown once>",
"recording_url": "https://<domain>/r/<sessionId>",
"instruction": "You are recording this conversation to Personal Context Protocol. ...",
"expires_in": "7d",
"expires_at": "2026-06-30T00:00:00.000Z",
"status": "active"
}
Agent Recording (URL + token)
An external AI agent needs only two things: a recording URL and an access token. The URL carries all non-secret session information and is the entry point for discovering upload routes; the token is the only credential. Agent routes return a structured envelope:
{
"ok": false,
"code": "TOKEN_EXPIRED",
"retryable": false,
"message": "The session token expired.",
"next_steps": ["Ask the user to generate a new access token."]
}
GET /r/:sessionId (public)
Recording URL entry point. Returns the protocol descriptor so an agent can discover where to upload. No token required.
GET /api/v1/agent/resolve?url=<recording-url> (public)
Resolves a recording URL to its protocol descriptor.
GET /api/v1/agent/sessions/:sessionId/protocol (public)
Returns the protocol descriptor: auth, routes (read_messages,
record_messages, record_compact, ingest_any), allowed_actions (note
manage_topics: false), limits (max_messages_per_request: 50,
max_content_chars: 100000), existing_message_count, a hint, and
recording_mode + recording_guidance. The mode is wild (the agent may
redact unsafe secrets) or exact (record verbatim, including credentials, for
task migration). Fetch read_messages to discover the full conversation history,
then record ALL messages — not only the current exchange.
POST /api/v1/agent/sessions/:sessionId/messages
Append-only message recording. Same body as the legacy message route; requires
Authorization: Bearer <access-token>.
POST /api/v1/agent/sessions/:sessionId/compact
Stores a durable session summary when full message upload is impossible. Compactions are additional records and never replace raw messages.
Body (summary required):
{ "summary": "...", "decisions": ["..."], "open_questions": ["..."] }
POST /api/v1/agent/sessions/:sessionId/ingest
Forgiving ingestion. Accepts { messages }, { messages, compaction } (mixed),
<PCP_INGEST>...</PCP_INGEST>, <PCP_APPEND>...</PCP_APPEND>,
<PCP_COMPACT>...</PCP_COMPACT>, ChatML-like arrays, or raw transcript text, and
normalizes into messages, a compaction, or both. On failure it returns structured
retry guidance (code: "UNPARSEABLE_PAYLOAD"), not a vague error.
GET /api/v1/agent/sessions/:sessionId/review
Scoped message read (access token). Returns all messages already recorded in
this session so the agent knows the full conversation history and can pick up
from the next ordinal. This is the read_messages route from the protocol
descriptor.
AI Message Append (legacy)
POST /sessions/:sessionId/messages
Appends messages to the session bound to the bearer token. Retained for
back-compat; new agents should use /api/v1/agent/sessions/:sessionId/messages.
Body:
{
"messages": [
{
"role": "user",
"content": "Hello",
"provider": "unknown",
"base_model": "unknown"
},
{
"role": "assistant",
"content": "Hi.",
"provider": "anthropic",
"base_model": "claude"
}
],
"suggested_session_title": "Greeting"
}
Rules:
messagesmust contain 1 to 50 messages.rolemust beuser,assistant,system,tool, orcorrection.contentis required.Topic fields are not accepted.
suggested_session_titlerequires a token with rename permission; the server normalizes it and auto-suffixes duplicates within the topic.
Public
GET /public/sessions/:id (public)
Read-only view of a session and its messages when the session is marked
public. No admin token required. A private or missing session returns 404 so a
private session’s existence is not leaked. Tokens are never exposed. The /s/:id
page renders this.
Agent Schemas (public)
These endpoints return machine-readable JSON schemas so an agent can generate correct payloads without guessing or searching the web. The name “Personal Context Protocol” has been reused by unrelated projects — use ONLY these definitions, not web search results.
GET /agent/schema/message
Returns the JSON schema for the POST record_messages payload.
GET /agent/schema/compact
Returns the JSON schema for the POST record_compact payload.
GET /agent/schema/ingest
Returns the JSON schema for the POST ingest_any canonical fallback block,
including <PCP_INGEST> and <PCP_COMPACT> usage.
Recording URL origin
The browser UI builds the recording URL from window.location.origin (where the
admin opened the app), so it always matches the deployment. Server-side,
PCP_APP_URL is used only when it is a valid absolute http(s) URL; an
unexpanded template such as the literal ${VERCEL_URL} is ignored in favor of
the request origin. PCP_APP_URL is about constructing absolute URLs, not CORS.
Export
GET /export
Exports app data as JSON for the UI token holder.
The export omits plaintext tokens.