# Build Your Own Integration (/byo-integration) You do not need an official SDK to use Honch. If your product already moves events through your own app or backend, you can integrate directly against the [HTTP JSON API](/http-api). This guide walks through a complete client, using a representative topology: a device produces events, your mobile app collects and forwards them, and your app uploads to Honch Capture. ```text on-device events -> your mobile app (collect, batch, retry) -> POST https://i.honch.io/capture ``` The mobile app owns identity, batching, retries, and the project key. The device only needs to hand events to the app. ## Choose JSON Or The Binary Wire Format [#choose-json-or-the-binary-wire-format] | Use JSON when | Use the binary wire format when | | ---------------------------------------- | ------------------------------------------------------------------------- | | You can make ordinary HTTPS requests. | The device transport is severely bandwidth- or power-constrained. | | You want a readable, debuggable payload. | Every byte and wakeup matters (low-power radios, metered links). | | You want descriptive validation errors. | You are relaying opaque frames from firmware that cannot upload directly. | JSON is the recommended default. It expands to the exact same canonical event as the binary format, so you lose nothing on the analytics side by choosing it. Only reach for the [wire format](/wire-format) when the transport constraints genuinely require it. ## Identity Model [#identity-model] There are three IDs **you** send, plus one Honch manages for you. Getting the ones you send right is what makes a device's history follow a user. | ID | Who sets it | Lifetime | What it is | | ------------- | ---------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `distinct_id` | you (in `context`) | changes at identify | The identity each event is attributed to. **Starts as the device id**, becomes the user id after you identify. | | `$device_id` | you (in `context`) | the device's life (new id on factory reset) | The physical hardware. Independent of `distinct_id`. | | `$session_id` | you (in `context`, optional) | one logical session | A recording, workout, trip, etc. | | `person_id` | Honch (server-side) | — | The canonical person every `distinct_id` resolves to. **You never send it** — Honch mints it. You only *see* it as the person's id in the dashboard, and you influence it indirectly through `$identify`. | You only ever manage `distinct_id` (and `$device_id` / `$session_id`). `person_id` is Honch's internal grouping of all the `distinct_id`s that belong to one person; a `$identify` is how you tell Honch that two `distinct_id`s are the same person, and Honch merges them under one `person_id`. ### Anonymous, then identified [#anonymous-then-identified] Before you know who the user is, send events with `context.distinct_id` set to the **device id** (the same value as `$device_id`). Honch creates an anonymous person for that id. When the user signs in, you must **send a `$identify` event** — this is the only thing that links the anonymous device history to the user. Set `context.distinct_id` to the user id and put the *previous* (device) id in the event's `$anon_distinct_id` property: ```json { "context": { "distinct_id": "user-98234", "$device_id": "device-abc", "$device_model": "pocket-cam-1", "$firmware_version": "1.4.2", "$sdk_platform": "pocket-ios", "$sdk_version": "0.1.0" }, "events": [ { "event": "$identify", "properties": { "$anon_distinct_id": "device-abc", "$set": { "email": "sam@example.com", "plan": "pro" }, "$set_once": { "signup_source": "app" } } } ] } ``` Honch then **merges** the anonymous device person into `user-98234`: every past and future event under either id resolves to the same person. After this, send subsequent events with `context.distinct_id = "user-98234"`. > **Important:** Just *switching* `distinct_id` from the device id to the user id **without** a `$identify` event does **not** stitch anything — it creates a second, unconnected person. The `$anon_distinct_id` property on a `$identify` event is what performs the merge. Because `context.distinct_id` is shared by every event in a request, don't mix identities in one request: flush the device-id events first, then send the `$identify` request, then continue under the user id. ### Setting person properties without identifying [#setting-person-properties-without-identifying] To attach properties to the current person without an identity change, send a `$set` event with `$set` / `$set_once` (e.g. for a hardware-only device with no user login): ```json { "context": { "distinct_id": "device-abc", "$device_id": "device-abc", "...": "..." }, "events": [ { "event": "$set", "properties": { "$set": { "region": "us-west" } } } ] } ``` `$set` overwrites existing values; `$set_once` only fills gaps. Both are also accepted inside a `$identify` event (shown above). ### Devices [#devices] `$device_id` is tracked independently of identity: Honch keeps a device record per `$device_id` and links it to the most recent person seen on it. One person can own several devices (each sends its own `$device_id`); a device sold to a new owner gets a new `$device_id` on factory reset and starts fresh. See [Shared Concepts](/concepts) for the full model. ## Emit Lifecycle Events [#emit-lifecycle-events] Honch recognizes a set of standard lifecycle events. Emitting them gives you device health and engagement analytics for free, and they expand the same way any other event does. Send them as ordinary events with the property names below. | Event | When to emit | Properties | | ---------------------- | ------------------------------------------------- | ---------------------------------------------------------------------- | | `$device_boot` | Device or app comes up | `reset_reason` (string) | | `$session_start` | A user session begins | `session_name` (string, optional) | | `$session_end` | A user session ends | — | | `$firmware_update` | Firmware or app version changed | `previous_version`, `new_version` | | `$battery_low` | Battery drops below your threshold | `level` (int) | | `$connectivity_change` | Network state changes | `state` (string) | | `$crash` | The device recovered from an abnormal reset/crash | `reset_reason`, `severity`, `crash_id` (+ `backtrace` if you have one) | | `$error` | An actionable recoverable error occurs | your own diagnostic properties | | `$device_reset` | Device returned to a factory state | — | | `$device_shutdown` | Device or app shuts down | — | The reserved lifecycle property names (`reset_reason`, `session_name`, `previous_version`, `new_version`, `state`, `$battery_level`, `$wifi_rssi`) are allowed as event properties. They are not context keys, so put them inside the event's `properties`. The promoted context keys (`$device_id`, `distinct_id`, and so on) are the ones you must not set per-event. For the canonical definitions, read [Shared Concepts](/concepts) and the [auto-properties spec](https://github.com/honch-io/SDK/tree/main/spec/auto-properties.md). ## Batch Events [#batch-events] Collect events in your app and send them together rather than one request per event. * Up to 500 events per request. Split larger queues across requests. * Declare `context` once per request; it applies to the whole batch. That means every event in a single request shares one `distinct_id`. If your batch spans an identity change, split it at the boundary. * Flush on a timer (for example every 30-60 seconds), when the queue reaches a threshold, on app background, and before shutdown. ```json { "context": { "distinct_id": "user-42", "$device_id": "device-abc", "$device_model": "pocket-cam-1", "$firmware_version": "1.4.2", "$sdk_platform": "pocket-ios", "$sdk_version": "0.1.0" }, "events": [ { "event": "$session_start", "timestamp": 1700000000000, "properties": { "session_name": "edit" } }, { "event": "video_exported", "timestamp": 1700000005000, "properties": { "duration_ms": 5000 } }, { "event": "$session_end", "timestamp": 1700000060000 } ] } ``` ## Retry And Backoff [#retry-and-backoff] Keep events in a local queue until Capture accepts them, and classify each response: | Response | Action | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `200` | At least one event was stored — remove the batch from your queue. If `rejected > 0`, the events in `errors` were permanently dropped (bad data); log them, don't retry them. | | `429`, `5xx`, network/timeout error | Retryable. Keep the batch and retry with backoff. | | `400`, `401`, `415`, `422` | Permanent. Nothing was stored; fix the request, key, or content type first. | The contract is simple: **`2xx` means at least one event was stored; `4xx` means nothing was.** Capture accepts the valid events in a batch and reports the bad ones rather than failing the whole batch, so a single malformed event never blocks the rest of a device's data. A whole-batch `422` only happens when the shared `context` is wrong, the batch is empty/too large, or *every* event was individually invalid. Use exponential backoff with jitter, matching the official SDK policy: * Initial delay: 1 second. * Maximum delay: 5 minutes. * Jitter: plus or minus 25% on each delay. ```ts function nextBackoffMs(attempt: number): number { const base = Math.min(1000 * 2 ** attempt, 5 * 60 * 1000); // 1s -> cap 5min const jitter = base * 0.25 * (Math.random() * 2 - 1); // +/- 25% return Math.max(0, Math.round(base + jitter)); } ``` On a permanent `4xx`, do not loop on the same batch. Log it, drop or dead-letter it, and fix the cause. The [error table](/http-api#errors) tells you exactly which code you hit and why. ## Validate Before Launch [#validate-before-launch] Before you send live data, point your client at `POST https://i.honch.io/capture/validate` instead of `/capture`. It authenticates and runs the full validation and expansion, returns the canonical events it would store, and surfaces every error — without writing anything or consuming rate limit. ```bash curl -sS https://i.honch.io/capture/validate \ -H "Content-Type: application/json" \ -H "X-Honch-Project-Key: honch_your_project_key" \ -d @payload.json ``` Iterate until the response is `{ "ok": true, ... }` and the `expanded_events` match what you expect, then switch the URL to `/capture`. You can also assert your client against the shared [JSON conformance fixtures](https://github.com/honch-io/SDK/tree/main/spec/conformance/json), which pin the request/response contract case by case. See the [validation workflow](/http-api#validate-before-you-send) for the full response shape. ## Verify It Worked [#verify-it-worked] After you switch to `/capture` and get `{ "status": "ok", "accepted": N }`, open your project's live events feed in the Honch dashboard. Your events appear there shortly after ingest, with the promoted context (`$device_id`, `$device_model`, and so on) attached to each one. If they do not show up, work through the [FAQ](/faq): confirm the key is active and scoped, the content type is `application/json`, and you are not seeing a `4xx` you treated as retryable. ## Reference Client [#reference-client] A reference implementation of this guide — modeling exactly the device → companion app → capture relay, with durable offline queueing — lives in the open-source [SDK repository](https://github.com/honch-io/SDK) (which also holds the wire-format spec and conformance fixtures; the SDK and contract are open source, while the Honch platform itself is a hosted service): * [TypeScript reference client](https://github.com/honch-io/SDK/tree/main/examples/http-json/typescript) — zero-dependency, uses `fetch`, with `identify`, lifecycle helpers, retry/backoff, and pluggable durable persistence (`initialQueue` / `onQueueChange`) so the pending queue survives an app restart. ## Next Steps [#next-steps] # Shared Concepts (/concepts) 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 [#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](#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 [#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 [#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 [#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 [#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 [#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 [#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 | * **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 [#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 [#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 [#the-upload-contract] Device SDKs upload the compact binary chunk format to a single endpoint: ```text POST /capture Content-Type: application/vnd.honch.chunk X-Honch-Project-Key: X-Honch-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](/wire-format) page; you do not need it to use an SDK. ## Relay Flow [#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](/sdks/react-native-relay), [Swift](/sdks/swift-relay)) and the relay-chunks spec. ## Verify Your Integration [#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. # FAQ (/faq) For step-by-step fixes, see [Troubleshooting](/troubleshooting). For the full behavior, see [Shared Concepts](/concepts). ## Which SDK should I use? [#which-sdk-should-i-use] Match it to your platform: ESP-IDF for ESP32 firmware, C/POSIX for embedded Linux and gateways (and local development), MicroPython for MicroPython firmware, Arduino for ESP32 Arduino sketches (preview). If a device cannot reach the internet, pair it with the React Native relay. If none fit, send events directly with the [HTTP API](/http-api). ## I sent an event. What should happen? [#i-sent-an-event-what-should-happen] Initialization queues `$device_boot` immediately. Your `track()` adds an event to the local queue. On the next `tick()`/`flush()` while online, the SDK posts to `/capture` and Capture returns `204` (batch accepted) or `202` (chunk stored). The event then appears in your project's live events. ## What endpoint do the SDKs use? [#what-endpoint-do-the-sdks-use] ```text POST /capture Content-Type: application/vnd.honch.chunk X-Honch-Project-Key: X-Honch-Stream-Id: ``` The default host is `https://i.honch.io`. Direct HTTP integrations post JSON to the same `/capture` path. ## Should I use HTTPS? [#should-i-use-https] Yes, in production. The device ports verify the server certificate and provide no production way to disable it. Use plain HTTP only when intentionally talking to a local capture service during development. ## What gets collected automatically? [#what-gets-collected-automatically] A fixed context on every event: `$device_id`, `$device_model`, `$firmware_version`, `$sdk_platform`, `$sdk_version`, `$environment`. Conditionally: `$session_id` during a session, `$battery_level` if you supply a battery callback, and `$wifi_rssi` if your port supplies it. The SDK does not auto-collect heap, uptime, hardware revision, or location. ## Can user properties override SDK properties? [#can-user-properties-override-sdk-properties] No. If an event includes a reserved key (an SDK-owned `$` key or `distinct_id`), the `track()` call is **rejected** with an invalid-argument error. Reserved keys are never silently overwritten — rename your property. ## What property values can I pass? [#what-property-values-can-i-pass] Typed values: null, boolean, integer, float, string, bytes, arrays, and maps (nested). Up to 64 properties per event; event names up to 128 bytes. ## Which errors are retryable? [#which-errors-are-retryable] `408`, `409`, `429`, `5xx`, and transport failures are retryable — events stay queued and retry with backoff (1 s to 5 min, ±25% jitter). `401` and other `4xx` are permanent; the batch is dropped or dead-lettered. ## What happens when the device is offline? [#what-happens-when-the-device-is-offline] Events stay queued. Supply a connectivity callback so the SDK skips DNS/TLS work while offline; when connectivity returns, queued events flush with backoff. Nothing is lost as long as the queue (RAM by default, drop-oldest past `max_queued_events`) has not overflowed or been cleared by a reset. ## What happens on reset? [#what-happens-on-reset] `reset()` clears identity, the active session, and the local queue, and emits no event. With a configured `device_id`, identity returns to it; otherwise the SDK generates a new random `$device_id`. Use it at a factory-reset or logout boundary. ## Why does MicroPython need a user C module? [#why-does-micropython-need-a-user-c-module] The MicroPython wrapper is a thin Python layer over the same C core, bound through the `_honch_core` user C module. The module must be compiled into your firmware — there is no pure-Python implementation. See the [MicroPython guide](/sdks/micropython). ## Can I call `track()` from an ISR? [#can-i-call-track-from-an-isr] No. `track()` can allocate, and `tick()`/`flush()` do blocking network I/O. From an ISR, push minimal data (like a pin number) onto a queue and call `track()` from a normal task. See the [ESP-IDF GPIO pattern](/sdks/esp-idf#5-track-gpio-safely). ## How does the relay acknowledge a device? [#how-does-the-relay-acknowledge-a-device] Your app hands each BLE frame to the relay; the relay durably stores it and returns ACK bytes (a version byte plus a big-endian sequence number). Your app writes those bytes to the device's ACK characteristic. The relay never touches Bluetooth itself — that stays host-owned. # HTTP JSON API (/http-api) The Capture JSON API is a standards-friendly front door for clients that build their own integration instead of using an official SDK. You send plain JSON over HTTPS and Capture expands it into the exact same canonical events the binary wire format produces, then runs it through the same enrichment and storage pipeline. If you are integrating from a phone, a backend, or any host that can make an HTTPS request, this is the path to use. For severely bandwidth- or power-constrained device transports, see [Wire Format](/wire-format). For a narrative walkthrough of building a client end to end, see [Build Your Own Integration](/byo-integration). ## Endpoint And Authentication [#endpoint-and-authentication] ```text POST https://i.honch.io/capture Content-Type: application/json X-Honch-Project-Key: ``` | Item | Value | | ------------ | ------------------------------------------------------- | | Method | `POST` | | URL | `https://i.honch.io/capture` (aliases: `/e`, `/chunks`) | | Content type | `application/json` | | Auth header | `X-Honch-Project-Key: ` | The project key looks like `honch_...`. It must be active and carry the `capture` or `all` scope. There is no token in the request body; authentication is the header only. The same URL also accepts the binary wire format under `Content-Type: application/vnd.honch.chunk`. Capture branches on the content type, so you do not need a different path for each format. ## Request Body [#request-body] ```json { "context": { "distinct_id": "user-or-device-id", "$device_id": "device-abc", "$device_model": "pocket-cam-1", "$firmware_version": "1.4.2", "$sdk_platform": "pocket-ios", "$sdk_version": "0.1.0", "$environment": "production", "$session_id": "session-xyz" }, "events": [ { "event": "video_exported", "timestamp": 1700000000000, "properties": { "duration_ms": 5000, "resolution": "4k" } } ] } ``` ### Context Keys [#context-keys] `context` is declared once per request and applies to every event in the batch. Only the keys below are accepted. | Key | Required | Type | Description | | ------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------- | | `distinct_id` | Required | string (non-empty) | The analytics identity for every event in the request. Becomes the top-level `distinct_id` field, not a property. | | `$device_id` | Required | string (non-empty) | Stable device identifier. | | `$device_model` | Required | string (non-empty) | Hardware or product model. | | `$firmware_version` | Required | string (non-empty) | Firmware or app version. | | `$sdk_platform` | Required | string (non-empty) | Your platform tag, for example `pocket-ios`. | | `$sdk_version` | Required | string (non-empty) | Your client version. | | `$environment` | Optional | string (non-empty) | Defaults to `production` when omitted. | | `$session_id` | Optional | string (non-empty) | Present only when a session is active. | Any context key not in this table is rejected with `422` and code `unknown_context_key`. Put additional dimensions in per-event `properties` instead of inventing context keys. The request body itself accepts only `context` and `events` — an unexpected top-level field fails the whole request with `400` `invalid_json`. ### Event Fields [#event-fields] `events` is an array of one to 500 events. | Field | Required | Type | Description | | ------------ | -------- | ------------------ | --------------------------------------------------------------------------------------------------- | | `event` | Required | string (non-empty) | The event name, for example `video_exported`. | | `timestamp` | Optional | integer or string | Epoch **milliseconds** (integer) or an RFC3339 string. Omit it and Capture stamps the receive time. | | `properties` | Optional | object | Custom per-event properties. | ## Context Promotion [#context-promotion] `context` is promoted into every event before storage: * `distinct_id` becomes the event's top-level identity field. * Every other context key (`$device_id`, `$device_model`, `$firmware_version`, `$sdk_platform`, `$sdk_version`, `$environment`, and `$session_id` when present) is copied into each event's `properties`. This mirrors the binary wire format exactly: a JSON request expands to the same canonical event as the equivalent binary message. You declare device context once and it rides along with each event automatically. Because those keys are set from context, a per-event property **must not** reuse a promoted key. Sending `$device_id` (or any other promoted key) inside an event's `properties` is rejected with `422` and code `reserved_property`. Use a different property name if you need a custom value. Reserved **lifecycle** property names are allowed as event properties, because they describe an event rather than device context: | Property | Type | Used by | | ------------------ | -------------- | ----------------------------------- | | `$battery_level` | number (0-100) | Any event — current battery state | | `$wifi_rssi` | number (dBm) | Any event — current signal strength | | `reset_reason` | string | `$device_boot` | | `state` | string | `$connectivity_change` | | `previous_version` | string | `$firmware_update` | | `new_version` | string | `$firmware_update` | | `session_name` | string | `$session_start` | `$battery_level` and `$wifi_rssi` must be **numbers** — a non-numeric value (e.g. `"low"`) rejects that event with `invalid_property_value`, so hardware metrics stay clean. The string lifecycle properties accept any string. For the full event model, identity, and the list of recommended lifecycle events, read [Shared Concepts](/concepts). ## Identifying People And Setting Properties [#identifying-people-and-setting-properties] `distinct_id` starts as the device id (an anonymous person). To tie that history to a known user, send a **`$identify` event** — not just a new `distinct_id`. Set `context.distinct_id` to the user id and name the previous (device) id in `$anon_distinct_id`; Honch merges the anonymous person into the user. These are reserved property names with special server-side meaning (they are allowed as event properties, unlike promoted context keys): | Property | On event | Meaning | | ------------------- | ------------------- | --------------------------------------------------------------------------------------- | | `$anon_distinct_id` | `$identify` | The previous `distinct_id` (usually the device id) to merge into `context.distinct_id`. | | `$set` | `$identify`, `$set` | Object of person properties to set (overwrites existing keys). | | `$set_once` | `$identify`, `$set` | Object of person properties to set only if not already present. | ```json { "context": { "distinct_id": "user-98234", "$device_id": "device-abc", "$device_model": "pocket-cam-1", "$firmware_version": "1.4.2", "$sdk_platform": "pocket-ios", "$sdk_version": "0.1.0" }, "events": [ { "event": "$identify", "properties": { "$anon_distinct_id": "device-abc", "$set": { "email": "sam@example.com", "plan": "pro" } } } ] } ``` To set person properties without an identity change, send a `$set` event with `$set` / `$set_once` and no `$anon_distinct_id`. Identity resolution and merging happen downstream of capture; the [Build Your Own Integration](/byo-integration#identity-model) guide walks through the full flow and the four IDs (`distinct_id`, `$device_id`, `$session_id`, server-side `person_id`). ## Examples [#examples] ### curl [#curl] ```bash curl -sS https://i.honch.io/capture \ -H "Content-Type: application/json" \ -H "X-Honch-Project-Key: honch_your_project_key" \ -d '{ "context": { "distinct_id": "device-abc", "$device_id": "device-abc", "$device_model": "pocket-cam-1", "$firmware_version": "1.4.2", "$sdk_platform": "pocket-ios", "$sdk_version": "0.1.0" }, "events": [ { "event": "app_started" }, { "event": "video_exported", "timestamp": 1700000000000, "properties": { "duration_ms": 5000, "resolution": "4k" } } ] }' ``` ### TypeScript (`fetch`) [#typescript-fetch] ```ts const CAPTURE_URL = "https://i.honch.io/capture"; const PROJECT_KEY = "honch_your_project_key"; const payload = { context: { distinct_id: "device-abc", $device_id: "device-abc", $device_model: "pocket-cam-1", $firmware_version: "1.4.2", $sdk_platform: "pocket-ios", $sdk_version: "0.1.0", }, events: [ { event: "app_started" }, { event: "video_exported", timestamp: Date.now(), properties: { duration_ms: 5000, resolution: "4k" }, }, ], }; const response = await fetch(CAPTURE_URL, { method: "POST", headers: { "Content-Type": "application/json", "X-Honch-Project-Key": PROJECT_KEY, }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`capture failed: ${response.status}`); } console.log(await response.json()); // { status: "ok", accepted: 2, rejected: 0, errors: [] } ``` ## Success Response [#success-response] On acceptance Capture returns `200 OK` with: ```json { "status": "ok", "accepted": 2, "rejected": 0, "errors": [] } ``` * `accepted` — events expanded and enqueued from this request. * `rejected` — events dropped for a per-event problem (see below). * `errors` — one `FieldError` per dropped event, empty when nothing was dropped. ### Partial Acceptance [#partial-acceptance] A single malformed event does **not** sink the whole batch. Per-event problems (a bad `event` name, a bad `timestamp`, a reserved-property collision, a non-numeric `$battery_level`) drop only that event; the rest are still stored, and `200` reports `rejected` and the per-event `errors`. Whole-request problems still reject everything with a `4xx`: bad or missing shared `context`, an empty batch, more than 500 events, or **every** event being individually invalid (nothing left to store). The rule your client can rely on: > **`2xx` ⇒ at least one event was stored. `4xx` ⇒ nothing was stored.** Events listed in a `200`'s `errors` are permanently rejected — do not retry them. ## Errors [#errors] Validation and auth failures return a JSON body describing exactly what was wrong and where: ```json { "errors": [ { "code": "missing_context_key", "message": "context.$device_id is required", "field": "context.$device_id" } ] } ``` `field` is present when the error points at a specific location (for example `context.$device_id` or `events[0].timestamp`) and omitted otherwise. The `errors` array may contain more than one entry: validation collects every problem so you can fix them in one pass. | Status | `code` | Meaning | | ----------- | ------------------------ | ----------------------------------------------------------------------------------- | | `400` | `invalid_json` | The body is not valid JSON. | | `401` | `missing_project_key` | The `X-Honch-Project-Key` header is missing or empty. | | `401` | `unauthorized` | The key is invalid, inactive, or lacks the `capture`/`all` scope. | | `415` | `unsupported_media_type` | The `Content-Type` is neither `application/json` nor `application/vnd.honch.chunk`. | | `422` | `missing_context_key` | A required context key is absent. | | `422` | `unknown_context_key` | A context key outside the accepted set was sent. | | `422` | `invalid_context_value` | A context value is the wrong type or an empty string. | | `422` | `empty_batch` | `events` is empty. | | `422` | `too_many_events` | More than 500 events in one request. | | `200`/`422` | `invalid_event` | An event name is missing or empty. | | `200`/`422` | `invalid_timestamp` | A timestamp is not epoch milliseconds or RFC3339. | | `200`/`422` | `reserved_property` | A per-event property reused a promoted context key. | | `200`/`422` | `invalid_property_value` | A typed property had the wrong type (e.g. `$battery_level` not a number). | | `429` | `rate_limited` | The project exceeded its rate limit. | | `5xx` | — | Server-side failure. | The first group are **whole-request** failures (`422`, nothing stored). The `200`/`422` group are **per-event** failures: they appear in the `errors` array of a `200` when other events succeeded, or cause a `422` only when they leave no events to store. ### Retry policy [#retry-policy] Treat `429`, `5xx`, and network/timeout failures as retryable: retain the batch and retry with backoff. Treat `4xx` as permanent — fix the request before resending, because retrying the same payload fails the same way. A `200` with a non-empty `errors` array means the listed events were permanently rejected (don't retry them) while the rest were stored. The [Build Your Own Integration](/byo-integration) guide describes a concrete backoff schedule. ## Validate Before You Send [#validate-before-you-send] `POST https://i.honch.io/capture/validate` is a dry run. It uses the same content types and the same `X-Honch-Project-Key` auth, and it authenticates, decodes, validates, and expands your payload — but it does **not** store anything and does **not** count against your rate limit. It always returns `200 OK` with this shape: ```json { "ok": true, "content_type": "application/json", "accepted": 1, "rejected": 0, "expanded_events": [ { "event": "video_exported", "distinct_id": "device-abc", "timestamp": 1700000000000, "properties": { "$device_id": "device-abc", "$device_model": "pocket-cam-1", "$firmware_version": "1.4.2", "$sdk_platform": "pocket-ios", "$sdk_version": "0.1.0", "$environment": "production", "duration_ms": 5000 }, "uuid": "...", "received_at": "...", "ip": null, "geo_country": null, "geo_city": null } ], "errors": [] } ``` | Field | Meaning | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ok` | `true` only when the payload would be stored exactly as sent — nothing rejected and no errors. | | `content_type` | The content type Capture decoded. | | `accepted` | Number of events that would be stored (the length of `expanded_events`). | | `rejected` | Number of events that would be dropped for a per-event problem. | | `expanded_events` | The canonical events Capture would store, including promoted context. `uuid`, `received_at`, `ip`, `geo_country`, and `geo_city` are stamped at ingest and are placeholders here. | | `errors` | The `FieldError` array — fatal errors, or the per-event errors for the rejected events. | Use `/capture/validate` to confirm your payloads decode the way you expect and to debug error responses without writing test data into your project. For a partially-valid batch it shows both the events that would be accepted and the errors for those that would be dropped, so you can iterate until `ok` is `true`, then switch the URL to `/capture`. ```bash curl -sS https://i.honch.io/capture/validate \ -H "Content-Type: application/json" \ -H "X-Honch-Project-Key: honch_your_project_key" \ -d '{ "context": { "distinct_id": "device-abc" }, "events": [ { "event": "boot" } ] }' ``` The endpoint also accepts a single complete binary frame (`Content-Type: application/vnd.honch.chunk`) and returns the decoded and expanded result, which is handy for debugging the opaque binary path. You can lock your client against the shared conformance fixtures, which define this contract case by case: [JSON ingestion conformance fixtures](https://github.com/honch-io/SDK/tree/main/spec/conformance/json). ## Limits [#limits] | Limit | Value | | -------------------- | ----- | | Events per request | 500 | | Properties per event | 64 | | Nested value depth | 8 | Events-per-request (`422 too_many_events`) and nesting depth are enforced. The 64-properties-per-event figure is the canonical event model's shape, shared with the binary wire format — stay within it so your events expand identically on both paths, even though the JSON endpoint does not separately reject a request for exceeding it. ## Next Steps [#next-steps] # Honch SDKs (/) Honch is product analytics for connected hardware. Its SDKs run inside your firmware or app, capture events with typed properties, queue them locally, and upload them to Honch over HTTPS when the device is connected. One small C core defines the behavior; each platform port adapts storage, transport, timing, and packaging around it. Every SDK reports the same SDK version on the wire — `0.3.0` — because the four device ports share a single source of truth in the core. Maturity differs by platform, which is what the status column below reflects. ## Start Here [#start-here] If this is your first integration, read in this order: 1. [Quickstart](/quickstart) — pick an SDK, send one event, and confirm it reaches Honch. 2. Your SDK guide — install, configure, track, and verify on your target. 3. [Shared Concepts](/concepts) — identity, queueing, retries, lifecycle events, and the upload contract. 4. [Troubleshooting](/troubleshooting) and the [FAQ](/faq) — when something does not behave as expected. Resist adding every product event on day one. First prove that a single event queues and either uploads or stays pending for a reason you understand. ## Choose Your SDK [#choose-your-sdk] | SDK | Status | Use it for | | ---------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ | | [ESP-IDF](/sdks/esp-idf) | Stable · `0.3.0` | ESP32-family products built with Espressif's ESP-IDF framework. | | [C / POSIX](/sdks/c-posix) | Stable · `0.3.0` | Embedded Linux devices, gateways, and local development with file-backed queues you can inspect. | | [MicroPython](/sdks/micropython) | Stable · `0.3.0` | Firmware built with the `_honch_core` MicroPython user C module. | | [Arduino (ESP32)](/sdks/arduino) | Preview · `0.3.0` | ESP32 Arduino sketches. Evaluate and pilot before shipping. | | [React Native Relay](/sdks/react-native-relay) | Preview · `0.1.0` | Companion apps that forward BLE frames from offline devices to Honch. | | [Swift Relay](/sdks/swift-relay) | Coming soon | iOS companion apps. Same relay model as React Native. | No SDK for your platform? Send events straight to the HTTP API — see [Build Your Own Integration](/byo-integration). ## What Every SDK Does [#what-every-sdk-does] Each event carries an event name, an on-device timestamp, a `distinct_id`, and typed properties. The SDK attaches its own context properties to every event automatically: * `$device_id`, `$device_model`, `$firmware_version` * `$sdk_platform`, `$sdk_version`, `$environment` * `$session_id` while a session is active; `$battery_level` and `$wifi_rssi` when your integration supplies them These keys are reserved. If you pass a property that reuses a reserved key, the call is **rejected** with an invalid-argument error — reserved keys are never silently overwritten. The SDK is not silent on the wire. Initializing it queues a `$device_boot` event on its own, and these lifecycle events are emitted automatically as the relevant calls happen: * `$device_boot` — at init * `$device_shutdown` — at shutdown * `$firmware_update` — when the stored firmware version changes * `$battery_low` — when battery crosses your low threshold (edge-triggered) * `$session_start` / `$session_end` — around sessions * `$identify` — when you set a `distinct_id` ## The Upload Contract [#the-upload-contract] Device SDKs upload the compact binary chunk format: ```text POST /capture Content-Type: application/vnd.honch.chunk X-Honch-Project-Key: X-Honch-Stream-Id: ``` Uploads are cooperative: nothing happens on a background thread. Your code pumps delivery by calling `tick()` periodically (and `flush()` when you want to force a send). Initialization does its work synchronously and performs no network I/O. The default endpoint is `https://i.honch.io`; use HTTPS in production and reserve plain HTTP for intentional local testing. If you are integrating without an SDK, send JSON to the same endpoint instead — the [HTTP JSON API](/http-api) is the simplest path. ## Where To Go Next [#where-to-go-next] # Production Checklist (/production-checklist) Run through this before you ship. It assumes you already have one verified event from the [Quickstart](/quickstart). ## Identity [#identity] * [ ] Decide whether you set a fixed `device_id` or let the SDK generate and persist one. If you rely on a generated ID, confirm it persists across restarts (you have wired durable state storage on the device ports). * [ ] Call `identify()` at the right moment if your product has accounts. Confirm earlier anonymous events merge to the person in Honch. * [ ] Confirm `reset()` is called at your factory-reset / logout boundary (it clears identity, session, and queue, and emits no event). ## Transport And Security [#transport-and-security] * [ ] Endpoint is `https://i.honch.io` (or your assigned host) over HTTPS. No production path disables TLS verification. * [ ] The project API key is supplied via configuration, kept out of source control and logs, and scoped to capture. * [ ] A `401` is understood as permanent (bad key), not a transient error. ## Queue And Durability [#queue-and-durability] * [ ] Event buffer / queue is sized for your worst-case offline window. Remember the queue is drop-oldest past `max_queued_events` (default 1000). * [ ] You have chosen volatile RAM vs durable storage deliberately. If losing buffered events on power loss is unacceptable, wire durable queue/state adapters and consider `SYNC_ALWAYS`. * [ ] Largest event stays under `max_event_bytes` (default 8192) — oversized events are rejected. ## Flushing [#flushing] * [ ] You pump `tick()` from a dedicated task/thread, never from an ISR or latency-sensitive path. On the device ports, that task has at least 8192 bytes of stack for the TLS handshake. * [ ] Flush cadence (`flush_interval_seconds`, `flush_event_threshold`, `flush_min_interval_ms`) suits your event volume and power budget. * [ ] You supply a `connectivity_callback` (or equivalent) so the SDK skips DNS/TLS while the radio is down. ## Time [#time] * [ ] The device clock is synced (NTP/SNTP) before you depend on absolute timestamps. The SDK records boot-relative time before the clock is set and normalizes it at flush, but a synced clock is cleaner. ## Lifecycle And Errors [#lifecycle-and-errors] * [ ] You expect the automatic traffic: init alone queues `$device_boot`, and `$firmware_update` fires when the stored firmware version changes. * [ ] If you want crash/error capture, error tracking is enabled at runtime (`enable_error_tracking`). For ESP-IDF coredump backtraces, also set `enable_crash_symbolication` and ESP-IDF's coredump-to-flash options. Full coredumps and symbolicated backtraces are **ESP-IDF only** (Xtensa targets); MicroPython captures the Python traceback, and the other ports report a `$crash` event without a coredump. See [Crash And Error Reporting](/concepts#crash-and-error-reporting). ## Verify On Real Hardware [#verify-on-real-hardware] * [ ] You have watched a real device boot, queue, and upload — `204` (accepted) or `202` (chunk stored) — and seen events land in Honch. * [ ] You have tested the offline → reconnect path: events stay pending while offline and flush on reconnect with backoff. * [ ] You have power-cycled mid-queue and confirmed the durability behavior you expect. When all of the above hold, you are ready to ship. Keep an eye on [Troubleshooting](/troubleshooting) for the failure modes you will inevitably field. # Quickstart (/quickstart) Pick your SDK below and follow the steps. The goal is one verified event — install, configure, send, and confirm it reaches Honch. Deeper configuration lives on each SDK's guide. ## Set Your Capture Values [#set-your-capture-values] Every integration needs the same handful of values: | Value | Example | Notes | | ---------------- | ------------------ | ---------------------------------------------------------------- | | Project API key | `your-project-key` | Sent as the `X-Honch-Project-Key` header. | | Device model | `demo-board` | Static per product. | | Firmware version | `1.0.0` | Drives `$firmware_update` when it changes. | | Environment | `production` | Optional; defaults to `production`. Use `development` for tests. | The capture endpoint defaults to `https://i.honch.io` . You only set `host` / `endpoint_url` to point elsewhere — for example a local capture service during development. The examples below pass it explicitly for clarity. ## Build Your Integration [#build-your-integration] ### Install the component [#install-the-component] ```bash idf.py add-dependency "honch/honch^0.3.0" ``` ### Configure and initialize [#configure-and-initialize] Bring up NVS, the network, Wi-Fi, and time first (your firmware's job), then initialize Honch. `honch_init()` is synchronous and does no network I/O. ```c #include "honch.h" static uint8_t buf[16384]; honch_config_t config = { .api_key = CONFIG_HONCH_API_KEY, .host = "https://i.honch.io", .device_model = "demo-board", .firmware_version = "1.0.0", .event_buffer = buf, .event_buffer_size = sizeof(buf), }; honch_init(&config); ``` ### Send your first event [#send-your-first-event] ```c honch_track("app_started", NULL, 0); ``` ### Keep events flowing [#keep-events-flowing] There is no background thread. Pump delivery from a low-priority task with at least 8192 bytes of stack: ```c static void honch_task(void *arg) { for (;;) { honch_tick(); vTaskDelay(pdMS_TO_TICKS(1000)); } } // xTaskCreate(honch_task, "honch", 8192, NULL, 2, NULL); ``` ### Verify [#verify] ```bash idf.py build flash monitor ``` Watch the log for `$device_boot` at init, then a `POST /capture` once Wi-Fi and time are up. Full guide: [ESP-IDF](/sdks/esp-idf). ### Build the SDK [#build-the-sdk] ```bash cmake -S . -B build -DHONCH_BUILD_EXAMPLES=ON cmake --build build ``` ### Configure a client [#configure-a-client] Every call takes an explicit client handle, and the queue is file-backed under `queue_directory`. ```c #include "honch/honch.h" honch_config_t config = { .api_key = "your-project-key", .endpoint_url = "https://i.honch.io", .device_model = "linux-gateway", .firmware_version = "1.0.0", .queue_directory = "/var/lib/honch", }; honch_client_t *client = NULL; honch_init(&client, &config); ``` ### Send your first event [#send-your-first-event-1] ```c honch_track(client, "app_started", NULL, 0); honch_flush(client); ``` ### Keep events flowing [#keep-events-flowing-1] Pump `honch_tick(client)` from a dedicated thread; it does a synchronous POST and blocks up to `transport_timeout_ms`. ```c while (running) { honch_tick(client); sleep(1); } ``` ### Verify [#verify-1] Run your binary, then inspect the on-disk queue — events move out of `/pending/` as they upload. Full guide: [C / POSIX](/sdks/c-posix). ### Build firmware with `_honch_core` [#build-firmware-with-_honch_core] The wrapper needs the user C module compiled into your firmware: ```bash make -C ports/unix \ USER_C_MODULES=/path/to/SDK/ports/micropython/usermod/honch/micropython.cmake ``` ### Configure the client [#configure-the-client] ```python import honch client = honch.Honch( api_key="your-project-key", endpoint_url="https://i.honch.io", device_id="dev-board-001", device_model="dev-board", firmware_version="1.0.0", event_buffer=bytearray(8192), ) ``` ### Send your first event [#send-your-first-event-2] ```python client.track("app_started") client.flush() ``` ### Keep events flowing [#keep-events-flowing-2] ```python while True: client.tick() time.sleep(1) ``` ### Verify [#verify-2] Watch your capture service receive a `POST /capture`. Full guide: [MicroPython](/sdks/micropython). ### Add the library [#add-the-library] In `platformio.ini`: ```ini lib_deps = honch/Honch@^0.3.0 ``` ### Configure Honch [#configure-honch] Connect Wi-Fi first, then construct the config empty and assign fields (do not use designated initializers). ```cpp #include static uint8_t buf[8192]; HonchConfig config = {}; config.apiKey = "your-project-key"; config.host = "https://i.honch.io"; config.deviceModel = "demo-board"; config.firmwareVersion = "1.0.0"; config.rootCaPem = ROOT_CA_PEM; config.eventBuffer = buf; config.eventBufferSize = sizeof(buf); honch::defaultClient().begin(config); ``` ### Send your first event [#send-your-first-event-3] ```cpp honch::defaultClient().track("app_started"); ``` ### Keep events flowing [#keep-events-flowing-3] Pump `tick()` from your loop or a dedicated task: ```cpp void loop() { honch::defaultClient().tick(); delay(1000); } ``` ### Verify [#verify-3] Flash and open the serial monitor; confirm a `POST /capture` after Wi-Fi connects. Full guide: [Arduino](/sdks/arduino). ### Send a JSON batch [#send-a-json-batch] POST plain JSON to the same endpoint with your project key: ```bash curl -X POST https://i.honch.io/capture \ -H "Content-Type: application/json" \ -H "X-Honch-Project-Key: your-project-key" \ -d '{ "context": { "distinct_id": "device-1", "$device_id": "device-1", "$device_model": "demo-board", "$firmware_version": "1.0.0", "$sdk_platform": "custom", "$sdk_version": "1.0.0" }, "events": [{ "event": "app_started" }] }' ``` ### Read the response [#read-the-response] A `200` with `{"status":"ok","accepted":1,"rejected":0}` means it was stored. A `4xx` means nothing was stored — fix the request. ### Validate before launch [#validate-before-launch] Send the same body to `POST /capture/validate` to see the expanded events without storing anything. Full guide: [Build Your Own Integration](/byo-integration) and the [HTTP JSON API](/http-api). ## Confirm The First Event [#confirm-the-first-event] | Check | What you should see | | --------------------- | ----------------------------------------------------------------------------------- | | Init succeeds | The SDK returns success and queues `$device_boot`. | | Flush attempts upload | A `POST /capture` goes out once connected. | | Capture accepts | `204` (batch accepted) or `202` (chunk stored). JSON returns `200` with `accepted`. | | Event appears | `app_started` shows up in your project's live events. | If events queue but never upload, work through [Troubleshooting](/troubleshooting). ## Add A Few Product Events [#add-a-few-product-events] Once one event is verified, instrument the moments that matter — keep event names stable and put the variable detail in properties: | Event | When | | -------------------------------- | ---------------------------------------------------- | | `app_started` | Firmware finished boot and is ready. | | `mode_changed` | The device switched modes (property `mode`). | | `sync_started` / `sync_finished` | A sync began/ended (property `duration_ms`). | | `button_pressed` | A physical input fired (property `pin` or `button`). | Then read [Shared Concepts](/concepts) to understand identity, sessions, and how queueing and retries behave. # Security (/security) This page covers what the SDKs do to keep data secure in transit and how to integrate without weakening those guarantees. ## Transport Is TLS By Default [#transport-is-tls-by-default] Every device SDK uploads over TLS **and verifies the server certificate** — there is no production path that disables verification. The per-port specifics: | Port | TLS behavior | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ESP-IDF | Verifies the server certificate. For the default `i.honch.io` endpoint it trusts the Google Trust Services roots directly (certificate pinning to Honch's backend); for custom endpoints it uses the ESP-IDF certificate bundle, so your endpoint's chain must terminate at a root the bundle includes. There is no option to turn verification off. | | C / POSIX | libcurl with peer and host verification enabled; only `http`/`https` schemes, no redirect following. | | Arduino | `WiFiClientSecure`. For the default `i.honch.io` endpoint it pins the Google Trust Services roots, so it verifies with no setup. Set `rootCaPem` to trust your own CA (required for custom endpoints, and it overrides the default pin). `insecureSkipTlsVerify` exists for local testing only and logs a warning. | | MicroPython | Verifies the server certificate. For the default `i.honch.io` endpoint it pins the Google Trust Services root R1 (the same anchor as ESP-IDF and Arduino). Because validity-period checks need a real clock, you must sync NTP after connecting (also required for correct event timestamps) — otherwise the handshake is rejected. For a custom/local endpoint not behind Google Trust Services, call `honch_transport.verify_tls(False)` before constructing the client. | Always configure the endpoint with `https://` in production. Plain HTTP is only appropriate when you intentionally point an SDK at a local capture service during development. ## The Project Key [#the-project-key] Your project API key authenticates uploads. It is sent as the `X-Honch-Project-Key` header — a transport credential, never part of the event body: ```text X-Honch-Project-Key: ``` A missing or invalid key returns `401`, which the SDK treats as a **permanent** failure: it stops retrying that batch rather than hammering the endpoint with a bad credential. Keep the key out of logs and source control, scope it to capture, and rotate it if it leaks. ## Data Handling [#data-handling] * **You choose what is collected.** Beyond the automatic context (`$device_id`, `$device_model`, firmware/SDK/platform/environment, and optional session/battery/Wi-Fi signal), the SDK sends only the events and properties you track. It does not auto-collect heap, uptime, hardware revision, or location. * **Reserved keys are rejected, not overwritten.** An event that reuses an SDK-owned key fails with an invalid-argument error, so the SDK's trusted context cannot be spoofed by user properties. * **Timestamps are on-device event time.** Events carry when they happened on the device, normalized to real time at flush — not upload or relay time. * **Identity is yours to assign.** `$device_id` is hardware identity; `distinct_id` is set by `identify()`. The anonymous-to-identified merge is driven by `$anon_distinct_id` and happens in Honch, not on the device. ## Durability And Power Loss [#durability-and-power-loss] The default RAM queue is volatile: a reset or power loss clears unsent events. Where a port supports persistence, the `SYNC_ALWAYS` durability mode fsyncs each write so queued events survive an abrupt power cut, at a throughput cost. Choose it when losing buffered events is unacceptable; otherwise the default `OS_BUFFERED` mode is faster. See each SDK guide for how to enable durable storage. ## Relays [#relays] Relay uploads add `X-Honch-Relay-*` headers identifying the relay but use the same TLS and project-key model. A relay must preserve the device's compact message bytes exactly; it never inspects or rewrites event contents. Bluetooth is owned by the host app — the relay packages never request BLE permissions or touch the radio. # Shared Core (/shared-core) `core/` is the portable C library that defines Honch SDK behavior. Every platform port — ESP-IDF, C/POSIX, MicroPython, Arduino — wraps these `honch_core_*` functions and supplies the platform adapters they need. You only call the core API directly when building a new port or embedding the core yourself; most integrations use a port's friendlier wrapper. The behavior is documented in [Shared Concepts](/concepts); this page is the API reference. ## What The Core Owns [#what-the-core-owns] Event semantics and validation, typed properties, identity and `identify`/`reset`, sessions, lifecycle events, the bounded drop-oldest queue policy, retry/drop classification, the compact wire-format encoder, and chunk packetization. Ports provide storage, transport, clock, randomness, and packaging — they do not redefine product semantics. Identity persistence (`distinct_id`, device ID, firmware version) is a *port* responsibility: the core declares the state-storage contract and each port implements it (file-backed on C/POSIX, NV-backed hooks on the device ports). ## Core API [#core-api] ```c honch_status_t honch_core_init(honch_client_t **client, const honch_core_config_t *config); honch_status_t honch_core_track(honch_client_t *, const char *event_name, const honch_property_t *properties, size_t property_count); honch_status_t honch_core_identify(honch_client_t *, const char *distinct_id, const honch_property_t *traits, size_t trait_count); honch_status_t honch_core_set_property(honch_client_t *, const char *key, honch_value_t value); honch_status_t honch_core_session_start(honch_client_t *, const char *session_name); honch_status_t honch_core_session_end(honch_client_t *); /* Port-facing: a port reports a crash recovered from the previous boot ($crash), or an error-level log line its error hook captured ($error). */ honch_status_t honch_core_report_crash(honch_client_t *, const honch_crash_report_t *); honch_status_t honch_core_report_log_error(honch_client_t *, const char *component, const char *message); honch_status_t honch_core_tick(honch_client_t *); honch_status_t honch_core_flush(honch_client_t *); honch_status_t honch_core_pause_uploads(honch_client_t *); honch_status_t honch_core_resume_uploads(honch_client_t *); honch_status_t honch_core_reset(honch_client_t *); honch_status_t honch_core_shutdown(honch_client_t *); const char *honch_core_get_device_id(honch_client_t *); honch_status_t honch_core_copy_device_id(honch_client_t *, char *buffer, size_t buffer_size); honch_status_t honch_core_get_queue_stats(honch_client_t *, honch_queue_stats_t *stats); const char *honch_status_string(honch_status_t status); ``` `honch_core_init` is synchronous and performs no network I/O: it validates config, derives identity, reconciles the queue, and queues `$device_boot`. `honch_core_tick` sends at most one chunk; `honch_core_flush` sends up to `flush_max_batches` batches. Uploads can be gated with the pause/resume calls. ## Typed Values [#typed-values] Property values are typed. `honch_value_t` and `honch_property_t` come from the wire-format layer, with inline constructors: ```c honch_null() honch_bool(b) honch_u64(n) honch_i64(n) honch_f32(x) honch_f64(x) honch_str(s) honch_strn(s, len) honch_bytes(p, len) honch_array(...) honch_map(...) honch_prop(key, value) honch_pair(key, value) ``` Limits: up to 64 properties per event, event names up to 128 bytes, `distinct_id` up to 256 bytes. ## Status Codes [#status-codes] `honch_status_t` runs from `HONCH_STATUS_OK` through `HONCH_STATUS_ERROR_OFFLINE` and covers invalid argument, not-initialized / already-initialized, no-memory, queue-full, I/O, transport, timeout, rate-limited, server, rejected, and offline conditions. Short aliases (`HONCH_OK`, `HONCH_ERROR_INVALID_ARGUMENT`, and so on) are provided. Use `honch_status_string()` for human-readable text. Ports map these onto their own return types (for example ESP-IDF's `honch_err_t`). ## Error Context & Diagnostics [#error-context--diagnostics] The coarse `honch_status_t` answers *what kind* of error occurred, not *why*. For debugging, the core also records structured detail about the most recent failure in a `honch_error_detail_t`: | Field | Meaning | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | The coarse `honch_status_t`. | | `reason` | A finer `honch_error_reason_t` — e.g. `HONCH_REASON_AUTH_INVALID_KEY`, `HONCH_REASON_DNS_FAILED`, `HONCH_REASON_TLS_CERT`, `HONCH_REASON_HTTP_STATUS`, `HONCH_REASON_QUEUE_FULL`. | | `http_status` | The HTTP status code, or `0` if none was received. | | `os_error` | The platform error (errno / CURLcode / esp\_err\_t), or `0`. | | `message` | A short static description, or `NULL`. | | `component` | The failing phase: `"http"`, `"queue"`, `"encode"`. | Read it with `honch_core_get_last_error(client, &detail)` (copied out under the state lock; safe to keep). Format a one-line summary with `honch_error_detail_format(&detail, buf, sizeof(buf))`, e.g. `rejected: HTTP 401 - API key invalid or revoked (reason=auth_invalid_key)`. A non-zero `os_error` is appended as ` os_error=`. Reason tokens are available as stable strings via `honch_error_reason_string()`. On a failure the core also emits **one** deduped line through `platform->log` — `HONCH_LOG_WARN` for retryable failures, `HONCH_LOG_ERROR` for terminal ones — so the reason reaches the device's serial/log output without any extra code. The line is deduped per distinct failure (identical retries don't spam) and reset after a successful upload. This proactive logging is compiled in by default and can be stripped with `HONCH_ENABLE_ERROR_DIAGNOSTICS=0`; the struct, accessor, and formatter are always available. The fields and reason enum are additive — new reasons are appended, never renumbered. ## Configuration [#configuration] `honch_core_config_t` carries the tunables documented in [Shared Concepts](/concepts#queueing-and-durability) (`batch_size`, `max_queued_events`, `max_event_bytes`, the flush and retry timings, `battery_low_threshold`, `durability_mode`, `environment`, `endpoint_url`), plus the adapter tables and callbacks below. A field left zero takes the core default. ## Platform Interfaces [#platform-interfaces] A port supplies these op tables so the core stays platform-free: | Interface | Provides | | --------------------------- | ----------------------------------------------------------------------------------- | | `honch_platform_ops_t` | `now_ms`, `uptime_ms`, `random_bytes`, `log`, and mutex lock/unlock/create/destroy. | | `honch_state_storage_ops_t` | Durable key/value reads and writes for identity and firmware version. | | `honch_event_queue_ops_t` | The event queue (the RAM queue is the default; ports can substitute a durable one). | | `honch_transport_ops_t` | The HTTP chunk upload. | Optional callbacks: `auto_properties_fn` (supply extra properties; only `$wifi_rssi` among reserved keys is honored), `connectivity_fn` (report online/offline so the core can gate uploads), and `battery_callback` (return 0–100 to enable `$battery_level` / `$battery_low`). ## Building A Port [#building-a-port] If you are bringing Honch to a new platform, implement the four op tables, set the required config fields, and forward your platform's API to the `honch_core_*` functions. The repository's `ports/posix` is the clearest reference implementation, and the cross-SDK conformance fixtures in `spec/conformance/` pin the behavior your port must reproduce. # Troubleshooting (/troubleshooting) Find your symptom, confirm the cause, apply the fix. For the underlying behavior, see [Shared Concepts](/concepts). ## Events Queue But Never Upload [#events-queue-but-never-upload] The SDK has no background thread — uploads only happen when you pump them. * **Are you calling `tick()`?** Create a task/thread that calls `tick()` periodically, and `flush()` when you want to force a send. Without this, events sit in the queue forever. * **Is the device online?** If you supplied a `connectivity_callback` that reports offline, `tick()` does nothing and `flush()` returns an offline status by design. Confirm the callback reflects real connectivity. * **Is the clock set?** Uploads still work before time sync (timestamps are normalized at flush), so this is not a blocker — but verify the network path itself with a `flush()` and watch for a `POST /capture`. ## Upload Returns 401 [#upload-returns-401] The project key is missing or invalid. The SDK treats `401` as permanent and stops retrying that batch. * Confirm `X-Honch-Project-Key` carries the correct key and that it is scoped to capture. * Check you are pointed at the right endpoint/project. * Rotate the key if it may have leaked, and reconfigure. ## Which Failures Retry? [#which-failures-retry] | Result | HTTP | Behavior | | --------- | ------------------------------------- | --------------------------------------------------------------------------------- | | Retryable | 408, 409, 429, 5xx, transport/timeout | Events stay queued; backoff 1 s → 5 min with ±25% jitter, honoring `Retry-After`. | | Permanent | 401, other 4xx | Batch is dropped or dead-lettered so it cannot block the queue. | If events are disappearing, you are likely getting a permanent rejection — inspect the request body and headers against the [HTTP API](/http-api) or [Wire Format](/wire-format). ## `track()` Returns Invalid Argument [#track-returns-invalid-argument] Usually a reserved key. Event properties may not reuse an SDK-owned key (`$device_id`, `$session_id`, `$battery_level`, `distinct_id`, and the other `$` context keys) — those are rejected, not overwritten. Rename your property. The same status also covers an empty/oversized event name (max 128 bytes) or an event that exceeds `max_event_bytes`. ## The Tick Task Crashes Or Resets The Device (ESP-IDF / Arduino) [#the-tick-task-crashes-or-resets-the-device-esp-idf--arduino] The TLS handshake needs stack headroom. Create your delivery task with **at least 8192 bytes** of stack. A too-small stack corrupts during the handshake and surfaces as bogus TLS or memory errors. Never call `tick()`/`flush()` from an ISR or a high-priority/real-time path — they do synchronous network I/O and can block up to `transport_timeout_ms`. ## Tracking A GPIO Press [#tracking-a-gpio-press] There is no GPIO helper, and you must not call `track()` from an ISR. Use the host-owned pattern: the ISR pushes the pin number onto a FreeRTOS queue, and a normal task drains it and calls `honch_track("button_pressed", ...)`. See the [ESP-IDF GPIO section](/sdks/esp-idf#5-track-gpio-safely). ## Timestamps Look Wrong [#timestamps-look-wrong] Before the device clock is set, the SDK stamps boot-relative time and normalizes it to real time at flush. If timestamps are off, confirm NTP/SNTP runs and the system clock reads a real wall-clock time (at or after 2020-01-01) before steady-state operation. ## Events Lost After A Reset Or Power Cut [#events-lost-after-a-reset-or-power-cut] The default queue is RAM-only and clears on reset. Wire a durable queue/state adapter (file-backed on C/POSIX; NV-backed on the device ports), and use `SYNC_ALWAYS` if you must survive abrupt power loss. See [Security → Durability](/security#durability-and-power-loss). ## MicroPython: `ImportError` On `import honch` [#micropython-importerror-on-import-honch] The wrapper requires firmware built with the `_honch_core` user C module. Rebuild MicroPython with `USER_C_MODULES` pointing at the module's CMake file. See the [MicroPython guide](/sdks/micropython). ## Relay: Device Never Gets An ACK [#relay-device-never-gets-an-ack] The relay returns ACK bytes only after a frame is durably stored, and your app must write them to the device's ACK characteristic. Confirm your `acknowledge` callback actually performs the BLE write — the relay does not touch Bluetooth itself. ## Still Stuck? [#still-stuck] Validate your payload without storing anything by posting to `POST /capture/validate` — it returns the expanded events so you can see exactly how Honch interprets your request. Then check the [FAQ](/faq). # Wire Format (/wire-format) The compact wire format is a strict binary upload contract used by the official Honch SDKs and accepted by Capture. It produces smaller payloads than JSON and is designed for one chunk transport that works across direct HTTP, relay, BLE, and gateway flows. For most integrations you should use the [HTTP JSON API](/http-api) instead. JSON expands into the exact same canonical event as the binary format, so you give up nothing on the analytics side, and it is far easier to build and debug. Reach for the wire format only when the device transport is severely bandwidth- or power-constrained, where every byte and wakeup matters. This page is an orientation. The full byte grammar — header bits, varint encoding, the string table, value tags, and CRC — is defined in the canonical spec: [Compact Wire Format v2](https://github.com/honch-io/SDK/tree/main/spec/wire-format-v2.md). ## When To Use It [#when-to-use-it] | Use the wire format when | Use JSON instead when | | ------------------------------------------------------------------------- | ------------------------------------------------------------- | | The transport is bandwidth- or power-constrained. | You can make ordinary HTTPS requests. | | You are relaying opaque frames from firmware that cannot upload directly. | You want readable payloads and descriptive validation errors. | | You are implementing an official-SDK-equivalent client. | You are building a typical mobile or backend integration. | ## Endpoint [#endpoint] ```text POST https://i.honch.io/capture Content-Type: application/vnd.honch.chunk X-Honch-Project-Key: X-Honch-Stream-Id: # required for multi-chunk HTTP uploads ``` This is the same URL the JSON path uses (aliases `/e` and `/chunks` also accept it). Capture branches on the `Content-Type`. The project key is a transport credential and is not encoded inside the device message body. `Content-Encoding` is not allowed on this binary path. `X-Honch-Stream-Id` must be present and at most 128 bytes for any multi-frame upload; a missing or oversized stream id is rejected with `400`. ## Chunk Frame And Compact Message [#chunk-frame-and-compact-message] The format has two layers: * **Chunk frame** — the transport envelope. The request body is exactly one frame. A frame carries a header byte (protocol version, source type, continuation/more bits), a message id, optional offset or total length, the payload bytes, and a trailing CRC16 on final frames. * **Compact message** — the payload assembled from one or more frames. It encodes a string table, the device context (the same promoted keys as the JSON path: `distinct_id`, `$device_id`, and so on), and the event batch with delta-encoded timestamps and tagged property values. A direct upload that fits in one request sends a single final frame. Constrained links split the same compact message into an init frame plus continuation frames, which Capture reassembles by stream id and message id before decoding. Once decoded, a compact message expands into the identical canonical events the JSON path produces. The header's **source type** distinguishes what the frame carries: source type `0` is an event batch (the compact message above); source type `1` is a raw **coredump** blob (the ESP-IDF crash pipeline) streamed in the same CRC-checked, resumable chunks. Coredump frames are reassembled by stream id (the crash's `crash_id`) and stored as an opaque blob for backend symbolication rather than decoded into events. ## Response Codes [#response-codes] | Response | Meaning | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | `202` | A non-final chunk was stored. Send the next frame. | | `204` | The complete message was accepted. | | `400` | Malformed frame or compact message (bad header, varint, length, or CRC16). | | `409` | A frame conflicts with stored bytes for the same message (offset mismatch or message-id collision). | | `413` | The frame or message exceeds Capture's limits. | | `415` | Wrong `Content-Type`, or `Content-Encoding` was set. | | `422` | Semantic validation failed (bad UTF-8, duplicate or reserved keys, missing required context, or an out-of-range string reference). | | `429`, `5xx` | Retryable. Retain the frame and retry with backoff. | Retry on `409`, `408`, `429`, `5xx`, and network errors; treat `400`, `413`, `415`, and `422` as permanent. This matches the SDK upload policy described in [Shared Concepts](/concepts). ## Debugging Binary Payloads [#debugging-binary-payloads] The binary path returns bare status codes, which can be opaque. To inspect what a frame decodes to, send a single complete frame to `POST https://i.honch.io/capture/validate` with `Content-Type: application/vnd.honch.chunk`. It returns the decoded and expanded events (or a description of the decode failure) without storing anything. See [Validate Before You Send](/http-api#validate-before-you-send). ## Next Steps [#next-steps] # Setup Wizard (/wizard) The setup wizard is the fastest way to add Honch to a project. It scans your codebase, connects your Honch account, lets you choose which optional SDK features to compile in, and then runs an agent that does the integration — finishing with a setup report you can review. ```bash # npm npx @honch/start # bun bunx @honch/start ``` It installs into the current directory by default; point it elsewhere with `--install-dir /path/to/project`. Prefer to watch without changing anything? `--dry-run` walks the whole flow and writes the report without running the agent or touching your files. ## How it works [#how-it-works] ### Scan [#scan] The wizard inspects the project and detects the most likely SDK target — ESP-IDF, C/POSIX, MicroPython, Arduino, or a React Native relay — and pre-selects it. ### Connect [#connect] Sign in to your Honch account (browser login) and pick or create a project. The wizard mints a short-lived token for the install and never writes your raw project API key into source. ### Pick your features [#pick-your-features] Choose which optional SDK features to compile in. Everything is on by default, so confirming unchanged installs the full SDK. See [below](#pick-your-features-1) for what each feature costs. ### Confirm [#confirm] Review the plan. On a git repo the wizard offers to do the work on a fresh branch, so you can review or discard the whole integration cleanly. ### Install & report [#install--report] An agent applies the integration using your project's own conventions, runs the available build/test checks, and writes `honch-setup-report.md` describing exactly what changed. ## Pick your features [#pick-your-features-1] The Honch SDK is built from a small always-on core plus optional features gated at compile time. Turning a feature off in the wizard sets its compile-time toggle, so the code is genuinely excluded from the build — not just disabled at runtime. The picker shows the estimated cost of each feature so you can weigh the tradeoff on a constrained device. | Feature | What it adds | Toggle | | ------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------- | | **Core** *(required)* | Event tracking, typed properties, identity, the compact wire format, and the local queue. | — | | **Error tracking** | `$crash` events + ESP32 coredump upload, and `$error` capture coalesced from your logs. | `HONCH_ENABLE_ERROR_TRACKING` | | **Lifecycle events** | `$device_boot`, `$firmware_update`, `$device_shutdown`. | `HONCH_ENABLE_LIFECYCLE_EVENTS` | | **Sessions** | `$session_start` / `$session_end`. | `HONCH_ENABLE_SESSIONS` | | **Battery telemetry** | `$battery_low`, plus a `$battery_level` property on events. | `HONCH_ENABLE_BATTERY` | On ESP-IDF, crash reporting and log capture share one Kconfig switch (`CONFIG_HONCH_ERROR_TRACKING`), so the wizard presents them as a single **Error tracking** feature. On C/POSIX and MicroPython the core also exposes the finer `HONCH_ENABLE_CRASH_CAPTURE` / `HONCH_ENABLE_LOG_CAPTURE` macros if you want to strip them independently by hand. ## How the footprint numbers are measured [#how-the-footprint-numbers-are-measured] The numbers in the picker are measured, not estimated — and regenerated from the SDK so they stay honest. They reflect a release build on **ESP32 with ESP-IDF v6.0.1, optimized for size (`-Os`)**, and are shown for every C-core target as a representative figure. | Feature | Flash | Static RAM | Per-event wire | | ----------------- | -----: | ---------: | ------------------------- | | Error tracking | 4.9 KB | 316 B | \~166 B (`$crash`) | | Lifecycle events | 793 B | — | \~29 B (`$device_boot`) | | Sessions | 833 B | — | \~32 B (`$session_start`) | | Battery telemetry | 452 B | — | \~26 B (`$battery_low`) | Turning every option off leaves the \~37 KB always-on core; the full SDK is \~44 KB of flash. So feature stripping saves up to \~7 KB — real, but small next to the core. Each number is derived as follows: * **Flash** and **static RAM** are the *marginal* cost of each feature: the SDK is built with all features on, then once more with that single feature off, and the difference in the linked `libhonch.a` archive is attributed to the feature. Flash counts the `.text`/`.rodata` sections; static RAM counts `.bss`/`.data`. * **Per-event wire** is the full encoded size of the feature's headline event in the [compact wire format](/wire-format) — event name, timestamp, and all its properties. It's an upper bound: when many events upload together, shared strings are deduplicated and the real per-event cost drops. **Static RAM is not total RAM.** It counts only the SDK's static `.bss`/`.data`. The dominant runtime cost is your event queue, which is sized by *your* config (`max_queued_events`, `max_event_bytes`), not by these feature toggles. **Per-event wire is per event, not per second.** Total network egress depends on how often each event fires — boots, sessions, and battery alerts are infrequent; logs and crashes depend on your device's behavior. The measurement tools live in the SDK repo (`tools/measure_feature_footprint.py` for flash/RAM, `tools/measure_feature_wire.c` for wire bytes); rerun them to refresh the figures after a core change. # Arduino (ESP32) (/sdks/arduino) The Arduino port is a C++ wrapper around the shared core for ESP32 boards using the Arduino framework. It ships to the PlatformIO registry as `honch/Honch` and vendors a byte-identical copy of the core so the library is self-contained. Preview · `0.3.0` . ESP32-only. Use it for evaluation and controlled pilots until your product has passed hardware, TLS, offline-queue, flush, retry, and power-cycle validation. ## Before You Start [#before-you-start] * Targets ESP32 with the Arduino-ESP32 core (`WiFi`, `HTTPClient`, and friends). It is not a relay and does not handle OTA. * The default queue is RAM-only — events and identity are lost on reset or power loss unless you supply durable adapters. * There is no background task. Call `tick()` (or its alias `loop()`) from a task or your sketch loop. ## 1. Add The Library [#1-add-the-library] In `platformio.ini`: ```ini lib_deps = honch/Honch@^0.3.0 ``` ## 2. Configure And Send A First Event [#2-configure-and-send-a-first-event] `HonchConfig` uses default member initializers, so construct it empty and assign fields — do not use designated-initializer syntax (it will not compile in C++). ```cpp #include #include #include static uint8_t eventBuffer[8192]; void setup() { WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) delay(250); // ESP32 has no battery-backed RTC: sync the clock before tracking, or every // event is stamped near 1970 and falls outside the dashboard's time window. configTime(0, 0, "pool.ntp.org"); while (time(nullptr) < 1577836800UL) delay(250); HonchConfig config = {}; config.apiKey = "your-api-key"; config.host = "https://i.honch.io"; config.deviceModel = "demo-board"; config.firmwareVersion = "1.0.0"; config.rootCaPem = ROOT_CA_PEM; // required for HTTPS config.eventBuffer = eventBuffer; config.eventBufferSize = sizeof(eventBuffer); if (!honch::defaultClient().begin(config)) { Serial.printf("honch begin failed: %s\n", honch::defaultClient().lastError()); return; } honch::defaultClient().track("boot"); } void loop() { honch::defaultClient().tick(); delay(1000); } ``` The ESP32 has no battery-backed real-time clock, so its clock reads `1970` until you set it. Honch stamps each event with the **on-device time**, so tracking before the clock is set produces events that upload and ingest fine but are dated to \~1970 and fall **outside the dashboard's time window** — they look "missing." Call `configTime(...)` after Wi-Fi connects and wait for a real time before `begin()`/`track()`, as shown above. See [Timestamps](/concepts#timestamps). Methods return `bool` (`true` on success); read `lastError()` for the status string when one returns `false`. Calls are serialized on a per-instance mutex with a 10 ms timeout — under contention a call returns `false` with `lastError()` of `"busy"`. | Field | Default | Notes | | ------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `apiKey`, `host`, `deviceModel`, `firmwareVersion` | — | Required. | | `rootCaPem` | — | PEM root CA for HTTPS. | | `environment` | `"production"` | | | `eventBuffer` / `eventBufferSize` | — | RAM queue backing; `eventBufferSize` also sets `max_event_bytes`. | | `flushIntervalSeconds`, `flushMinIntervalMs`, `flushEventThreshold` | shared defaults | `flushEventThreshold` also sets the batch size. | | `transportTimeoutMs` | 8000 | Capped at 10000. | | `connectivityCallback` | — | Return `false` when offline. | | `enableErrorTracking` | `false` | Detect an abnormal ESP32 reset at `begin()` and emit a `$crash` with the reset reason. This port captures the reset reason only — no coredump or symbolicated backtrace (those are ESP-IDF only). | | `insecureSkipTlsVerify` | `false` | Local testing only; logs a warning. | | `stateStorageOps` / `eventQueueOps` | — | Durable identity / durable queue. | The wrapper hardcodes `$sdk_platform` to `arduino-esp32` and caps the RAM queue at 1000 entries. The device ID defaults to `esp32-`. ## 3. Track, Identify, And Sessions [#3-track-identify-and-sessions] ```cpp const honch_property_t props[] = { honch_prop("mode", honch_str("record")) }; honch::defaultClient().track("mode_changed", props, 1); honch::defaultClient().identify("user-123"); honch::defaultClient().sessionStart("recording"); honch::defaultClient().sessionEnd(); honch::defaultClient().flush(); ``` ## Transport And TLS [#transport-and-tls] Uploads use `HTTPClient` over `WiFiClientSecure` to `/capture`. Set `rootCaPem` for real HTTPS; `insecureSkipTlsVerify = true` disables verification for local testing only and logs a warning. Production must use a real root CA. ## Durability [#durability] By default the queue is RAM-only and identity is not persisted, so both are lost on reset. For persistence, wire `eventQueueOps` to a durable queue (a tiered RAM + non-volatile queue is available; the `HonchDurableQueue` example shows a LittleFS cold tier) and `stateStorageOps` to non-volatile storage such as `Preferences`. If you call the tiered queue's persist function yourself, hold the same lock as `track()`/`tick()`. ## Examples And Build Check [#examples-and-build-check] The library's registered examples are `HonchBasic` and `HonchOfflineQueue`. The repository also includes a `HonchDedicatedTask` sketch and `HonchDurableQueue` reference helpers — a LittleFS cold tier for the tiered queue, shipped as `.h`/`.cpp` rather than a standalone sketch. Compile-check with arduino-cli: ```bash arduino-cli compile --fqbn esp32:esp32:esp32 examples/HonchBasic ``` ## Debugging Failures [#debugging-failures] `lastError()` returns a short status word (`"transport error"`, `"rejected"`). For *why* a call failed, use the structured accessors: ```cpp if (!Honch.flush()) { honch_error_detail_t detail; Honch.lastErrorDetail(&detail); Serial.println(Honch.lastErrorMessage()); // "rejected: HTTP 401 - API key invalid or revoked (reason=auth_invalid_key)" if (detail.http_status == 401) { /* bad project key */ } } ``` `lastErrorDetail(&out)` fills `status`, `reason`, `http_status`, `os_error`, `message`, and `component`; `lastErrorMessage()` returns the formatted one-liner. The SDK also logs that same line once per distinct failure via `log_w`/`log_e`, so failures show up on the serial monitor automatically. See [Error Context & Diagnostics](/shared-core#error-context--diagnostics). ## Public API [#public-api] `honch::defaultClient()` returns the singleton. Methods: `begin(config)`, `track(name, props, count)`, `identify(id, traits, count)`, `setProperty(key, value)`, `sessionStart(name)`, `sessionEnd()`, `flush()`, `tick()` (alias `loop()`), `reset()`, `shutdown()`, `deviceId()`, `queueStats(&stats)`, `lastError()`, `lastErrorDetail(&detail)`, and `lastErrorMessage()`. # C / POSIX (/sdks/c-posix) The C/POSIX port runs on macOS and Linux. It targets embedded Linux devices and gateways, and doubles as the fastest way to exercise core behavior on your development machine — its queue is file-backed, so you can watch events move through `pending/` and `dead/` directories on disk. Stable · `0.3.0` . Reports `$sdk_platform` as `c-posix` . ## Before You Start [#before-you-start] * Requirements: CMake `3.20+`, a C11 compiler, libcurl, and a POSIX threads (pthreads) implementation. * Each active client needs its own writable `queue_directory`. Do not point two clients at the same directory. * Unlike the device ports, this port's API is multi-client: every call takes an explicit `honch_client_t *`. ## 1. Build The SDK [#1-build-the-sdk] ```bash cmake -S . -B build -DHONCH_BUILD_TESTS=ON -DHONCH_BUILD_EXAMPLES=ON cmake --build build ctest --test-dir build --output-on-failure ``` The build is warnings-as-errors. To compile out crash/error reporting (`$crash` + `$error`), configure with `-DHONCH_ENABLE_ERROR_TRACKING=OFF`. To install and link from another CMake project: ```bash cmake -S . -B build-install -DHONCH_BUILD_TESTS=OFF -DHONCH_BUILD_EXAMPLES=OFF -DCMAKE_INSTALL_PREFIX=/opt/honch-posix cmake --build build-install --target honch_posix cmake --install build-install ``` ```cmake find_package(honch_posix REQUIRED) target_link_libraries(app PRIVATE honch::honch_posix) ``` ## 2. Configure And Send A First Event [#2-configure-and-send-a-first-event] ```c #include "honch/honch.h" honch_config_t config = { .api_key = "your-api-key", .endpoint_url = "https://i.honch.io", .device_model = "linux-gateway", .firmware_version = "1.0.0", .queue_directory = "/var/lib/honch", }; honch_client_t *client = NULL; if (honch_init(&client, &config) != HONCH_STATUS_OK) { return 1; } const honch_property_t props[] = { honch_prop("role", honch_str("developer")) }; honch_identify(client, "local-user-001", props, 1); honch_session_start(client, "demo"); honch_track(client, "button_pressed", NULL, 0); honch_session_end(client); honch_flush(client); honch_shutdown(client); ``` Required fields are `api_key`, `endpoint_url`, `device_model`, `firmware_version`, and `queue_directory`. Everything else falls back to the shared [defaults](/concepts#queueing-and-durability). `honch_init()` is synchronous — it validates config, reconciles the queue directory, persists identity, and queues `$device_boot` — but does no network I/O. | Field | Default | Notes | | ------------------------ | --------------------- | ---------------------------------------------------------------------------- | | `device_id` | generated + persisted | A random ID is created on first run if you do not set one. | | `environment` | `"production"` | | | `durability_mode` | `OS_BUFFERED` | `SYNC_ALWAYS` fsyncs every write for power-loss safety at a throughput cost. | | `flush_interval_seconds` | 120 | | | `flush_event_threshold` | 20 | | | `transport_timeout_ms` | 8000 | | | `connectivity_callback` | — | Return offline so ticks skip DNS/TLS and keep events pending. | ## 3. Pump Delivery [#3-pump-delivery] Call `honch_tick(client)` from a dedicated thread. It performs a synchronous HTTP POST on the calling thread and blocks up to `transport_timeout_ms`, so keep it off latency-sensitive paths. ```c while (running) { honch_tick(client); sleep(1); } ``` `honch_flush(client)` forces queued batches out immediately — handy in short-lived processes before exit. ## 4. Inspect Local Storage [#4-inspect-local-storage] The queue is a directory tree you can read directly: ```text / pending/ events waiting to upload (one file each) dead/ permanently rejected events state/ device_id, distinct_id, firmware_version ``` Events are written atomically (temp file then rename) and evicted oldest-first when the queue is full. Retryable failures leave events in `pending/`; permanent rejections move them to `dead/`. This visibility is the main reason to validate an integration here before moving to a constrained device. ## 5. Crash Breadcrumbs (Optional) [#5-crash-breadcrumbs-optional] `honch_install_error_handlers(queue_directory)` installs async-signal-safe handlers for `SIGABRT`, `SIGSEGV`, `SIGBUS`, `SIGILL`, and `SIGFPE`. They write a bounded breadcrumb file to the directory; the next `honch_init()` imports it as a `$crash` event. The path is process-global, so use it with a single client. Because the breadcrumb is persisted to disk, a fatal crash is delivered on the next run even though the in-memory queue does not survive it. POSIX captures the signal/reset context, not a coredump — there is no symbolicated backtrace on this port. For handled, non-fatal errors call `honch_core_report_log_error(client, component, message)`. ## Transport Contract [#transport-contract] ```text POST /capture Content-Type: application/vnd.honch.chunk X-Honch-Project-Key: X-Honch-Stream-Id: ``` Transport is libcurl with TLS verification enabled (peer and host); only `http`/`https` are allowed and redirects are not followed. Use `https://` in production. ## Examples [#examples] The repository ships runnable examples under `ports/posix/example/`: `posix_device` (the snippet above), `connected_camera`, `posix_gpio` (edge-tracking from a host GPIO source), and `identify_merge`. ## Debugging Failures [#debugging-failures] When a call returns a non-`HONCH_OK` status, `honch_core_get_last_error()` gives the reason behind it: ```c honch_error_detail_t detail; if (honch_tick(client) != HONCH_OK && honch_core_get_last_error(client, &detail) == HONCH_OK) { char line[192]; honch_error_detail_format(&detail, line, sizeof(line)); fprintf(stderr, "honch: %s\n", line); // "transport error: HTTP 503 - server returned an error status (reason=http_status)" // or, for a connect-phase failure (raw CURLcode in os_error): // "transport error: DNS resolution failed - check the configured endpoint (reason=dns_failed) os_error=6" } ``` The libcurl transport fills `http_status` from the response and maps the `CURLcode` of a connect/DNS/TLS failure into `reason` (with the raw code in `os_error`). The core also emits the same one-line summary once per distinct failure through the platform `log` hook. See [Error Context & Diagnostics](/shared-core#error-context--diagnostics). ## Public API [#public-api] | Function | Purpose | | ----------------------------------------------------------------- | ------------------------------------------------------------------------- | | `honch_init(&client, &config)` | Create and initialize a client. | | `honch_core_get_last_error(client, &detail)` | Structured detail (reason, http\_status, os\_error) for the last failure. | | `honch_track(client, event, props, count)` | Queue an event. | | `honch_identify(client, distinct_id, traits, count)` | Set identity; emits `$identify`. | | `honch_set_property(client, key, value)` | Emit `$set_property`. | | `honch_session_start(client, name)` / `honch_session_end(client)` | Bracket a session. | | `honch_core_report_log_error(client, component, message)` | Report a handled, non-fatal error as an `$error`. | | `honch_install_error_handlers(queue_directory)` | Install crash-breadcrumb signal handlers (`$crash` on next run). | | `honch_tick(client)` | Cooperative delivery step. | | `honch_flush(client)` | Send queued batches now. | | `honch_reset(client)` | Clear identity, session, and queue. | | `honch_shutdown(client)` | Emit `$device_shutdown`, final flush, free the client. | | `honch_get_device_id(client)` | Borrowed device-ID pointer. | | `honch_copy_device_id(client, buf, size)` | Copy the device ID into your buffer (thread-safe). | | `honch_status_string(status)` | Human-readable status text. | Calls return `honch_status_t` (`HONCH_STATUS_OK` on success), except `honch_get_device_id` and `honch_status_string`, which return `const char *`. # ESP-IDF (/sdks/esp-idf) The ESP-IDF port is the production Honch SDK for ESP32-family chips. It is a thin wrapper over the shared core, registered as an ESP-IDF component and published to the Espressif Component Registry as `honch/honch`. Stable · `0.3.0` . Requires ESP-IDF `>= 5.0` (verified against v6.0.1). ## Before You Start [#before-you-start] * The SDK does no network setup of its own. Your firmware brings up NVS, the network interface, Wi-Fi, time (SNTP), and the TLS trust store. * There is **no background task**. You pump delivery by calling `honch_tick()` from a FreeRTOS task you create. Allow at least 8192 bytes of stack for that task — the TLS handshake needs the headroom. * The default queue is in RAM and clears on reset or power loss. See [Queue And Durability](#queue-and-durability) for persistence. ## 1. Install The Component [#1-install-the-component] Add the dependency with the component manager: ```bash idf.py add-dependency "honch/honch^0.3.0" ``` Or vendor the SDK as a git submodule (the component pulls in the shared `core/`, so submodule the whole repository): ```bash git submodule add https://github.com/honch-io/SDK.git components/honch ``` ## 2. Configure And Initialize [#2-configure-and-initialize] `honch_init()` validates its config, derives identity, reconciles the queue, and queues `$device_boot`. It is synchronous and performs no network I/O. ```c #include "honch.h" static uint8_t honch_event_buffer[16384]; void app_main(void) { // Bring up NVS, netif, Wi-Fi, and SNTP first (your firmware's job). honch_config_t config = { .api_key = CONFIG_HONCH_API_KEY, .host = "https://i.honch.io", .device_model = "demo-board", .firmware_version = "1.0.0", .event_buffer = honch_event_buffer, .event_buffer_size = sizeof(honch_event_buffer), }; honch_err_t err = honch_init(&config); if (err != HONCH_OK) { ESP_LOGE("app", "honch_init failed: %d", err); return; } } ``` Required fields are `api_key`, `host`, `device_model`, `firmware_version`, and an event buffer (`event_buffer` + `event_buffer_size`) unless you supply your own `event_queue_ops`. Recommended buffer size is 8192 bytes or more. | Field | Default | Notes | | -------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `environment` | `"production"` | Set to `development`/`staging` to separate test data. | | `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 | Clamped to 1000–10000 ms. A constrained ESP32 TLS handshake plus the POST needs headroom above the old 2.5 s. | | `flush_max_batches` | 1 | Batches sent per flush. | | `battery_callback` / `battery_low_threshold` | — / 15 | Return 0–100 to enable `$battery_level` and `$battery_low`. | | `connectivity_callback` | — | Return 0 when offline so the SDK skips DNS/TLS. Defaults to always-connected. | | `enable_error_tracking` | `false` | Master switch for crash + error reporting: emit a `$crash` after an abnormal reset and capture `ESP_LOGE` lines as `$error`. See [Crash And Error Tracking](#crash-and-error-tracking). | | `enable_crash_symbolication` | `false` | When a crash is reported, also capture the full coredump for backend symbolication (a function/file/line backtrace) and stamp the firmware build ID. Requires `enable_error_tracking`. | | `state_storage_ops` / `event_queue_ops` | — | Supply durable identity / a durable queue. | If you do not set a `device_id`, the SDK derives a stable one from the Wi-Fi station MAC, formatted `esp32-`. ## 3. Pump Delivery [#3-pump-delivery] Create a low-priority task that calls `honch_tick()` on an interval. `tick()` sends at most one chunk per call and blocks up to `transport_timeout_ms`, so keep it off any latency-sensitive path. ```c static void honch_task(void *arg) { for (;;) { honch_err_t err = honch_tick(); if (err != HONCH_OK && err != HONCH_ERR_TRANSPORT && err != HONCH_ERR_TIMEOUT && err != HONCH_ERR_OFFLINE) { ESP_LOGW("honch", "tick: %d", err); } vTaskDelay(pdMS_TO_TICKS(1000)); } } // xTaskCreate(honch_task, "honch", 8192, NULL, 2, NULL); ``` ## 4. Track Events, Identity, And Sessions [#4-track-events-identity-and-sessions] ```c const honch_property_t props[] = { honch_prop("mode", honch_str("record")), honch_prop("duration_ms", honch_i64(4200)), }; honch_track("mode_changed", props, 2); honch_identify("user-123", NULL, 0); honch_session_start("recording"); honch_track("frame_captured", NULL, 0); honch_session_end(); honch_flush(); // force a send now ``` Reusing a reserved key (any `$`-prefixed key, or `distinct_id`) in your properties returns `HONCH_ERR_INVALID_ARG`. ## 5. Track GPIO Safely [#5-track-gpio-safely] There is no GPIO helper in the SDK — and you should not call `honch_track()` from an ISR, since it can allocate and block. The supported pattern is host-owned: the ISR only hands a pin number to a FreeRTOS queue, and a normal task drains the queue and tracks the event. This is exactly what the `example_gpio` example does. ```c static QueueHandle_t s_gpio_queue; // xQueueCreate(16, sizeof(uint32_t)) static void IRAM_ATTR button_isr(void *arg) { uint32_t pin = (uint32_t)(uintptr_t)arg; xQueueSendFromISR(s_gpio_queue, &pin, NULL); } static void gpio_task(void *arg) { uint32_t pin; for (;;) { if (xQueueReceive(s_gpio_queue, &pin, portMAX_DELAY) == pdTRUE) { const honch_property_t props[] = { honch_prop("pin", honch_i64(pin)) }; honch_track("button_pressed", props, 1); } } } ``` Debounce and pin setup are your firmware's responsibility, exactly as with any other GPIO work. ## Configuration (Kconfig) [#configuration-kconfig] Two build-time options gate the error-tracking code, both enabled by default in `menuconfig` under **Honch SDK**: | Option | Default | Effect | | ---------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `CONFIG_HONCH_ERROR_TRACKING` | on | Compiles in the crash (`$crash`) + error-log (`$error`) capture path. | | `CONFIG_HONCH_CRASH_SYMBOLICATION` | on | Compiles in coredump capture: stamps the firmware build ID and streams the raw ELF coredump for backend symbolication. Depends on error tracking. | These control whether the code is *compiled in*. To actually report at runtime you must also set `enable_error_tracking = true` (and `enable_crash_symbolication = true` for coredumps) in your config. The runtime switch gates **both** the `$crash` snapshot and the `ESP_LOGE` → `$error` capture, so turning it off silences both. ## Crash And Error Tracking [#crash-and-error-tracking] With `enable_error_tracking` on, the SDK does the following automatically — there are no manual reporting calls: * **Crash detection.** During init it reads the reset reason and, for an abnormal reset (panic, watchdog, brownout, and similar), queues a `$crash` carrying the reset cause and a `crash_id`. Because it is re-derived from the hardware reset reason on the next boot, the `$crash` does not depend on the queue surviving the reset. * **Error-log capture.** It chains `ESP_LOGE` so error-level logs become bounded, de-duplicated `$error` events. The runtime switch gates this too — turning `enable_error_tracking` off stops both the `$crash` snapshot and log capture. * **Coredump capture** (with `enable_crash_symbolication`). When ESP-IDF's coredump-to-flash is configured (`CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH`, with a `coredump` partition), a panic writes a full coredump to flash. On the next boot the SDK streams it to Capture in CRC-checked, resumable chunks; the backend symbolicates it against your firmware's debug symbols into a **function/file/line backtrace**, joined to the `$crash` by `crash_id`. The on-flash copy is erased only after the upload is acknowledged. Symbolicated backtraces are produced for Xtensa targets (the original ESP32 and S-series). RISC-V targets (C3/C6/H2) capture the crash but backtrace resolution is more limited. ## Transport Contract [#transport-contract] Each upload is: ```text POST /capture Content-Type: application/vnd.honch.chunk X-Honch-Project-Key: X-Honch-Stream-Id: ``` TLS uses the ESP-IDF certificate bundle for server verification; there is no option to disable verification. A `Retry-After` response header is honored. Configure `host` with `https://` in production — the examples use an `http://` host only to talk to a local capture service. ## Queue And Durability [#queue-and-durability] By default events live in the RAM buffer you provide, capped at 1000 queued entries with drop-oldest eviction. Nothing is persisted, so a reset or power loss clears the queue and any identity you have not made durable. To survive restarts, supply your own adapters: * `state_storage_ops` — durable identity (`distinct_id`) and firmware version. A common backing is NVS. (Note NVS keys are capped at 15 characters, so firmware version is stored under `fw_version`.) * `event_queue_ops` — a durable event queue in place of the RAM queue. The SDK ships the hooks; you provide the storage backend. ## Verify On Hardware [#verify-on-hardware] ```bash idf.py set-target esp32s3 idf.py build flash monitor ``` Watch the serial log for `$device_boot` queuing at init, then a `POST /capture` from your tick task once Wi-Fi and time are up. A `204` means the batch was accepted; a `202` means a chunk was stored and more follow. See [Troubleshooting](/troubleshooting) if events queue but never upload. ## Debugging Failures [#debugging-failures] The `esp_http_client` transport records structured detail for every failed upload: the HTTP status maps to a `reason` (e.g. `401` → `auth_invalid_key`), and an `esp_err_t` connect/DNS/TLS failure maps into `reason` with the raw code in `os_error`. Read it with `honch_get_last_error(&detail)`. The core also emits one deduped line per distinct failure through the platform log hook under the `honch` tag — `ESP_LOGW` for retryable, `ESP_LOGE` for terminal — so `idf.py monitor` shows, for example: ```text E (45231) honch: rejected: HTTP 401 - API key invalid or revoked (reason=auth_invalid_key) ``` Because the line is tagged `honch`, it is skipped by the `ESP_LOGE` → `$error` capture hook (no feedback loop). See [Error Context & Diagnostics](/shared-core#error-context--diagnostics). ## Public API [#public-api] | Function | Purpose | | --------------------------------------------------- | ------------------------------------------------------------------------- | | `honch_init(&config)` | Initialize the singleton; synchronous, no network. | | `honch_get_last_error(&detail)` | Structured detail (reason, http\_status, os\_error) for the last failure. | | `honch_track(event, props, count)` | Queue an event. | | `honch_identify(distinct_id, traits, count)` | Set identity; emits `$identify`. | | `honch_set_property(key, value)` | Emit `$set_property`. | | `honch_session_start(name)` / `honch_session_end()` | Bracket a session. | | `honch_tick()` | Cooperative delivery step (one chunk). | | `honch_flush()` | Send queued batches now. | | `honch_reset()` | Clear identity, session, and queue. | | `honch_shutdown()` | Emit `$device_shutdown`, final flush, tear down. | | `honch_get_device_id()` | Borrowed pointer to the device ID. | | `honch_get_queue_stats(&stats)` | Read queue depth and counters. | All calls return `honch_err_t` (`HONCH_OK` on success). # MicroPython (/sdks/micropython) The MicroPython port is a thin Python wrapper over the same C core, bound through a user C module named `_honch_core`. It is not pure Python and not standalone: the module must be compiled into your MicroPython firmware. CircuitPython is out of scope. Stable · `0.3.0` . PyPI package `honch-micropython` . Requires firmware built with the `_honch_core` user C module. ## Before You Start [#before-you-start] * You build MicroPython firmware yourself with the Honch user C module included. The Python wrapper alone cannot work without it — importing `honch` without `_honch_core` raises `ImportError`. * The wrapper rejects host-side hooks. Passing `platform`, `transport`, `battery_callback`, or `auto_properties_callback` to the constructor raises `InvalidArgumentError` — those adapters live in the C module. ## 1. Build Firmware With `_honch_core` [#1-build-firmware-with-_honch_core] Point your MicroPython build at the module's CMake file with `USER_C_MODULES`: ```bash # Unix port (handy for host testing) make -C ports/unix \ USER_C_MODULES=/path/to/SDK/ports/micropython/usermod/honch/micropython.cmake ``` For a board, also freeze the Python wrapper into the image: ```bash make -C ports/esp32 BOARD=ESP32_GENERIC \ USER_C_MODULES=/path/to/SDK/ports/micropython/usermod/honch/micropython.cmake \ FROZEN_MANIFEST=/path/to/SDK/ports/micropython/manifest.py ``` The user C module does not set firmware-global options such as the GC heap size — keep that in your board or host configuration. ## 2. Install Or Freeze The Wrapper [#2-install-or-freeze-the-wrapper] The five `honch/*.py` wrapper modules ship on PyPI as `honch-micropython` and can be frozen via the manifest (recommended for boards) or installed with `mip`. If the wrapper is frozen into the firmware, do not also copy it into `/lib`. ## 3. Configure And Send A First Event [#3-configure-and-send-a-first-event] ```python import honch client = honch.Honch( api_key="your-api-key", endpoint_url="https://i.honch.io", device_id="dev-board-001", device_model="dev-board", firmware_version="1.0.0", event_buffer=bytearray(8192), ) client.identify("user-123", {"plan": "beta"}) client.session_start("demo") client.track("button_pressed", {"button": "boot"}) client.session_end() client.flush() client.shutdown() ``` Required keyword arguments: `api_key`, `endpoint_url`, `device_id`, `device_model`, `firmware_version`, and `event_buffer` (a `bytearray` sized to your `max_event_bytes`, default 8192). Optional arguments map to the shared [defaults](/concepts#queueing-and-durability): `environment`, `batch_size`, `max_queued_events`, `max_event_bytes`, `transport_timeout_ms`, `flush_interval_seconds`, `flush_min_interval_ms`, `flush_event_threshold`, `flush_retry_initial_ms`, `flush_retry_max_ms`, `battery_low_threshold`, and `connectivity_callback`. Property values may be `None`, `bool`, `int`, `float`, `str`, `bytes`, `list`/`tuple`, or `dict` (with string keys), nested. The Pico W (and most MicroPython boards) has no battery-backed real-time clock, so `time.time()` reads `2000-01-01` until you set it. Honch stamps every event with the **on-device time**, so if you `track()` before the clock is set, events upload and ingest fine but are stamped near **1970** and fall **outside the dashboard's time window** — they look "missing" when they are really just dated to the epoch. Sync NTP once after Wi-Fi is up and **before** you construct the client / track: ```python import ntptime # ... after Wi-Fi is connected ... ntptime.settime() # set the RTC from NTP (UTC) ``` See [Timestamps](/concepts#timestamps) for how the core treats an unset clock. ## 4. Pump Delivery, Identity, And Connectivity [#4-pump-delivery-identity-and-connectivity] There is no background thread. Call `client.tick()` on an interval from your main loop, and `client.flush()` to force a send. ```python while True: client.tick() time.sleep(1) ``` Connectivity is explicit. `client.connectivity_changed(connected)` (and the `connected()` / `disconnected()` shorthands) records the state and emits a `$connectivity_change` event with a `state` property of `"connected"` or `"disconnected"`. When the wrapper believes it is offline, `flush()` raises `OfflineError` and `tick()` is a no-op, so it never spends time on DNS/TLS. ## Transport And TLS [#transport-and-tls] Uploads POST the compact chunk to `/capture`. On rp2 the port drives the entire exchange — connect, TLS handshake, request, and status read — on a non-blocking socket bounded by a single deadline. This matters because `urequests`' `timeout=` does **not** bound `connect()` or the TLS handshake on rp2: on a flaky or transitional link a plain upload would park the single-threaded VM indefinitely. With the bounded transport a stalled link raises promptly and the core retries on the next `tick()`/`flush()`; `transport_timeout_ms` (default 8000, max 10000) is the ceiling for the whole exchange. The port **verifies the server certificate** against the Google Trust Services root R1 — the trust anchor for the default `i.honch.io` endpoint, the same root the ESP-IDF and Arduino ports pin. Verification checks the certificate's validity period, which needs a real clock, so the **NTP sync above is required for the handshake too** (not just for timestamps) — without it the connection is rejected. For a custom or local endpoint that isn't behind Google Trust Services, call `honch_transport.verify_tls(False)` before constructing the client. See [Security → Transport](/security#transport-is-tls-by-default). ## 5. Crash And Error Reporting [#5-crash-and-error-reporting] ```python # Report a non-fatal error you handled — e.g. wire this into your logging: client.report_log_error("sensor read failed", component="imu") ``` For **uncaught crashes**, pick one of the two options below. Both report the crash as a `$crash` carrying the exception type, message, and the **Python traceback** (file/line/function, crash-site first). On MicroPython the traceback *is* the symbolicated backtrace — there is no coredump. **Option A — wrap your entry point (works on every build).** Run your `main` through Honch; any uncaught exception is reported, flushed, and re-raised so behavior is unchanged: ```python def main(): while True: client.tick() read_sensors() client.run(main) # instead of calling main() directly ``` **Option B — install a global hook (needs `sys.excepthook`).** If your firmware is built with `MICROPY_PY_SYS_EXCEPTHOOK` enabled, you can hook the interpreter instead of wrapping `main`: ```python client.install_error_hook() # idempotent; returns False if sys.excepthook is unavailable # ... later, to restore the previous hook ... client.uninstall_error_hook() ``` * **Stock MicroPython firmware (including the Pico W) ships without `sys.excepthook`**, so `install_error_hook()` returns `False` and captures nothing. On those builds use `client.run(main)` — it relies only on a plain `try/except` and needs no firmware changes. Build a custom firmware with `MICROPY_PY_SYS_EXCEPTHOOK` only if you genuinely cannot wrap your entry point. * Either way the `$crash` is reported **in-process**: a fatal crash is delivered only if the flush completes before the board resets (or you supply a durable queue). `client.run()` flushes for you before re-raising. * There is no coredump on MicroPython (that is ESP-IDF only); the Python traceback is the equivalent readable backtrace. Wrapper exceptions subclass `HonchError`: `InvalidArgumentError`, `StorageError`, `RejectedError`, `NotInitializedError`, `CompressionUnavailableError`, and `TransportError` — with `OfflineError`, `RateLimitedError`, and `ServerError` subclassing `TransportError`. ## Examples [#examples] `ports/micropython/examples/` includes `basic.py` (above), a persistent-queue example, and a Pico W example. ## Debugging Failures [#debugging-failures] `Client.last_error()` returns a dict describing the most recent failure, so you can see *why* a flush failed rather than just that it did: ```python try: honch.flush() except HonchError: err = honch.last_error() print(err) # {'status': 'rejected', 'reason': 'auth_invalid_key', 'http': 401, # 'os_error': 0, 'message': 'API key invalid or revoked', 'component': 'http'} ``` `status` is `'ok'` and `reason` is `'none'` when nothing has failed yet. The SDK also logs the same one-line summary once per distinct failure through the platform log hook. See [Error Context & Diagnostics](/shared-core#error-context--diagnostics). ## Public API [#public-api] | Method | Purpose | | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Honch(**config)` | Construct and initialize the client. | | `track(event, properties=None)` | Queue an event. | | `last_error()` | Dict of structured detail (reason, http, os\_error, message) for the last failure. | | `identify(distinct_id, traits=None)` | Set identity; emits `$identify`. | | `set_property(key, value=None)` | Emit `$set_property`. | | `session_start(name=None)` / `session_end()` | Bracket a session. | | `report_log_error(message, *, component=None)` | Report a handled, non-fatal error as an `$error`. | | `run(fn, *args, **kwargs)` | Run your entry point with crash capture: an uncaught exception becomes a `$crash` (with traceback), is flushed, and re-raised. Works on every build (no `sys.excepthook` needed). | | `install_error_hook()` / `uninstall_error_hook()` | Alternative to `run()`: capture uncaught exceptions globally via `sys.excepthook` (returns `False` if the firmware lacks it). | | `connectivity_changed(connected)` / `connected()` / `disconnected()` | Report connectivity; emits `$connectivity_change`. | | `tick()` | Cooperative delivery step. | | `flush()` | Send queued batches now (raises `OfflineError` when offline). | | `reset()` | Clear identity, session, and queue. | | `shutdown()` | Emit `$device_shutdown`, final flush, tear down. | | `get_device_id()` | Return the device ID. | | `queue_stats()` | Return queue depth and counters as a dict. | # React Native Relay (/sdks/react-native-relay) The React Native relay is **not a device analytics SDK** — it has no `track` or `identify`. Use it only when firmware cannot upload directly. It receives Honch relay frames that an offline device sends over BLE, durably reassembles complete compact messages, acknowledges receipt to the device, and uploads to Honch. Preview · `0.1.0` . npm package `@honch/react-native-relay` . ## The Host-Owned Bluetooth Model [#the-host-owned-bluetooth-model] Bluetooth is owned by your app, not by this package. It never scans, connects, subscribes, requests BLE permissions, or writes characteristics. Your app owns the BLE stack and hands frame bytes to the relay; the relay returns ACK bytes only after the frame is durably stored, and your app writes those bytes back to the device's ACK characteristic. In short: **Bluetooth is host-owned; durable assembly, ACK construction, and upload are relay-owned.** The BLE service and characteristic UUIDs and the frame format are defined in the relay-chunks spec. ## 1. Install [#1-install] ```bash bun add @honch/react-native-relay ``` Peer dependency: `react-native >= 0.72`. For the MMKV-backed durable store, also add `react-native-mmkv` (optional). The package publishes TypeScript source — Metro transpiles it; there is no separate build step. ## 2. Wire The Relay [#2-wire-the-relay] ```ts import { NativeModules } from "react-native"; import { createMMKV } from "react-native-mmkv"; import { createMmkvRelayStore, createMobileRelay, createRelayNativeBindings, } from "@honch/react-native-relay"; const bindings = createRelayNativeBindings(NativeModules.HonchReactNativeRelay); export const relay = createMobileRelay({ durableStore: createMmkvRelayStore(createMMKV({ id: "honch-relay" })), uploaderConfig: { endpointUrl: "https://i.honch.io", projectKey: "your-project-key", relayId: "mobile-relay-01", relaySdkPlatform: "react-native", relaySdkVersion: "0.1.0", streamId: (m) => `relay-${m.deviceId}`, messageId: (m) => Number(m.sequence), }, schedulerNative: bindings.schedulerNative, }); ``` `createMobileRelay` returns `{ receiveFrame, pending, startUploadScheduler, stopUploadScheduler, drainUploads }`. `uploaderConfig.endpointUrl` defaults to `https://i.honch.io`. ## 3. Hand Frames In And ACK [#3-hand-frames-in-and-ack] When your BLE layer receives a frame notification, pass the bytes to `receiveFrame`. Provide an `acknowledge` callback that writes the returned ACK bytes to the device's ACK characteristic — the relay calls it only after the frame is durably stored. ```ts await relay.receiveFrame(deviceId, frameBytes, { acknowledge: async ({ ackBytes }) => { await writeAckCharacteristic(deviceId, ackBytes); }, }); ``` An ACK is 9 bytes: a version byte (`0x01`) followed by a big-endian uint64 sequence number. ## 4. Upload To Capture [#4-upload-to-capture] Uploads add relay headers on top of the standard contract: ```text POST /capture Content-Type: application/vnd.honch.chunk X-Honch-Project-Key: X-Honch-Stream-Id: X-Honch-Relay-Id: X-Honch-Relay-SDK-Platform: X-Honch-Relay-SDK-Version: ``` | Result | HTTP | Action | | --------- | ---------------------------------- | ---------------------------------------------- | | Accepted | 204 (final), 202 (non-final chunk) | Message delivered / continue. | | Retryable | 408, 409, 429, 5xx | Back off and retry (1 s → 5 min, ±25% jitter). | | Permanent | 400, 401, 404, 413, 415, 422 | Drop. | On iOS, background scheduling requires a native binding; without one, `startUploadScheduler()` drains immediately and uploads are foreground-only. On Android the package registers a `HonchRelayUpload` headless task (add `androidx.work:work-runtime`). ## 5. Native Host Requirements [#5-native-host-requirements] * **iOS**: `NSBluetoothAlwaysUsageDescription`, the `bluetooth-central` background mode. No native background upload module ships in `0.1.0` — call `drainUploads()` from the foreground. * **Android**: `BLUETOOTH_SCAN` / `BLUETOOTH_CONNECT` (and location where required by your OS version), plus `androidx.work:work-runtime` for the headless upload task. The package does not merge BLE, location, or notification permissions into your app manifest — declare what your app needs. ## Public TypeScript Surface [#public-typescript-surface] Frame handling: `decodeRelayFrame`, `createRelayFrameReceiver`, `buildRelayAck`. Queues: `createInMemoryRelayQueue`, `createDurableRelayQueue`, `drainRelayQueue`, `nextBackoffDelayMs`. Durable stores: `createMemoryDurableStore`, `createMmkvRelayStore`. Uploading: `buildRelayUploadBuffer`, `uploadRelayMessage`, `uploadRelayMessageOutcome`. Orchestration: `createMobileRelay`, `createRelayUploadScheduler`, `createRelayNativeBindings`. # Swift Relay (/sdks/swift-relay) The Swift relay is the iOS counterpart to the [React Native relay](/sdks/react-native-relay): a relay/uploader, not a device analytics SDK. It receives Honch relay frames over BLE, durably reassembles complete compact messages, acknowledges receipt to the device, and uploads to Honch. The Swift relay's source is implemented, but a supported Swift Package Manager distribution is not published yet, so there is no install line to copy. If you need the iOS relay today, [contact us](mailto:support@honch.io) . This page documents the surface so you can plan ahead. ## The Host-Owned Bluetooth Model [#the-host-owned-bluetooth-model] As with the React Native relay, your app owns CoreBluetooth — scanning, connection, notification subscription, and ACK characteristic writes. The package validates, reassembles, durably stores, acknowledges, and uploads. It does not touch the BLE stack. The BLE service and characteristic UUIDs and the frame format are defined in the relay-chunks spec. An ACK is 9 bytes: a version byte (`0x01`) followed by a big-endian uint64 sequence number. ## Planned Surface [#planned-surface] The module is `HonchSwiftRelay`. The entry point is an actor: ```swift public actor HonchRelay { init(store:config:uploader:scheduler:nowMs:random:) func receiveFrame(deviceId:frameBytes:acknowledge:) async throws -> RelayFrameReceipt func pending() async throws -> [StoredRelayMessage] func startUploadScheduler() func stopUploadScheduler() func drainUploads() async } ``` `HonchRelayConfig` defaults `endpointURL` to `https://i.honch.io` and `relaySdkPlatform` to `ios`. Supporting types include `RelayUploading` / `URLSessionRelayUploader`, `RelayDurableStore` / `FileRelayStore` / `MemoryRelayStore`, and `RelayScheduling`. ## What's Left For General Availability [#whats-left-for-general-availability] The package lives in a subdirectory of the SDK monorepo and has no plain-semver git tag, so SwiftPM cannot resolve it from a `.package(url:)` declaration yet. Shipping it means publishing it where `Package.swift` is at a repo root with a semver tag (a mirror repo or subtree split). Until then, treat this page as a preview of the API, not an integration guide. ## Use Today [#use-today] * iOS apps that need the relay now: [reach out](mailto:support@honch.io). * Building a relay on another platform: see the [React Native relay](/sdks/react-native-relay) for the working reference, or the [relay flow](/concepts#relay-flow) for the model.