Matrix logo

API Reference

Package chronos/internal/server exposes chronosd over HTTP. All /v1/ endpoints require two-layer auth: the transport bearer on every request, and the agent principal token (X-Ch...

Package chronos/internal/server exposes chronosd over HTTP. All /v1/* endpoints require two-layer auth: the transport bearer on every request, and the agent principal token (X-Chronos-Agent header) on alarm endpoints.

Source file: internal/server/server.go.


Base URL

http://127.0.0.1:9096    (box-local, default port)

In production, agents reach it through the public nginx route (e.g. https://matrix.paxeer.app/chronos/127.0.0.1:9096).


Response envelope

Every response uses the uniform {ok, data, error} envelope:

// Success
{"ok": true, "data": {  }}

// Failure
{"ok": false, "error": {"code": "unauthorized", "message": "…"}}

Error codes (stable; callers may branch on these):

CodeHTTP statusMeaning
invalid_request400Malformed body, missing required field, invalid kind
unauthorized401Missing/invalid transport bearer or principal token
not_found404Alarm id unknown or not owned by the caller
conflict409Idempotency key clash with a different alarm
internal500Database error or unexpected failure

Public endpoints

GET /healthz

No auth. Returns service health + DB connectivity.

→ 200
{
  "ok": true,
  "data": {
    "status": "ok",
    "version": "0.1.0",
    "db": true
  }
}

db: false → HTTP 503. The router's waitDaemonReady equivalent for chronosd would poll this.

GET /

No auth. Returns service identity.

→ 200
{
  "ok": true,
  "data": {
    "service": "chronosd",
    "version": "0.1.0",
    "health": "/healthz"
  }
}

Agent auth endpoints

POST /v1/agent/auth/challenge

Transport auth required. Requests a challenge nonce for a DID.

Request:

{
  "did": "did:matrix:11111111-2222-3333-4444-555555555555:0123456789abcdef"
}

Response (200):

{
  "ok": true,
  "data": {
    "did": "did:matrix:11111111-2222-3333-4444-555555555555:0123456789abcdef",
    "nonce": "base64url-encoded-24-random-bytes",
    "message": "matrix-chronos-auth:did:matrix:…:nonce",
    "expires_in": 120
  }
}

Errors:

  • 400 invalid_request — malformed DID

POST /v1/agent/auth/verify

Transport auth required. Proves possession of the DID's ed25519 key.

Request:

{
  "did": "did:matrix:11111111-2222-3333-4444-555555555555:0123456789abcdef",
  "public_key": "hex-encoded-32-byte-ed25519-public-key",
  "nonce": "the-nonce-from-challenge",
  "signature": "hex-encoded-ed25519-signature-of-the-challenge-message"
}

Response (200):

{
  "ok": true,
  "data": {
    "token": "base64url-payload.base64url-mac",
    "owner_user_id": "11111111-2222-3333-4444-555555555555",
    "expires_in": 86400
  }
}

Errors:

  • 401 unauthorized — unknown/expired/used nonce, signature verification failed, or public key doesn't match DID fingerprint
  • 400 invalid_request — malformed DID after successful signature (edge case)

Alarm endpoints

All alarm endpoints require BOTH transport auth AND a valid X-Chronos-Agent principal token. Owner scoping is enforced from the token's DID — never from a request field.

POST /v1/alarms

Create an alarm.

Headers:

Authorization: Bearer <CHRONOS_TOKEN>
X-Chronos-Agent: <principal-token>

Request:

{
  "label": "daily portfolio summary",
  "kind": "cron",
  "cron_expr": "0 9 * * *",
  "timezone": "America/New_York",
  "conversation_id": "conv_abc123",
  "wake_message": "Generate the daily portfolio summary: check balances…",
  "payload": {"task": "daily_summary", "last_run": null},
  "idempotency_key": "daily-summary-v1",
  "max_failures": 3
}

For kind=once, use exactly one of delay_seconds or fire_at:

{
  "kind": "once",
  "delay_seconds": 600,
  "wake_message": "Check if the transaction confirmed…"
}
{
  "kind": "once",
  "fire_at": "2026-06-16T09:00:00Z",
  "wake_message": "Meeting reminder: …"
}

Response (200):

{
  "ok": true,
  "data": {
    "id": "uuid-of-the-alarm",
    "next_fire_at": "2026-06-16T13:00:00Z",
    "status": "active"
  }
}

Errors:

  • 400 invalid_requestwake_message is empty, invalid kind, invalid schedule parameters
  • 401 unauthorized — missing/invalid transport bearer or principal token
  • 500 internal — database error

Idempotency: When idempotency_key is non-empty and matches an existing alarm for the same owner, the existing alarm is returned (same 200 shape). No duplicate is created.

GET /v1/alarms

List the caller's own alarms.

Headers: same as create.

Query params:

  • limit (int, default 100, max 500)

Response (200):

{
  "ok": true,
  "data": {
    "alarms": [
      {
        "id": "uuid",
        "label": "daily portfolio summary",
        "kind": "cron",
        "cron_expr": "0 9 * * *",
        "timezone": "America/New_York",
        "next_fire_at": "2026-06-16T13:00:00Z",
        "conversation_id": "conv_abc123",
        "wake_message": "Generate the daily portfolio summary…",
        "payload": {"task": "daily_summary"},
        "status": "active",
        "idempotency_key": "daily-summary-v1",
        "max_failures": 3,
        "failure_count": 0,
        "last_error": "",
        "created_at": "2026-06-15T08:00:00Z",
        "last_fired_at": null
      }
    ],
    "count": 1
  }
}

GET /v1/alarms/{id}

Get one alarm by ID. Owner-checked.

Headers: same as create.

Response (200): single alarm view (same shape as list items).

Errors:

  • 404 not_found — alarm unknown or not owned by caller

DELETE /v1/alarms/{id}

Cancel an active alarm. Owner-checked. Idempotent — already-fired or already-cancelled alarms return success.

Headers: same as create.

Response (200): the alarm view with status: "cancelled".

Errors:

  • 404 not_found — alarm unknown or not owned by caller

Internal note

All /v1/alarms* require BOTH the transport bearer AND a valid agent token. The transport bearer alone is not sufficient to scope to an owner. The agent token alone is not sufficient to prove the caller is a legitimate daemon. Both must be present.