Shared Concepts
The behavior every Honch SDK shares — events, identity, automatic properties, lifecycle, queueing, retries, and upload.
Every Honch SDK is a thin platform port wrapped around one portable C core. The core owns the behavior described on this page, so it is identical across ESP-IDF, C/POSIX, MicroPython, and Arduino. Ports only supply storage, transport, timing, and randomness. When you read "the SDK does X" below, X lives in the core and holds everywhere.
The Event Model
An event is an event name, an on-device timestamp, a distinct_id, and a list of typed properties.
| Part | Rules |
|---|---|
| Event name | Non-empty, up to 128 bytes. |
| Properties | Up to 64 per event. Keys are strings; values are typed (see below). |
| Event size | An encoded event over max_event_bytes (default 8192) is rejected when you track it. |
| Timestamp | Assigned by the SDK at track() time — never upload time. See Timestamps. |
Property values are typed, not stringly-typed. The supported types are null, boolean, unsigned integer, signed integer, 32- and 64-bit float, string, bytes, array, and map. Each port exposes constructors for these (for example honch_str(...), honch_i64(...), honch_prop(key, value) in C).
Property Precedence
When an event is assembled, properties are merged in this order: your event properties first, then the SDK's reserved context, then any port-supplied automatic properties, then $battery_level. Two rules matter:
- Reserved keys are rejected, not overwritten. If your event properties include a reserved key (any
$-prefixed SDK key, ordistinct_id), thetrack()call fails with an invalid-argument error. The SDK does not silently drop or replace your value — it refuses the event so the mistake is visible. - Duplicate keys are rejected. Passing the same key twice in one event is an error.
Timestamps
The timestamp is stamped when you call track(). If the device clock reads a real wall-clock time (at or after 2020-01-01), that time is used. If the clock has not been set yet — common right after boot, before NTP/SNTP — the SDK records a boot-relative time and normalizes it to real time at flush, once a real clock is available. Each upload records which clock produced its timestamps so Honch can interpret them correctly. The takeaway: event time reflects when the event happened on the device, not when it was uploaded or relayed.
Identity
Honch tracks two identifiers:
| Identifier | Meaning |
|---|---|
$device_id | Stable hardware identity. Either the value you configure, or one the SDK generates and persists on first run. |
distinct_id | Who the events currently belong to. Until you identify the device, it equals $device_id. |
Calling identify(distinct_id, traits) sets a new distinct_id, persists it, and emits an $identify event whose $anon_distinct_id property carries the previous distinct_id. That property is what lets Honch merge the device's earlier anonymous activity into the identified person downstream — the merge happens in Honch, not on the device. Traits you pass are ordinary user properties on the $identify event.
reset() clears stored identity, the session, and the local queue. It does not emit an event. If you configured a fixed device_id, identity returns to that value; otherwise the SDK mints a new random $device_id. Use it at a factory-reset or user-logout boundary.
Automatic Properties
The SDK attaches context to every event so you do not have to. These are always present:
$device_id, $device_model, $firmware_version, $sdk_platform, $sdk_version, $environment.
$environment defaults to production when you do not set it. These are conditional:
| Property | Present when |
|---|---|
$session_id | A session is active (between session_start and session_end). |
$battery_level | You configure a battery callback; value is an integer 0–100. |
$wifi_rssi | Your port's automatic-properties callback supplies it (integer dBm). It is the only reserved key a callback may set. |
The core never auto-detects Wi-Fi signal, heap, or uptime. $free_heap_bytes, $uptime_seconds, and $hardware_revision are explicitly not stamped — send them as your own properties if you want them.
Lifecycle Events
The SDK emits these automatically. Be aware that simply initializing the SDK produces wire traffic.
| Event | When | Notable properties |
|---|---|---|
$device_boot | End of init | reset_reason |
$firmware_update | At init, when the stored firmware version differs from the configured one | previous_version, new_version |
$battery_low | After a tracked event, when battery is below your threshold — edge-triggered (once until it recovers) | level |
$session_start | session_start() | session_name (when non-empty) |
$session_end | session_end(), and automatically before a new session starts | — |
$device_shutdown | Start of shutdown() | — |
$identify | identify() | $anon_distinct_id + your traits |
$set_property | set_property() | the single key/value you set |
$crash | At init after an abnormal reset, when error tracking is enabled | reset_reason, severity, crash_id, and — depending on platform — backtrace, coredump_available |
$error | A non-fatal error is captured: automatically from error logs (ESP-IDF) or via an explicit error-report call | component, severity, message |
Note $battery_low is edge-triggered: it fires once when the battery drops below the threshold and will not fire again until the level recovers above it.
Crash And Error Reporting
Honch reports both fatal crashes (a $crash event) and non-fatal errors (an $error event). Both are built on the shared core, so every port can report that a crash or error happened — but how much detail is captured, and how much is automatic, depends on the platform.
| Platform | Fatal crash ($crash) | Crash detail captured | Non-fatal error ($error) |
|---|---|---|---|
| ESP-IDF (ESP32) | Automatic — abnormal reset detected at next boot (enable_error_tracking) | Full coredump → symbolicated backtrace (enable_crash_symbolication) | Automatic — ESP_LOGE lines become $error |
| MicroPython | Uncaught Python exceptions — wrap your entry point with client.run(main), or install_error_hook() where available | Python traceback (file/line/function) | Explicit — report_log_error() |
| C / POSIX | Automatic via signal handlers (honch_install_error_handlers()) | Reset/signal context (no coredump) | Explicit — honch_core_report_log_error() |
| Arduino (ESP32) | Automatic — abnormal reset detected at next boot (enableErrorTracking) | Reset reason (no coredump) | Explicit — core API |
Platform caveats — read before relying on these
- Full coredumps are ESP-IDF only. ESP32 captures the device's memory + registers at the moment of the crash, and the backend resolves it to a function/file/line backtrace. POSIX, MicroPython, and Arduino do not capture a coredump.
- The readable backtrace differs by platform. ESP-IDF gets a symbolicated coredump backtrace; MicroPython gets the Python traceback; POSIX and Arduino get the
$crashevent (that it happened, plus reset/signal context) but no deep backtrace. - Automatic error-log capture is ESP-IDF only. On the other ports an
$erroris emitted only when your code calls the explicit error-report API (for example, wired into your logging) — nothing is captured automatically. - MicroPython captures crashes by wrapping your entry point with
client.run(main)(works on every build) or viainstall_error_hook()(which needssys.excepthook— stock firmware like the Pico W omits it, and the hook then returnsFalse). Preferrun()unless your firmware enables the hook. - Delivery timing differs by platform. A
$crashis queued, not sent synchronously. ESP-IDF and Arduino re-derive it from the hardware reset reason on the next boot (and the ESP-IDF coredump lives on a flash partition), so it survives the reset; POSIX persists the crash to its queue directory. MicroPython reports in-process, so a fatal crash is delivered only if a flush completes before the board resets — or you supply a durable queue. The default RAM queue does not survive a reset.
Queueing And Durability
The default queue is a bounded, RAM-backed, drop-oldest buffer you own (you provide the backing memory). When the queue is full, the oldest event is dropped to make room for the newest. The defaults:
| Setting | Default | Notes |
|---|---|---|
batch_size | 20 | Events per upload batch. Hard cap 50. |
max_queued_events | 1000 | Entries retained before drop-oldest. |
max_event_bytes | 8192 | Largest single encoded event. |
flush_interval_seconds | 120 | Periodic flush cadence. |
flush_event_threshold | 20 | Queue depth that requests a flush. |
flush_min_interval_ms | 15000 | Minimum spacing between upload attempts. |
transport_timeout_ms | 8000 | Per-request timeout. |
flush_retry_initial_ms | 1000 | First retry backoff. |
flush_retry_max_ms | 300000 | Backoff ceiling (5 minutes). |
battery_low_threshold | 15 | $battery_low trigger level. |
Dropping the oldest entry is cheap; dropping a non-tail entry compacts the buffer and costs O(n). Keep per-event work bounded on hot paths.
Durability is a port concern. The RAM queue is volatile — a reset or power loss clears it. Ports that offer persistence (file-backed on C/POSIX, opt-in NV-backed queues on the device ports) carry events across restarts. Two durability modes exist where persistence applies: OS_BUFFERED (default, no per-write fsync) and SYNC_ALWAYS (fsync each write, safest against power loss, slowest). Each SDK page documents what its port does by default.
A queued event stays pending until Honch confirms delivery. It is removed from the queue only on an accepted upload — not when the request is merely sent.
Flushing And Retry
Uploads are cooperative. The SDK has no background thread: you call tick() periodically to let it make progress (it sends at most one chunk per tick), and flush() to push batches now. Both do network I/O synchronously on the calling thread and can block up to transport_timeout_ms, so pump them from a task you control, never from an ISR or a latency-sensitive path.
The SDK classifies every upload result:
| Result | HTTP | Queue action |
|---|---|---|
| Accepted | 204 | Consume the batch. |
| Chunk stored | 202 | Keep sending the remaining chunks. |
| Retryable | 408, 409, 429, 5xx, transport failure | Preserve events; back off and retry. |
| Permanent | 401 (auth), other 4xx | Stop retrying — dead-letter or drop per the port. |
Retries use exponential backoff from flush_retry_initial_ms to flush_retry_max_ms with ±25% jitter, honoring a server Retry-After when present. Retryable failures never lose events; permanent rejections move the batch out of the way so it cannot block the queue forever.
The Upload Contract
Device SDKs upload the compact binary chunk format to a single endpoint:
POST /capture
Content-Type: application/vnd.honch.chunk
X-Honch-Project-Key: <project_api_key>
X-Honch-Stream-Id: <stream_id>The project key is a transport credential sent as a header, never in the body. X-Honch-Stream-Id ties together the chunks of a message that spans more than one frame. The full byte grammar is on the Wire Format page; you do not need it to use an SDK.
Relay Flow
Some devices cannot reach the internet directly and instead talk to a phone or gateway over BLE or serial. In that model:
- The device encodes a compact message and splits it into relay frames.
- It sends those frames over BLE/serial to a companion app or gateway.
- The relay reassembles the complete message, durably stores it, and acknowledges receipt to the device.
- The relay uploads the message to Honch, adding
X-Honch-Relay-*headers that identify the relay.
The relay must preserve the device's compact message bytes exactly; it may re-chunk for its own transport. The BLE/serial relay framing is a different format from the HTTP chunk frame — see the relay packages (React Native, Swift) and the relay-chunks spec.
Verify Your Integration
Whatever the SDK, the first checkpoint is the same:
| Step | Proof |
|---|---|
| SDK initializes | Init returns success. |
| Identity exists | A device ID is configured, generated, or persisted. |
| Event queues | Your first track() is accepted locally. |
| Flush attempts upload | A POST /capture is made. |
| Failure is understood | Retryable failures stay pending; permanent rejections drop or dead-letter. |
Once those hold, add product events, identity, sessions, and the rest.