Heron API Contract
Overview
Heron has two integration surfaces, used by three clients:
| Surface | Used by | Purpose |
|---|---|---|
| USB WebSerial | heron-onboard PWA only | First-time setup over USB (identify, challenge, provision) |
Local HTTP :8080 | heron-dashboard + heron CLI | Ongoing monitoring on LAN |
Important: The local dashboard and CLI do not use WebSerial and do not authenticate with device identity. They find the Pico on the network and read the HTTP API.
Passive monitor: observe only
Heron is a passive DNS observer. It runs a local DNS server that forwards every query transparently to upstream (1.1.1.1 / 8.8.8.8) and logs it. It does not block, redirect, or filter. The buzzer on the board is an alert, not enforcement.
USB WebSerial Provisioning Protocol
Apps: heron-onboard ↔ Pico firmware (lib/provisioning.py)
Transport: USB CDC/ACM, 115200 baud, newline-delimited JSON
Browser: Chrome/Edge only (WebSerial API)
Opening the serial port resets the Pico. Unprovisioned devices enter the provisioning loop immediately (before I2C/OLED init).
Boot signals
While waiting for commands, the Pico emits a ready beacon:
{"status": "ready"}
Repeated every ~2 seconds. The PWA waits for this before sending commands.
identify
Read device identity from key storage.
Send:
{"cmd":"identify"}
Response:
{"status":"ok","serial":"75e409665340a674","has_keys":true}
serial is an 8-byte hex identifier. has_keys is true when cryptographic keys are present.
challenge
Cryptographic device attestation. The PWA sends a random 32-byte nonce; the device signs it with its private key and returns the signature plus its certificate.
Send:
{"cmd":"challenge","nonce":"<64 hex chars>"}
Response:
{"status":"ok","serial":"75e409665340a674","cert":"<hex>","signature":"<hex>"}
Errors: missing_nonce, bad_nonce, bad_nonce_length, no_keys, no_cert, sign_failed:*
The PWA verifies the certificate against the embedded Northsline root public key, then verifies the nonce signature against the device public key extracted from the cert. Both checks use Web Crypto (SubtleCrypto, ECDSA P-256). No network call.
scan
Return available WiFi networks as JSON.
Send:
{"cmd":"scan"}
Response:
{
"status": "ok",
"networks": [
{"ssid": "HomeWiFi", "bssid": "aa:bb:cc:dd:ee:ff", "channel": 6, "rssi": -45, "hidden": false}
]
}
Capped at 15 networks.
router_info
Return the connected AP's BSSID and the Pico's IP address.
Send:
{"cmd":"router_info"}
Response:
{"status":"ok","bssid":"aa:bb:cc:dd:ee:ff","ip":"192.168.1.42"}
Errors: {"status":"error","reason":"not_connected"} if WiFi is not connected.
provision
Write Wi-Fi credentials to /config.json. No sticker code needed. Device identity was already verified via challenge.
Send:
{"cmd":"provision","ssid":"HomeWiFi","pass":"hunter2"}
Response: {"status":"ok"} or {"status":"error","reason":"..."}
| Reason | Meaning |
|---|---|
bad_json | Line could not be parsed |
unknown_cmd | Missing or invalid cmd |
missing_wifi | SSID or password missing |
write_failed:* | Flash write error |
After success, device reboots, joins Wi-Fi, starts DNS + HTTP.
Onboarding PWA flow (UI order)
1. Connect WebSerial open → wait for ready → identify
2. Verify Cryptographic challenge-response (WebSerial + Web Crypto)
3. Wifi WebSerial scan networks → provision with SSID/pass
4. Router WebSerial router_info → OUI lookup → router DNS instructions
5. Done User unplugs → wall power
Source: heron-onboard/src/lib/components/StickerGate.svelte, heron-onboard/src/lib/serial.ts, heron-onboard/src/lib/crypto.ts
Device Identity (crypto attestation)
Device verification is fully offline. No cloud, no server, no account.
Certificate format (binary)
[0:1] signature length (1 byte)
[1:1+sigLen] DER-encoded ECDSA signature (root signs serial || pubKey)
[1+sigLen:1+sigLen+8] serial number (8 bytes)
[1+sigLen+8:1+sigLen+8+65] public key (65 bytes, uncompressed)
Crypto primitives
- Curve: NIST P-256 (secp256r1)
- Signature: ECDSA with SHA-256
- Browser verification: Web Crypto SubtleCrypto (native, no dependencies)
- Device signing: Pure Python ECDSA (
lib/ecdsa.py, ~3s on RP2350)
Serial number
8-byte hex (e.g., 75E409665340A674). Printed on sticker for reference. No security role. The cryptographic identity is what proves the device is genuine.
Local HTTP API
App: heron-dashboard and heron CLI
Base URL:
- Primary:
http://heron.local:8080(mDNS) - Fallback:
http://<PICO_IP>:8080
Discovery (heron-dashboard/src/lib/api/discovery.ts, heron-cli/heron_cli/cli.py):
- Probe
heron.local:8080/health(2s timeout) - Probe saved IP (localStorage /
~/.heron-host) - Manual IP entry (banner /
--hostflag)
No authentication. LAN access only.
GET /health
{"status": "ok"}
GET /audit/weekly
Query params: since (epoch seconds), limit (default 50, max 150)
Response (MVP):
[
{
"source": "192.168.1.26",
"domain": "telemetry.example.com",
"timestamp": 1717123456.789,
"flagged": false
}
]
timestamp is seconds (float). flagged always false in MVP.
GET /devices
[
{
"id": "187",
"ip": "192.168.1.26",
"name": "Device at 192.168.1.26",
"first_seen": 1717120000,
"last_seen": 1717123456,
"trust_level": "unknown",
"query_count": 12,
"flagged_count": 0
}
]
GET /stats
{
"total_queries": 1240,
"unique_domains": 45,
"flagged_count": 3,
"device_count": 7,
"period_start": 1717120000
}
GET /allowlist
[
{
"id": "1717123456789012345",
"pattern": "*.telemetry.microsoft.com",
"created_at": 1717123456
}
]
PUT /allowlist
Request: { "pattern": "*.example.com" }
Response: 201 { "status": "ok", "id": "..." }
DELETE /allowlist/<id>
200 {"status":"ok"} or 404
GET /debug
Raw internal state. Useful for diagnosing empty-log problems.
{
"dns_requests": {"count": 42, "last_5": [...]},
"device_tracker": {"device_count": 5, "devices": [...]},
"dns_last_error": null,
"dns_sock_bound": true
}
GET /token
Truncated device token hint. Full token never exposed publicly.
{"status": "ok", "token_hint": "a1b2c3d4"}
Returns 404 if no token exists.
Not implemented (MVP)
GET /config,POST /config. Wi-Fi changes via USB reprovision onlyGET /devices/<id>
CLI Tool
A terminal client (heron) lives in heron/backend/heron-cli/. It talks to
the same HTTP API as the dashboard and adds capabilities the web UI can't
offer: per-device live streams, snapshot-based change detection, exit-code
monitoring for cron jobs, and JSON output for piping to other tools.
Install: pip install ./heron-cli (from the backend/ directory)
Commands:
| Command | Description |
|---|---|
heron | Pretty summary (stats, devices, activity) |
heron report --table | Compact table format |
heron report --json | Raw JSON output (pipeable) |
heron report --compact | One-line status |
heron watch | Live refresh every 5s |
heron follow <ip> | Live DNS query stream from one device |
heron diff | What changed since last run |
heron monitor | Silent check: exit 0 if OK, exit 1 if alert |
heron allow add/rm/ls | Allowlist management |
heron debug | Raw /debug dump |
heron --host <ip> | Manual device IP |
Source: heron/backend/heron-cli/heron_cli/cli.py
Dependencies: Python 3.8+ stdlib only. Zero external packages.
Data Adaptation Layer
The official dashboard (heron-dashboard/src/lib/api/adapters.ts) transforms firmware MVP responses into rich UI types:
- Seconds → milliseconds (
timestamp * 1000) sourceIP →deviceIdvia/devicescache- Generated event IDs from
source + domain + timestamp - Defaults for
severity,kind,vendor,bytes, etc.
Optional for third-party clients; required for the official dashboard UI.
Error Responses (HTTP)
200OK201Created (allowlist)204CORS preflight400Bad request404Not found503MemoryError during serialization
Changelog
| Date | Version | Change |
|---|---|---|
| 2026-05-31 | v0.1.0 | Initial draft |
| 2026-06-01 | v0.2.0 | Heron B: WebSerial + /activate + weekly audit |
| 2026-06-07 | v0.2.1 | Data Adaptation Layer section |
| 2026-06-07 | v0.2.2 | App separation, ready beacon, onboarding flow, dashboard discovery |
| 2026-06-20 | v0.3.0 | Cryptographic device attestation replaces cloud activation. challenge command added. provision no longer needs sticker code. Cloud /activate removed. Fully offline. |
| 2026-06-22 | v0.4.0 | heron CLI tool added (heron-cli/heron_cli/cli.py). /debug and /token endpoints documented. CLI adds: report (table/json/compact), watch, follow, diff, monitor, allow, debug. |