---
title: SLWP wire protocol
path: concepts/wire-protocol
status: published
---

# SLWP wire protocol

SLWP (the ScaiLog Wire Protocol, version 0.1) is the small JSON-based protocol
that carries log entries across the two hops of a ScaiLog deployment: from your
service to the local agent, and from the agent to the central server. This page
describes the local frame vocabulary, the batch codec used for the server hop,
and the canonical checksum that protects a batch in transit.

Every SLWP message is a JSON object carrying a version field `"v": "0.1"`.
Unknown major versions are rejected; unknown fields are ignored, so the format
can grow without breaking existing peers.

## Local frames (SDK to agent)

The SDK-to-agent hop is a local socket carrying newline-delimited JSON — one
frame per line. The frame vocabulary is: `emit`, `ack`, `nack`, `ping`, `pong`
(plus `query`, `row`, `done` for local reads).

The `emit` frame is the one that carries a log entry. It holds the routing and
content of one entry in plaintext, with PI still in marker form:

```json
{
  "v": "0.1",
  "op": "emit",
  "event_id": "01J...",
  "tenant": "acme",
  "service": "checkout",
  "env": "prod",
  "site": "SCAI-CHECKOUT-PAY-OK-001",
  "level": "info",
  "ts": "2026-07-13T10:00:00Z",
  "msg": "payment captured",
  "fields": {"amount": 4200},
  "pi": {"email": {"val": "jan@example.nl", "type": "direct_identifier"}},
  "subject": "usr_8842",
  "trace_id": null,
  "request_id": null
}
```

Note the shape of `pi`: a map from field name to `{"val": <scalar or flat list>,
"type": <pi_type>}`. This is the pre-encryption shape — the agent runs the ingest
pipeline on receipt, turning `pi` markers into encrypted `pi_fields` before
anything touches disk.

The agent answers each emit with an `ack` (carrying the `event_id`) or a `nack`
(carrying the `event_id`, an error `code`, and optional `detail`). `ping`/`pong`
are the liveness check.

## Batch codec (agent to server)

The agent ships entries to the server's `POST /v1/ingest` in batches. A batch is
**NDJSON compressed with zstd**: each entry is serialized to one compact JSON
line, the lines are joined with newlines, and the whole body is zstd-compressed.
Entries in a batch are in **post-encryption** shape — they carry `pi_fields` and
`pi_meta`, not the plaintext `pi` markers.

Three headers accompany the body:

| Header | Meaning |
|--------|---------|
| `X-SLWP-Version` | The SLWP version (`0.1`). |
| `X-SLWP-Batch-Id` | The batch identifier. |
| `X-SLWP-Checksum` | Checksum over the canonical body (see below). |

NDJSON on the wire lets the server stream the body line by line rather than
buffering a whole array.

## Canonical checksum

Each batch is integrity-checked with a SHA-256 checksum computed over a
**canonical** form of the batch — not over the compressed bytes. The canonical
form is JSON with sorted keys, `,`/`:` separators, and no whitespace, taken over
the object `{"batch_id": <id>, "entries": [...]}` *before* compression. The
checksum is the string `sha256:<hex>`.

On receipt the server decompresses the body, recomputes the checksum over the
same canonical form, and compares it to the `X-SLWP-Checksum` header. The check
**fails closed**: a batch that arrives with no checksum header is treated as a
mismatch (`BATCH_CHECKSUM_MISMATCH`) rather than waved through. Because the
checksum is over canonical JSON, it is stable regardless of how any particular
peer orders keys or spaces its output — which is what lets the four SDKs and the
server all agree on it byte-for-byte.
