Hanzo ZT

C SDK

Complete documentation for zt-sdk-c, a stable-ABI C library providing ZAP framing helpers and billing integration over libzt.

C SDK

The C SDK (zt-sdk-c) provides a minimal, stable-ABI interface for embedding Hanzo ZT in C programs and as a foundation for FFI bindings in other languages. It wraps libzt with ZAP length-prefixed framing, billing guards, and straightforward error codes.

Repository: github.com/hanzozt/zt-sdk-c

Installation

From Source

git clone https://github.com/hanzozt/zt-sdk-c.git
cd zt-sdk-c
make
sudo make install

This installs headers to /usr/local/include/zt/ and the library to /usr/local/lib/.

Linking

cc -o app app.c -lzt -lzt_zap

Or with pkg-config:

cc -o app app.c $(pkg-config --cflags --libs zt_zap)

CMake

find_package(PkgConfig REQUIRED)
pkg_check_modules(ZT_ZAP REQUIRED zt_zap)

add_executable(my_app main.c)
target_link_libraries(my_app ${ZT_ZAP_LIBRARIES})
target_include_directories(my_app PRIVATE ${ZT_ZAP_INCLUDE_DIRS})

Project Structure

includes/zt/
  zt_zap.h          ZAP framing, billing, and context functions

library/
  zt_zap.c          Implementation

examples/
  echo_client.c     Minimal echo client example
  billing_check.c   Billing guard example

API Overview

All functions return int where 0 indicates success and negative values indicate errors. Opaque handles are used for context and guard objects to maintain ABI stability.

Return Codes

CodeConstantDescription
0ZT_OKSuccess
-1ZT_ERR_AUTHAuthentication failed
-2ZT_ERR_NOT_AUTHNot authenticated (call zt_zap_authenticate first)
-3ZT_ERR_SERVICEService not found
-4ZT_ERR_NO_ROUTERSNo edge routers available
-5ZT_ERR_CONNECTConnection failed
-6ZT_ERR_CLOSEDConnection closed
-7ZT_ERR_BALANCEInsufficient balance
-8ZT_ERR_BILLINGBilling API error
-9ZT_ERR_CONFIGInvalid configuration
-10ZT_ERR_TIMEOUTOperation timed out
-11ZT_ERR_IOI/O error
-12ZT_ERR_FRAMEFraming error (truncated or oversized)
-99ZT_ERR_UNKNOWNUnknown error

Error Strings

Convert a return code to a human-readable string:

const char *zt_zap_strerror(int code);
int rc = zt_zap_authenticate(ctx);
if (rc != ZT_OK) {
    fprintf(stderr, "Error: %s\n", zt_zap_strerror(rc));
}

Context and Connection

zt_zap_ctx_t

Opaque handle for a ZT context. Manages authentication, service discovery, and connections.

#include <zt/zt_zap.h>

/* Configuration */
zt_zap_config_t config = {
    .controller_url = "https://zt-api.hanzo.ai",
    .auth_token     = getenv("HANZO_API_KEY"),
    .billing        = 1,
    .commerce_url   = "https://api.hanzo.ai/commerce",
    .connect_timeout_ms = 30000,
    .request_timeout_ms = 15000,
};

/* Create context */
zt_zap_ctx_t *ctx = NULL;
int rc = zt_zap_ctx_create(&config, &ctx);
if (rc != ZT_OK) {
    fprintf(stderr, "Create failed: %s\n", zt_zap_strerror(rc));
    return 1;
}

/* Authenticate */
rc = zt_zap_authenticate(ctx);
if (rc != ZT_OK) {
    fprintf(stderr, "Auth failed: %s\n", zt_zap_strerror(rc));
    zt_zap_ctx_destroy(ctx);
    return 1;
}

/* Use context... */

/* Cleanup */
zt_zap_ctx_destroy(ctx);

Context functions:

FunctionDescription
zt_zap_ctx_create(config, out_ctx)Create a new context from configuration
zt_zap_authenticate(ctx)Authenticate with the controller
zt_zap_ctx_destroy(ctx)Destroy context and free all resources

zt_zap_config_t

Configuration structure:

FieldTypeDescription
controller_urlconst char *ZT controller URL (required)
auth_tokenconst char *JWT or API key (required)
billingintEnable billing (1 = yes, 0 = no)
commerce_urlconst char *Commerce API URL (NULL for default)
identity_fileconst char *Path to identity file (NULL for none)
connect_timeout_msuint32_tConnection timeout in milliseconds (0 = default 30000)
request_timeout_msuint32_tRequest timeout in milliseconds (0 = default 15000)

Dialing Services

int sock = -1;
rc = zt_zap_dial(ctx, "echo-service", &sock);
if (rc != ZT_OK) {
    fprintf(stderr, "Dial failed: %s\n", zt_zap_strerror(rc));
    zt_zap_ctx_destroy(ctx);
    return 1;
}

/* Use sock for framed I/O... */

zt_zap_close(sock);
FunctionDescription
zt_zap_dial(ctx, service, out_sock)Dial a service, returns socket descriptor
zt_zap_close(sock)Close a connection socket

ZAP Framing

The core of the C SDK: length-prefixed message framing over ZT sockets. Every ZAP message on the wire is structured as:

[4 bytes: big-endian payload length][payload bytes]

This framing ensures message boundaries are preserved over the stream-oriented ZT transport.

zt_zap_send_frame

Send a length-prefixed frame:

int zt_zap_send_frame(int sock, const void *payload, uint32_t len);

Writes a 4-byte big-endian length header followed by len bytes of payload. Returns ZT_OK on success or a negative error code on failure.

const char *msg = "Hello, ZAP!";
rc = zt_zap_send_frame(sock, msg, strlen(msg));
if (rc != ZT_OK) {
    fprintf(stderr, "Send failed: %s\n", zt_zap_strerror(rc));
}

zt_zap_recv_frame

Receive a length-prefixed frame:

int zt_zap_recv_frame(int sock, void *buf, uint32_t max_len, uint32_t *out_len);

Reads the 4-byte length header, then reads exactly that many bytes into buf. The actual payload length is written to out_len. Returns ZT_ERR_FRAME if the incoming frame exceeds max_len.

uint8_t buf[4096];
uint32_t received = 0;

rc = zt_zap_recv_frame(sock, buf, sizeof(buf), &received);
if (rc != ZT_OK) {
    fprintf(stderr, "Recv failed: %s\n", zt_zap_strerror(rc));
} else {
    printf("Received %u bytes\n", received);
}

Framing Summary

FunctionSignatureDescription
zt_zap_send_frame(sock, payload, len)Send a 4-byte BE prefix + payload
zt_zap_recv_frame(sock, buf, max_len, out_len)Read a framed message into buf

Authentication

The C SDK takes an auth token string directly. Resolution of JWT credentials from environment or files is the caller's responsibility (unlike the C++ SDK which has HanzoAuth::resolve()).

Typical pattern:

#include <zt/zt_zap.h>
#include <stdlib.h>

const char *token = getenv("HANZO_API_KEY");
if (!token) {
    fprintf(stderr, "Set HANZO_API_KEY\n");
    return 1;
}

zt_zap_config_t config = {
    .controller_url = "https://zt-api.hanzo.ai",
    .auth_token     = token,
};

For loading tokens from a JSON file, you can use a helper or parse it yourself:

#include <stdio.h>
#include <string.h>

/* Minimal token reader (production code should use a JSON parser) */
static int read_token(const char *path, char *buf, size_t buflen) {
    FILE *f = fopen(path, "r");
    if (!f) return -1;

    /* Read file, extract "token" field value */
    char line[1024];
    while (fgets(line, sizeof(line), f)) {
        char *p = strstr(line, "\"token\"");
        if (p) {
            p = strchr(p, ':');
            if (p) {
                p = strchr(p, '"');
                if (p) {
                    p++;
                    char *end = strchr(p, '"');
                    if (end && (size_t)(end - p) < buflen) {
                        memcpy(buf, p, end - p);
                        buf[end - p] = '\0';
                        fclose(f);
                        return 0;
                    }
                }
            }
        }
    }
    fclose(f);
    return -1;
}

Billing

BillingGuard

The billing guard checks account balance before allowing connections and records usage after sessions.

/* Create a billing guard */
zt_zap_billing_guard_t *guard = NULL;
rc = zt_zap_create_billing_guard(
    "https://api.hanzo.ai/commerce",
    getenv("HANZO_API_KEY"),
    &guard
);
if (rc != ZT_OK) {
    fprintf(stderr, "Guard creation failed: %s\n", zt_zap_strerror(rc));
    return 1;
}

/* Check balance before connecting */
rc = zt_zap_check_balance(guard, "my-service");
if (rc == ZT_ERR_BALANCE) {
    fprintf(stderr, "Insufficient balance. Add credits.\n");
    zt_zap_destroy_billing_guard(guard);
    return 1;
}

/* ... use the service ... */

/* Record usage after session */
zt_zap_usage_record_t usage = {
    .service       = "my-service",
    .session_id    = "sess_abc123",
    .bytes_sent    = 4096,
    .bytes_received = 8192,
    .duration_ms   = 5000,
};
rc = zt_zap_record_usage(guard, &usage);
if (rc != ZT_OK) {
    fprintf(stderr, "Usage recording failed: %s\n", zt_zap_strerror(rc));
}

/* Cleanup */
zt_zap_destroy_billing_guard(guard);

Billing functions:

FunctionDescription
zt_zap_create_billing_guard(url, token, out_guard)Create a billing guard handle
zt_zap_check_balance(guard, service)Check balance; returns ZT_ERR_BALANCE if insufficient
zt_zap_record_usage(guard, record)Submit a usage record to Commerce API
zt_zap_destroy_billing_guard(guard)Destroy guard and free resources

zt_zap_usage_record_t fields:

FieldTypeDescription
serviceconst char *Service name
session_idconst char *Session identifier
bytes_sentuint64_tTotal bytes sent
bytes_receiveduint64_tTotal bytes received
duration_msuint64_tSession duration in milliseconds

When billing is enabled in the context config, zt_zap_dial automatically calls zt_zap_check_balance and returns ZT_ERR_BALANCE if the check fails.

Error Handling

Every function returns an int status code. Always check the return value.

int rc = zt_zap_dial(ctx, "my-service", &sock);
switch (rc) {
    case ZT_OK:
        /* success */
        break;
    case ZT_ERR_AUTH:
        fprintf(stderr, "Authentication failed\n");
        break;
    case ZT_ERR_NOT_AUTH:
        fprintf(stderr, "Not authenticated. Call zt_zap_authenticate first.\n");
        break;
    case ZT_ERR_SERVICE:
        fprintf(stderr, "Service not found\n");
        break;
    case ZT_ERR_NO_ROUTERS:
        fprintf(stderr, "No edge routers available\n");
        break;
    case ZT_ERR_BALANCE:
        fprintf(stderr, "Insufficient balance\n");
        break;
    case ZT_ERR_CONNECT:
        fprintf(stderr, "Connection failed\n");
        break;
    case ZT_ERR_TIMEOUT:
        fprintf(stderr, "Connection timed out\n");
        break;
    default:
        fprintf(stderr, "Error %d: %s\n", rc, zt_zap_strerror(rc));
        break;
}

Defensive Pattern

A common pattern for robust error handling:

#define ZT_CHECK(expr)                                        \
    do {                                                      \
        int _rc = (expr);                                     \
        if (_rc != ZT_OK) {                                   \
            fprintf(stderr, "%s:%d: %s failed: %s\n",         \
                    __FILE__, __LINE__, #expr,                 \
                    zt_zap_strerror(_rc));                     \
            goto cleanup;                                     \
        }                                                     \
    } while (0)

int main(void) {
    zt_zap_ctx_t *ctx = NULL;
    int sock = -1;
    int exit_code = 1;

    zt_zap_config_t config = {
        .controller_url = "https://zt-api.hanzo.ai",
        .auth_token     = getenv("HANZO_API_KEY"),
    };

    ZT_CHECK(zt_zap_ctx_create(&config, &ctx));
    ZT_CHECK(zt_zap_authenticate(ctx));
    ZT_CHECK(zt_zap_dial(ctx, "echo-service", &sock));

    /* ... */
    exit_code = 0;

cleanup:
    if (sock >= 0) zt_zap_close(sock);
    if (ctx) zt_zap_ctx_destroy(ctx);
    return exit_code;
}

Quick Start

Minimal program that connects to a service and exchanges a single framed message:

#include <zt/zt_zap.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    const char *token = getenv("HANZO_API_KEY");
    if (!token) {
        fprintf(stderr, "Set HANZO_API_KEY\n");
        return 1;
    }

    zt_zap_config_t config = {
        .controller_url = "https://zt-api.hanzo.ai",
        .auth_token     = token,
    };

    zt_zap_ctx_t *ctx = NULL;
    if (zt_zap_ctx_create(&config, &ctx) != ZT_OK) {
        fprintf(stderr, "Failed to create context\n");
        return 1;
    }

    if (zt_zap_authenticate(ctx) != ZT_OK) {
        fprintf(stderr, "Auth failed\n");
        zt_zap_ctx_destroy(ctx);
        return 1;
    }

    int sock = -1;
    if (zt_zap_dial(ctx, "echo-service", &sock) != ZT_OK) {
        fprintf(stderr, "Dial failed\n");
        zt_zap_ctx_destroy(ctx);
        return 1;
    }

    /* Send a framed message */
    const char *msg = "Hello, ZT!";
    zt_zap_send_frame(sock, msg, strlen(msg));

    /* Receive framed response */
    uint8_t buf[4096];
    uint32_t len = 0;
    if (zt_zap_recv_frame(sock, buf, sizeof(buf), &len) == ZT_OK) {
        printf("Response: %.*s\n", (int)len, buf);
    }

    zt_zap_close(sock);
    zt_zap_ctx_destroy(ctx);
    return 0;
}

Compile and run:

cc -o echo_client echo_client.c -lzt -lzt_zap
HANZO_API_KEY=your-token ./echo_client

Complete Example

Full example with context creation, authentication, billing guard, framed I/O, usage recording, and cleanup:

#include <zt/zt_zap.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define ZT_CHECK(expr)                                        \
    do {                                                      \
        int _rc = (expr);                                     \
        if (_rc != ZT_OK) {                                   \
            fprintf(stderr, "%s:%d: %s -> %s\n",              \
                    __FILE__, __LINE__, #expr,                 \
                    zt_zap_strerror(_rc));                     \
            goto cleanup;                                     \
        }                                                     \
    } while (0)

int main(void) {
    const char *token = getenv("HANZO_API_KEY");
    if (!token) {
        fprintf(stderr, "Set HANZO_API_KEY\n");
        return 1;
    }

    zt_zap_ctx_t            *ctx   = NULL;
    zt_zap_billing_guard_t  *guard = NULL;
    int                      sock  = -1;
    int                      ret   = 1;

    /* 1. Configure */
    zt_zap_config_t config = {
        .controller_url     = "https://zt-api.hanzo.ai",
        .auth_token         = token,
        .billing            = 1,
        .commerce_url       = "https://api.hanzo.ai/commerce",
        .connect_timeout_ms = 30000,
        .request_timeout_ms = 15000,
    };

    /* 2. Create context and authenticate */
    ZT_CHECK(zt_zap_ctx_create(&config, &ctx));
    ZT_CHECK(zt_zap_authenticate(ctx));
    printf("Authenticated.\n");

    /* 3. Create billing guard and check balance */
    ZT_CHECK(zt_zap_create_billing_guard(
        config.commerce_url, token, &guard));
    ZT_CHECK(zt_zap_check_balance(guard, "echo-service"));
    printf("Balance OK.\n");

    /* 4. Dial the service */
    ZT_CHECK(zt_zap_dial(ctx, "echo-service", &sock));
    printf("Connected to echo-service.\n");

    /* 5. Send a framed message */
    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);

    const char *request = "Hello from C!";
    uint32_t req_len = (uint32_t)strlen(request);
    ZT_CHECK(zt_zap_send_frame(sock, request, req_len));

    /* 6. Receive framed response */
    uint8_t response[4096];
    uint32_t resp_len = 0;
    ZT_CHECK(zt_zap_recv_frame(sock, response, sizeof(response), &resp_len));

    clock_gettime(CLOCK_MONOTONIC, &end);
    uint64_t elapsed_ms = (uint64_t)(
        (end.tv_sec - start.tv_sec) * 1000 +
        (end.tv_nsec - start.tv_nsec) / 1000000
    );

    printf("Response: %.*s (%llu ms)\n",
           (int)resp_len, response,
           (unsigned long long)elapsed_ms);

    /* 7. Record usage */
    zt_zap_usage_record_t usage = {
        .service        = "echo-service",
        .session_id     = "sess_c_001",
        .bytes_sent     = req_len + 4,   /* payload + 4-byte header */
        .bytes_received = resp_len + 4,
        .duration_ms    = elapsed_ms,
    };
    ZT_CHECK(zt_zap_record_usage(guard, &usage));
    printf("Usage recorded.\n");

    ret = 0;

cleanup:
    if (sock >= 0)  zt_zap_close(sock);
    if (guard)      zt_zap_destroy_billing_guard(guard);
    if (ctx)        zt_zap_ctx_destroy(ctx);
    return ret;
}

Compile and run:

cc -o full_example full_example.c -lzt -lzt_zap
HANZO_API_KEY=your-token ./full_example

Expected output:

Authenticated.
Balance OK.
Connected to echo-service.
Response: Hello from C! (12 ms)
Usage recorded.

ABI Stability

The C SDK maintains a stable ABI:

  • All handles (zt_zap_ctx_t, zt_zap_billing_guard_t) are opaque pointers. Internal struct layouts can change without breaking callers.
  • Config structs use fixed-layout fields. New fields are appended; zero-initialized fields use defaults.
  • Error codes are stable. New codes are added at the end of the negative range.
  • Function signatures do not change within a major version.

This makes the C SDK suitable as a foundation for FFI bindings in languages like Python (ctypes/cffi), Ruby (FFI), and Lua.

Thread Safety

  • zt_zap_ctx_t is safe to share across threads after creation. zt_zap_dial serializes internally.
  • Individual sockets (from zt_zap_dial) must not be shared across threads without external synchronization.
  • zt_zap_billing_guard_t is safe to share across threads.
  • zt_zap_send_frame and zt_zap_recv_frame must not be called concurrently on the same socket.

Dependencies

LibraryPurpose
libztZeroTier network library
C99 standard libraryCore functionality
POSIX socketsUnderlying I/O (Linux, macOS)

No external dependencies beyond libzt. The SDK builds with any C99-compliant compiler.

On this page