API Reference
Chronos exposes an HTTP API on port 9096 with two-layer auth (transport bearer + agent-DID principal token). Endpoints cover health checks, agent auth, and alarm CRUD.
Chronos exposes an HTTP API on port 9096 (configurable) with two-layer auth: a shared transport bearer and an agent-DID principal token. All responses use the uniform {ok, data, error} envelope.
Source file: internal/server/server.go, pkg/types/types.go.
Response envelope
Every response uses the types.Envelope shape:
{
"ok": true,
"data": { ... }
}
or on failure:
{
"ok": false,
"error": {
"code": "unauthorized",
"message": "missing or invalid transport bearer",
"retryable": false
}
}
Error codes are stable constants the planner/agent may branch on:
| Code | Meaning |
|---|---|
invalid_request | Malformed request body or parameters |
unauthorized | Transport or principal auth failed |
not_found | Alarm ID unknown or not owned by caller |
conflict | Idempotency-key clash with a different alarm |
internal | Server-side failure |
Public endpoints
GET /
Returns service identity.
{"ok": true, "data": {"service": "chronosd", "version": "0.1.0", "health": "/healthz"}}
GET /healthz
Returns database connectivity status. Returns 503 when the database is unreachable.
{"ok": true, "data": {"status": "ok", "version": "0.1.0", "db": true}}
Agent auth endpoints
POST /v1/agent/auth/challenge
Opens the challenge/verify flow. Requires transport auth.
Request:
{"did": "did:matrix:<uuid>:<keyfp16>"}
Response:
{
"ok": true,
"data": {
"did": "did:matrix:...",
"nonce": "<base64url>",
"message": "matrix-chronos-auth:<did>:<nonce>",
"expires_in": 120
}
}
The agent must ed25519-sign the message string with the private key matching the DID's fingerprint.
POST /v1/agent/auth/verify
Proves possession of the DID's key. Requires transport auth.
Request:
{
"did": "did:matrix:...",
"public_key": "<hex ed25519 pubkey>",
"nonce": "<nonce from challenge>",
"signature": "<hex ed25519 signature>"
}
Response:
{
"ok": true,
"data": {
"token": "<hmac principal token>",
"owner_user_id": "<supabase uuid>",
"expires_in": 86400
}
}
The token is presented as X-Chronos-Agent on all alarm endpoints.
Alarm CRUD endpoints
All alarm endpoints require both transport auth and a valid X-Chronos-Agent principal token.
POST /v1/alarms
Creates a new alarm.
Request:
{
"label": "Review PR #42",
"kind": "once",
"delay_seconds": 600,
"conversation_id": "conv-abc",
"wake_message": "Time to review PR #42. The CI passed.",
"payload": {"pr_number": 42},
"idempotency_key": "pr-review-42",
"max_failures": 3
}
For once alarms, provide exactly one of delay_seconds or fire_at (RFC3339). For cron alarms:
{
"kind": "cron",
"cron_expr": "@every 5m",
"timezone": "America/New_York",
"conversation_id": "conv-abc",
"wake_message": "HEARTBEAT: review your active goals..."
}
Response:
{
"ok": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"next_fire_at": "2026-07-14T15:30:00Z",
"status": "active"
}
}
When the alarm carries an idempotency_key that already exists for the owner, the existing alarm is returned (no duplicate created).
GET /v1/alarms
Lists the caller's alarms, most recent first.
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
limit | int | 100 | Max alarms to return (capped at 500) |
Response:
{
"ok": true,
"data": {
"alarms": [/* array of View objects */],
"count": 3
}
}
GET /v1/alarms/{id}
Returns a single alarm by ID. Owner-checked: returns 404 if the alarm does not exist or belongs to a different agent.
Response:
{
"ok": true,
"data": {/* View object */}
}
DELETE /v1/alarms/{id}
Cancels an active alarm. Owner-checked. Already-fired or already-cancelled alarms return success (idempotent cancellation).
Response:
{
"ok": true,
"data": {/* View object with status="cancelled" */}
}
CreateAlarmRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
label | string | No | Human-readable label |
kind | string | Yes | "once" or "cron" |
delay_seconds | int64 | Once* | Relative delay from now (seconds) |
fire_at | string | Once* | Absolute RFC3339 instant. *Exactly one of delay_seconds or fire_at required for once |
cron_expr | string | Cron | 5-field expression, @descriptor, or @every Nm |
timezone | string | No | IANA timezone for cron evaluation (default UTC) |
conversation_id | string | No | Conversation to resume into (empty = fresh) |
wake_message | string | Yes | The contextful turn delivered on wake |
payload | JSON | No | Opaque state echoed back on wake |
idempotency_key | string | No | Per-owner dedup key |
max_failures | int | No | Wake-delivery retry ceiling (default 5) |
Request size limits
The maximum request body is 256 KB (maxBodyBytes). Larger bodies are rejected with invalid_request.
