Skip to main content

Recording

/api/recording/* is the ingest surface for Clone's Recording layer. Producers push CloneEvent rows here; the server validates them against packages/schema/events.schema.json, persists them under a RecordingSession keyed by session_id, and is fully idempotent on the per-event id.

Endpoints

MethodPathPurpose
POST/api/recording/events/Batch ingest. Body is a JSON array (1–1000 items).
GET/api/recording/events/<id>/Fetch a single event with full payload.
GET/api/recording/sessions/List the requester's 50 most recent sessions.
GET/api/recording/sessions/<id>/events/List events for a session, oldest first. ?limit= (default 500, max 2000).

Ingest semantics

  • Idempotent on id. Re-posting the same id returns a duplicates count; the row is unchanged. Producers can retry freely.
  • Per-item validation. Each event in the batch is validated independently. The endpoint returns accepted, duplicates, and an invalid: [{ index, errors }] array; one bad row does not fail the batch.
  • Session ownership. The first event for a given session_id creates the RecordingSession and locks it to request.user. Subsequent events from a different user with the same id are rejected as session belongs to another user.
  • Session bookends. A session.started event re-anchors the session's started_at, source, and source_detail even if it arrives out of order; a later session.stopped event sets ended_at if the new timestamp is later than the existing one.

Example

curl -sS -X POST https://api.clone.is/api/recording/events/ \
-H "X-Clone-API-Key: $CLONE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{
"id": "sess-1-start",
"session_id": "demo-1",
"occurred_at": "2026-05-05T12:00:00Z",
"source": "cli",
"type": "session.started"
},
{
"id": "sess-1-prompt",
"session_id": "demo-1",
"occurred_at": "2026-05-05T12:00:05Z",
"source": "agent",
"source_detail": "claude-code",
"type": "agent.prompt",
"agent": "Claude Code",
"prompt": "Run the test suite."
}
]'

Response shape

{
"accepted": 2,
"duplicates": 0,
"invalid": []
}

HTTP 201 if any event was newly persisted; HTTP 200 otherwise. Per-item failures look like:

{ "index": 0, "errors": ["'source' is a required property"] }

The first three schema errors per row are surfaced; remaining errors are truncated. The full validation rule set lives in Schema.