GitHub

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.

PartRules
Event nameNon-empty, up to 128 bytes.
PropertiesUp to 64 per event. Keys are strings; values are typed (see below).
Event sizeAn encoded event over max_event_bytes (default 8192) is rejected when you track it.
TimestampAssigned 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, or distinct_id), the track() 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:

IdentifierMeaning
$device_idStable hardware identity. Either the value you configure, or one the SDK generates and persists on first run.
distinct_idWho 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:

PropertyPresent when
$session_idA session is active (between session_start and session_end).
$battery_levelYou configure a battery callback; value is an integer 0–100.
$wifi_rssiYour 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.

EventWhenNotable properties
$device_bootEnd of initreset_reason
$firmware_updateAt init, when the stored firmware version differs from the configured oneprevious_version, new_version
$battery_lowAfter a tracked event, when battery is below your threshold — edge-triggered (once until it recovers)level
$session_startsession_start()session_name (when non-empty)
$session_endsession_end(), and automatically before a new session starts
$device_shutdownStart of shutdown()
$identifyidentify()$anon_distinct_id + your traits
$set_propertyset_property()the single key/value you set
$crashAt init after an abnormal reset, when error tracking is enabledreset_reason, severity, crash_id, and — depending on platform — backtrace, coredump_available
$errorA non-fatal error is captured: automatically from error logs (ESP-IDF) or via an explicit error-report callcomponent, 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.

PlatformFatal crash ($crash)Crash detail capturedNon-fatal error ($error)
ESP-IDF (ESP32)Automatic — abnormal reset detected at next boot (enable_error_tracking)Full coredump → symbolicated backtrace (enable_crash_symbolication)AutomaticESP_LOGE lines become $error
MicroPythonUncaught Python exceptions — wrap your entry point with client.run(main), or install_error_hook() where availablePython traceback (file/line/function)Explicit — report_log_error()
C / POSIXAutomatic 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 $crash event (that it happened, plus reset/signal context) but no deep backtrace.
  • Automatic error-log capture is ESP-IDF only. On the other ports an $error is 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 via install_error_hook() (which needs sys.excepthook — stock firmware like the Pico W omits it, and the hook then returns False). Prefer run() unless your firmware enables the hook.
  • Delivery timing differs by platform. A $crash is 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:

SettingDefaultNotes
batch_size20Events per upload batch. Hard cap 50.
max_queued_events1000Entries retained before drop-oldest.
max_event_bytes8192Largest single encoded event.
flush_interval_seconds120Periodic flush cadence.
flush_event_threshold20Queue depth that requests a flush.
flush_min_interval_ms15000Minimum spacing between upload attempts.
transport_timeout_ms8000Per-request timeout.
flush_retry_initial_ms1000First retry backoff.
flush_retry_max_ms300000Backoff ceiling (5 minutes).
battery_low_threshold15$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:

ResultHTTPQueue action
Accepted204Consume the batch.
Chunk stored202Keep sending the remaining chunks.
Retryable408, 409, 429, 5xx, transport failurePreserve events; back off and retry.
Permanent401 (auth), other 4xxStop 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:

  1. The device encodes a compact message and splits it into relay frames.
  2. It sends those frames over BLE/serial to a companion app or gateway.
  3. The relay reassembles the complete message, durably stores it, and acknowledges receipt to the device.
  4. 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:

StepProof
SDK initializesInit returns success.
Identity existsA device ID is configured, generated, or persisted.
Event queuesYour first track() is accepted locally.
Flush attempts uploadA POST /capture is made.
Failure is understoodRetryable failures stay pending; permanent rejections drop or dead-letter.

Once those hold, add product events, identity, sessions, and the rest.

honch.

Product analytics for consumer hardware.