Skip to main content

Reporting Protocol

All communication between a SLAW instance and a Botfather tower is instance-initiated over HTTPS. The instance calls four endpoints; the tower never pushes to the instance except via directive payloads returned inside heartbeat and sync responses.

Prerequisites: An enrolled, active instance with a valid API key. See Identity & Keys for how keys are issued and secured.


Protocol version

The current wire protocol is v1. Every request body must include "protocolVersion": 1.

The tower accepts the current version and one prior version (PROTOCOL_VERSION - 1). Instances on an older version receive HTTP 426 Upgrade Required with a human-readable reason. Upgrade SLAW to resume reporting.


Authentication

Authenticated endpoints require:

Authorization: Bearer <instance-api-key>

The enroll endpoint is the only unauthenticated call — the instance has no key yet.


Endpoints

1. Enroll — POST /api/ingest/v1/enroll

Token-less self-enrollment. The instance calls this on startup when no enrollment record exists locally.

Request body

{
"protocolVersion": 1,
"instance": {
"machineId": "<salted-hash>",
"instanceId": "<alphanumeric-id>",
"hostname": "eng-laptop-01",
"os": "darwin",
"slawVersion": "1.4.2"
},
"capabilities": {
"reportIssueTitles": true,
"liveStream": false
}
}
FieldTypeDescription
instance.machineIdstring (8–128 chars)Salted-hash machine identity from ~/.slaw/machine.json.
instance.instanceIdstring (≤64 chars, [a-zA-Z0-9_-])Stable identifier for this SLAW installation.
instance.hostnamestringSystem hostname, used in the tower's Fleet View.
instance.osdarwin | linux | win32Operating system.
instance.slawVersionstringInstalled SLAW version.
capabilities.reportIssueTitlesboolean (default true)When false, issue titles are redacted to their key in all subsequent sync payloads. See What Is and Isn't Synced.
capabilities.liveStreamboolean (default false)Whether this instance supports the live WebSocket channel.

Response

{
"enrollmentId": "550e8400-e29b-41d4-a716-446655440000",
"state": "pending",
"pollIntervalSec": 10
}
FieldDescription
stateOne of pending, active, rejected, revoked. If active (auto-approve rule matched), apiKey is also returned.
apiKeyPresent only when state === "active". Store this key — it is never returned again.
pollIntervalSecHow often to call the poll endpoint while waiting for approval.

When state is pending, the instance polls until it transitions to active or rejected.

Poll — POST /api/ingest/v1/enroll/poll

{
"protocolVersion": 1,
"enrollmentId": "550e8400-e29b-41d4-a716-446655440000"
}

Returns the same state + optional apiKey fields as the enroll response. The instance stops polling when state reaches a terminal value.


2. Heartbeat — POST /api/ingest/v1/heartbeat

Sent approximately every 60 seconds. Any successful authenticated ingest call counts as proof of life, but the Heartbeat is the primary liveness signal.

Request body

{
"protocolVersion": 1,
"sentAt": "2026-06-09T01:00:00.000Z",
"status": "ok",
"uptimeSec": 3600,
"counts": {
"squads": 2,
"agents": 8,
"activeRuns": 1,
"openIssues": 14
},
"spend": {
"todayCents": 420,
"monthCents": 6800
},
"lastEventCursor": "cursor-abc123",
"appliedLimitVersion": 3,
"appliedSkillCatalogVersion": 12
}
FieldDescription
statusok or degraded. Use degraded if the instance is running but experiencing errors.
countsLive snapshot: squads, agents, active runs, open issues.
spend.todayCentsSpend since UTC midnight (integer cents).
spend.monthCentsMonth-to-date spend (integer cents).
lastEventCursorThe cursor the instance will use in the next sync batch, for observability. Null on first run.
appliedLimitVersionThe monotonic version of the budget limit currently applied. Lets the tower skip a set_limits push it already sent.
appliedSkillCatalogVersionThe catalog version last seen/pulled (observability; never forced).

Response

{
"acknowledged": true,
"directives": [
{ "kind": "set_limits", "limit": { ... } }
]
}

The directives array is the tower's back-channel. Process each directive in order. See Directives below.


3. Sync — POST /api/ingest/v1/sync

Sends delta upserts (entity changes) and fact events (cost, run, activity). Called when there is new data to report; the interval is adjusted by the set_sync_interval directive.

Request body

{
"protocolVersion": 1,
"sentAt": "2026-06-09T01:01:00.000Z",
"batchCursor": "cursor-abc124",
"upserts": [ ... ],
"facts": [ ... ]
}
FieldDescription
batchCursorMonotonically increasing string cursor the instance proposes for this batch.
upsertsArray of entity upserts (squads, agents, skills, projects, issues). Max 2000 per batch.
factsArray of fact events (cost, run, activity). Max 5000 per batch.

See What Is and Isn't Synced for the full breakdown of which fields are included in upserts.

Response

{
"acknowledgedCursor": "cursor-abc124",
"accepted": {
"upserts": 5,
"facts": 12,
"deduplicated": 0
},
"directives": []
}

The acknowledgedCursor confirms the batch was stored. The instance advances its local cursor only after receiving this acknowledgement.


4. Manifest — POST /api/ingest/v1/manifest

Nightly self-healing reconciliation. The instance reports per-entity-type counts; the tower compares against its own view and requests a full resync of any divergent type.

Request body

{
"protocolVersion": 1,
"sentAt": "2026-06-09T02:00:00.000Z",
"counts": {
"squads": 2,
"agents": 8,
"projects": 3,
"issues": 142,
"costEvents": 1024
}
}

Response

{
"inSync": false,
"resyncTypes": ["issue"]
}

When resyncTypes is non-empty, the instance queues a full resync of those entity types in the next sync batch.


Directives

Directives are returned in Heartbeat and sync responses. The instance processes them in order.

kindPayloadAction
set_sync_intervalseconds: numberChange the sync polling interval. Range: 10–3600 s.
request_reconciliation(none)Trigger a manifest call on the next cycle.
request_live_streamdurationSec: numberOpen the WebSocket live channel for the specified duration. See Live Channel.
stop_live_stream(none)Close the live channel if open.
set_limitslimit: LimitSpecApply a new tower-governed budget limit. Apply only when limit.version is greater than the currently applied version.
skills_updatedcatalogVersion: numberThe skill catalog has advanced; pull at your leisure.

Error codes

HTTP statuscodeMeaning
426protocol_version_unsupportedClient version is too old. Upgrade SLAW.
400invalid_payloadRequest body failed schema validation.
401unauthorizedMissing or invalid API key.
404enrollment_not_foundThe enrollmentId is unknown to this tower. Re-enroll.
403enrollment_rejectedAn Operator rejected this instance.
403enrollment_revokedThis instance was revoked from the tower UI.
429rate_limitedToo many requests. Back off and retry.

Next steps