# Embedded C Library

> Build a C99 device client for Thalovant hubs with your own MQTT or WebSocket transport.

Source: https://docs.thalovant.com/developers/sdks/embedded-c/

---
Use the embedded C library when your client is a microcontroller or small device, such as ESP32, Zephyr, bare-metal, or a Linux single-board computer.

This is a protocol library, not a full SDK. It is transport-agnostic: you bring your own MQTT client or WebSocket client and your own TLS stack, and the library provides everything protocol-specific. It is pure C99 with zero external dependencies, and the core paths never allocate; every function writes into caller-provided buffers.

Source and releases: [thalovant-embedded-c on GitHub](https://github.com/thalovant/thalovant-embedded-c).

The library gives a device everything it needs to open an encrypted, authenticated session with a hub; your application supplies the sockets or broker connection and drives the exchange. Older envelope helpers remain for legacy integrations, but they cannot connect to a current hub.

## What The Library Provides

  
    Parse the identity JSON issued by the Thalovant API, with the same field aliases the Node and Go SDKs accept.
  
  
    Derive per-client MQTT topics, connection endpoints, ports, and client IDs, byte-for-byte identical to the Node SDK.
  
  
    Authenticated encryption for every message, verified against independent test vectors.
  
  
    Build and parse hub messages, run the authentication exchange, and encrypt every message, with ask request and reply helpers.
  

## What You Bring

| You provide | The library provides |
| --- | --- |
| MQTT and/or WebSocket client | Topics, endpoints, frame and envelope codecs |
| TLS stack | TLS flag plus scheme and port parsing |
| Cryptographically secure random generator | You supply a persistent client key and a fresh ephemeral key for every connection; the library manages message counters |
| Event loop and timers | Frame classifier and ask-loop semantics |
| Protected storage for the identity, the client key, and the saved hub identity | Identity parser and an authentication exchange that checks the saved hub identity |

The library has no control-plane client. Create the client identity through the [dashboard](/manage/connect-a-client/), the API, or another SDK, then store the identity JSON on the device.

## Get A Release

Use a fresh Ask request ID and a fresh Query ID for every logical operation, including later calls. Keep concurrent IDs unique within each matching namespace; Ask and Query namespaces are independent. This library leaves collector ownership to your transport. Check every negative builder or codec result before casting a length or publishing bytes.

The shallow object-key scanner validates the complete direct object before returning a matching field, including separators after that field. Nested containers retain their documented shallow parsing rules.

Vendor the library or fetch it by an immutable release tag. The current tag is `v0.8.0`.

```sh
git submodule add https://github.com/thalovant/thalovant-embedded-c.git \
    third_party/thalovant-embedded-c
git -C third_party/thalovant-embedded-c checkout v0.8.0
```

CMake `FetchContent`, ESP-IDF component references, and Zephyr west manifests work the same way with the `v0.8.0` tag. Each release includes a source archive and a `SHA256SUMS` file for checking your download.

Building needs only a C99 compiler:

```sh
make            # build/libthalovant.a
make test       # host-side, offline test suite
```

## Integration Sketch

Use the [integration guide](https://github.com/thalovant/thalovant-embedded-c/blob/main/docs/noise-v3.md) in the repository for the complete API and transport sequence:

1. Parse the identity and connect your TLS-protected transport. Sign in to the endpoint with the identity's credentials.
2. Keep the server's complete greeting and offer, and pick the mutual options with `thalovant_noise_select`.
3. Derive the session secret from the identity password and the server's node ID with `thalovant_noise_psk`. It needs 64 MiB of caller-owned `uint64_t` scratch memory and wipes it afterwards. A smaller device must receive the **same secret** through secure provisioning after deriving it off-device; never weaken the derivation.
4. Initialise the session with `thalovant_noise_init`, your protected client key, a fresh cryptographically random ephemeral key, and any saved hub identity. Exchange the authentication messages with `thalovant_noise_write` and `thalovant_noise_read`.
5. Verify and save the hub's identity. Send the encrypted client greeting before application messages.
6. Build ask or intent JSON with the frame helpers, encrypt with `thalovant_noise_encrypt`, and decrypt replies with `thalovant_noise_decrypt` before classifying them. Any authentication or framing error closes the session.

Your transport must keep all chunks of one message together, cap encrypted frames at 65535 bytes, and split large payloads into 65000-byte chunks. Reassemble into a caller-owned bounded buffer no larger than 32 MiB; choose a smaller limit if the device requires it. A reconnect needs a fresh ephemeral key while keeping the client key and the saved hub identity.

  Store client credentials, derived secrets, client keys, and saved hub identities in protected storage. Never include them in published firmware images. A changed hub identity needs confirmation from the hub owner before you save it.

## List What The Hub Can Be Asked

A connected client can list every intent its hub answers, per language, with the sentences a person says to reach each one, over its own session and with no control-plane token. See [What Can My Hub Be Asked?](/concepts/hub-intents/) for what the hub returns, which message types the connection must be allowed to publish, and what a refusal or a failed listing looks like.

Since release `v0.2.0` the library carries the protocol side of it, in the same style as `thalovant_ask_*`. `thalovant_intent_list_build_frame` and `thalovant_intent_describe_build_frame` build the `ovos.intent.list` and `ovos.intent.describe` frames, and `thalovant_intent_classify` reads a reply into a `thalovant_intent_event`. Your transport seals and sends the frames, feeds the replies back, and correlates them by the request id you set.

```c
struct inventory_query { const char *request_id; bool answered; };

static bool on_row(const thalovant_intent_registration *row, void *user)
{
    printf("%s:%s (%s) %s\n", row->skill_id, row->intent_name, row->lang, row->method);
    return true;                       /* false stops the walk */
}

struct inventory_query query = { "req-2", false };
thalovant_intent_list_request list = { "en-us", "sess-1", identity.site_id,
                                       query.request_id, false };
if (thalovant_intent_list_build_frame(&list, frame, sizeof(frame)) < 0) {
    /* THALOVANT_ERR_NOMEM: frame[] too small; nothing was built to send */
}
/* seal and publish the frame, then classify each reply that arrives */

thalovant_intent_event reply;
thalovant_intent_classify(plaintext, plaintext_len, query.request_id, &reply);
switch (reply.kind) {
case THALOVANT_INTENT_LIST_RESPONSE:
    if (query.answered) break;         /* ignore a duplicate reply */
    query.answered = true;
    thalovant_intent_list_rows(&reply, on_row, NULL);
    break;
case THALOVANT_INTENT_POLICY_DENIED:   /* reply.denied_type names the query */
    break;
default: break;                        /* THALOVANT_INTENT_IGNORE */
}
```

The walkers deliver one item at a time through a callback, so a manifest of any size is read in bounded memory: `thalovant_intent_list_rows` for the registrations, `thalovant_intent_definitions` for a describe reply, and `thalovant_intent_samples` for the sentences behind one definition. `thalovant_intent_same_language` folds language tags the way the other SDKs do, so `fr-fr` and `fr_FR` match. A row's `method` is `template` or `keyword`, which the full SDKs expose as the engines `padatious` and `adapt`.

A refusal arrives as `THALOVANT_INTENT_POLICY_DENIED` with the refused type in `reply.denied_type`. A listing the hub accepted and failed is a different case: since `v0.3.0`, `thalovant_intent_list_rows` returns `THALOVANT_ERR_HUB_REFUSED` with the hub's own words in `reply.error`, because a failed listing is not an empty hub. `thalovant_intent_definitions` returns zero definitions for the same answer to a describe, which is a real one: the hub does not know that registration. Correlation, retries, and the result model are yours to own; see [What Can My Hub Be Asked?](/concepts/hub-intents/) for the shape the full SDKs build from the same replies.

## When To Use A Full SDK Instead

Use [Python](/developers/sdks/python/), [Node.js](/developers/sdks/node/), [Go](/developers/sdks/go/), or [Rust](/developers/sdks/rust/) when the device can run a full runtime. Those SDKs own the transport, reconnect logic, and control-plane calls for you. Use [MQTT](/developers/mqtt/) for broker details and topic scope.

## Common Issues

| Symptom | Check |
| --- | --- |
| Identity parse fails | Confirm the stored JSON is the untouched identity payload from the API or dashboard download. |
| Broker rejects the connection | Confirm the derived endpoint, port, and TLS flag, and use the per-client broker credentials from the identity. |
| Replies never classify | Confirm the subscribe topic is `topics.outbound` and the request ID matches the one sent in the ask frame. |
| Authentication or decryption fails | Confirm the password, the server node ID, the saved hub identity, and the frame order. Close the failed session; do not retry with legacy envelopes or delete the saved hub identity automatically. |
## Fallback Handler Frames

`thalovant_fallback_list_build_payload` and `thalovant_fallback_list_build_frame` build the fallback listing. Both use `thalovant_intent_list_request` for session and request context, and send `ovos.skills.fallback.list` with an empty data object; handlers are language-neutral. `thalovant_intent_classify` identifies the response as `THALOVANT_FALLBACK_LIST_RESPONSE` and exposes the `data.fallbacks` array through `items_json`, `items_len`, and `count`.

The application owns the 1.5-second optional probe, priority parsing, sorting, and result model. Keep an unavailable, failed, or malformed response distinct from an explicitly empty list. Missing intent phrases alone cannot establish that a hub will not answer. See [fallback discovery](/concepts/hub-intents/#fallback-handlers-and-language-availability).

The library supports GCC, Clang, and 32-bit targets. It uses C99, caller-owned buffers, and the transport you supply.

## Request Hints, Examples, And Embedded Skill Audio

`thalovant_ask_build_payload_with_hints` and `thalovant_ask_build_frame_with_hints`
accept `thalovant_ask_hints`: `stt_lang`, a JSON string array `pipeline_json`,
and a JSON object `location_json`. Language and pipeline stages are trimmed;
empty stages are omitted. Build the location object with
`thalovant_build_location`; a city is required and invalid/zero coordinates are
omitted. Existing builders retain their original wire bytes.

The complete frame uses the caller's output buffer directly. Each decoded
pipeline stage must fit `THALOVANT_ASK_TEXT_MAX` including its terminator;
hint JSON must fit `THALOVANT_WIRE_MAX_TOKENS`. These compile-time limits keep
the tokenizer and normalization buffers bounded without dynamic allocation.

`thalovant_speakable` renders an intent pattern into a caller-owned buffer with
optional `thalovant_speakable_slot` replacements. Rank rendered examples using
the original pattern's slot presence, retain the best rank when deduplicating,
and apply the requested limit afterward.

`thalovant_ask_classify` recognizes `THALOVANT_ASK_AUDIO` without marking it a
failure. Audio never settles or extends an ask: retain its order alongside speech
within the existing reply window. `thalovant_ask_event_language` returns the
first nonempty language from event data, context, or session.
`thalovant_ask_audio_decode` validates correlation and decodes only embedded
hexadecimal audio into caller-owned storage; scratch space holds the unescaped
hex string. It never reads a file or URL. Check its returned length/error before
playing bytes. Buffer limits may be smaller than the 4 MiB clip maximum.

Zero-initialize `thalovant_audio_budget` for each reply and call
`thalovant_audio_budget_accept` before retaining encoded audio. It caps each
clip at 4 MiB and all reply clips at 16 MiB (encoded upper bounds); rejected
clips increment `dropped` and do not affect speech collection. Deduplicate
transport deliveries before collection. The library remains transport-agnostic;
control-plane skill/configuration management belongs to managed SDK clients.

## Reply Claims

`thalovant_ask_event_pipeline_id()` and `thalovant_ask_event_skill_id()` extract string stamps from recognized, correlated reply frames into caller-provided buffers. Keep distinct IDs in first-seen order in your own storage. `thalovant_reply_claimed()` classifies the reply from its handled/failure status and the retained pipeline IDs. Embedded NUL is rejected; insufficient buffers return `THALOVANT_ERR_NOMEM`.

Pipeline and skill IDs are nonempty strings in first-seen order, with duplicates removed. A successful reply with only pipeline IDs containing the case-sensitive text `fallback` reports an unclaimed reply. Any non-fallback stage makes a successful reply claimed. Successful replies from older hubs without stage stamps remain claimed. Failed or unhandled replies are never claimed. Malformed non-string stamps are ignored.

Use this as a hint when deciding whether to continue a conversation. Fallback text and the existing success status are retained. These stamps are not verified peer identity and must not authorize actions.
