ARCANUM
service api

Wardstone API

Send chat messages in. Receive signed verdicts at your endpoint.

POST/v1/messages

In order per conversation. A repeated message_id is ignored, so retries are safe. Up to 1,000 messages per batch.

POST /v1/messages { "messages": [ { "conversation_id": "c1", "message_id": "m91", "author": "u7", "text": "...", "ts": 1791712345123 } ] }
200 { "results": [ { "conversation_id": "c1", "author": "u7", "tier1_score": 0.71, "escalated": true } ], "dropped_escalations": 0 }

GET/v1/stream

The same ingest as a binary WebSocket stream on the same port. One WebSocket binary message = one SBE message (schema id 22355, v1, little-endian). Send batches without waiting; each IngestAck echoes its seq, in order.

wire layout

header blockLength u16 · templateId u16 · schemaId u16 = 22355 · version u16 = 1 IngestBatch (templateId 1, client → server) seq u64 messages blockLength u16 · numInGroup u16, then per message: ts i64 · conversationId · messageId · author · text // each: u16 length + UTF-8 bytes IngestAck (templateId 2) seq u64 · droppedEscalations u32 · duplicates u32 · results { tier1Score f64 · escalated u8 · conversationId · author } Error (templateId 3) seq u64 · status u16 (400 / 413 / 500) · message

encode a batch (C++, codecs generated by sbe-tool from wardstone-schema.xml)

using namespace wardstone::wire; char buf[64 * 1024]; IngestBatch batch; batch.wrapAndApplyHeader(buf, 0, sizeof(buf)).seq(++seq); auto& m = batch.messagesCount(2); m.next().ts(1791712345123) .putConversationId("c42").putMessageId("1001").putAuthor("u7").putText("..."); m.next().ts(1791712346001) .putConversationId("c42").putMessageId("1002").putAuthor("u9").putText("..."); ws.sendBinary(buf, MessageHeader::encodedLength() + batch.encodedLength()); // ws://host:8090/v1/stream

decode the ack

MessageHeader h(frame, 0, len, 1); if (h.templateId() == IngestAck::sbeTemplateId()) { IngestAck ack; ack.wrapForDecode(frame, MessageHeader::encodedLength(), h.blockLength(), h.version(), len); inflight.erase(ack.seq()); // acks arrive in seq order auto& r = ack.results(); while (r.hasNext()) { r.next(); if (r.escalated() == BooleanType::T) { /* r.tier1Score(), r.getAuthorAsString() */ } } } else if (h.templateId() == Error::sbeTemplateId()) { /* status 400/413: batch rejected, connection stays open */ }

Verdict

POSTed to your configured sink when a user is flagged or their level rises. Deduplicate by verdict_id.

{ "verdict_id": "...", "author": "u7", "flagged": true, "level": "high", "confidence_pct": 98.6, "windows": [ { "conversation_id": "c1", "p": 0.981, "message_ids": ["m67", "m91"] } ], "evidence": { "c1": [ { "message_id": "m67", "author": "u7", "text": "...", "ts": 1791712345123 } ] }, "artifact_version": "2026.10.1", "created_at": 1791712399000 }

Signature

X-Wardstone-Signature: sha256=<HMAC-SHA256> over the raw, zstd-compressed body. Verify, then decompress.

expected = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest() if not hmac.compare_digest(headers["X-Wardstone-Signature"], expected): return 401 verdict = json.loads(zstandard.ZstdDecompressor().decompress(body))