Linkit
SDK Reference

Zig SDK

Allocator-aware, transport-first Linkit Zig SDK — Zig 0.16.0+, pluggable HTTP, fluent builders, canonical route parity, and zero forced runtime dependencies.

Zig SDK

The Linkit Zig SDK provides an allocator-aware, transport-first client for the Linkit e-commerce API. Built for Zig 0.16.0+, it uses the idiomatic std.ArrayList (unmanaged-by-default pattern), std.Io for realtime delays (for example retry backoff), fluent builders with compile-time + runtime validation, and pluggable transport injection.

Design Goal

Lean and explicit: allocator-aware APIs, pluggable transport, fluent builders, and canonical /api/v1/* route parity. Zero forced runtime dependencies — inject your own HTTP stack.


Installation

// In your build.zig — add linkit as a module dependency
const linkit_dep = b.dependency("linkit", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("linkit", linkit_dep.module("linkit"));

// build.zig.zon
.dependencies = .{
    .linkit = .{
        .url = "https://linkit.works/downloads/sdks/zig/linkit-zig-0.2.0.tar.gz",
        .hash = "...",
    },
},
# Download and extract
tar xf linkit-zig-0.2.0.tar.gz

# In your build.zig:
const linkit_module = b.addModule("linkit", .{
    .root_source_file = b.path("linkit/src/linkit.zig"),
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("linkit", linkit_module);
# Copy SDK into your project
cp -r sdks/zig/linkit ./deps/linkit

# build.zig:
const linkit_module = b.addModule("linkit", .{
    .root_source_file = b.path("deps/linkit/src/linkit.zig"),
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("linkit", linkit_module);

Build & Test

cd sdks/zig/linkit
zig build test

Zig 0.16.0+ Required

The SDK targets Zig 0.16.0 or later — for example std.Io.sleep for retry backoff, std.mem.trimEnd, and the current std.ArrayList layout. Use a 0.16.x toolchain when building or running zig build test.


Quick Start

Implement a Transport

const std = @import("std");
const linkit = @import("linkit");

fn httpSend(ctx: *anyopaque, req: linkit.Request, allocator: std.mem.Allocator) !linkit.Response {
    _ = ctx;
    // Implement using std.http.Client, zig-curl, etc.
    // Map req.method, req.url, req.body, req.bearer_token
    return .{
        .status = 200,
        .body = try allocator.dupe(u8, "{\"status\":\"healthy\"}"),
    };
}

Initialize the Client

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();

var transport_state: u8 = 0;
var client = try linkit.Client.init(gpa.allocator(), .{
    .base_url = "https://linkit.works/api/v1",
    .jwt_token = "your-jwt-token",
    .timeout_ms = 30_000,
    .max_retries = 3,
}, .{
    .ctx = &transport_state,
    .send_fn = httpSend,
});

Use Service APIs

// Health check — no auth header sent
var health = try client.health().check();
defer health.deinit(gpa.allocator());

if (!health.ok()) {
    std.debug.print("API unhealthy: {d}\n", .{health.status});
    return;
}

// Fetch a product by IV ID
var product = try client.products().getByIvId("PROD-001");
defer product.deinit(gpa.allocator());
std.debug.print("Product: {s}\n", .{product.body});

Configuration

var client = try linkit.Client.init(allocator, .{
    .base_url = "https://linkit.works/api/v1",
    .jwt_token = "your-jwt-token",
    .timeout_ms = 30_000,
    .max_retries = 3,
    .retry_base_delay_ms = 150,
    .max_concurrent_requests = 16,
    .user_agent = "MyApp/1.0 LinkitZigSDK/0.2.0",
}, transport);

Configuration Fields

FieldTypeDefaultDescription
base_url[]const u8requiredAPI base URL (trailing / is stripped)
jwt_token[]const u8""JWT authentication token
timeout_msu3230_000Request timeout in milliseconds
max_retriesu83Maximum retry attempts for transient errors
retry_base_delay_msu32150Base delay for exponential backoff
max_concurrent_requestsu1616Concurrency limit
user_agent[]const u8"LinkitZigSDK/0.2.0"User-Agent header value

Config Validation

The client constructor validates configuration at init time:

// These will return errors at init:
// - Empty base_url → error.InvalidBaseUrl
// - Missing http:// or https:// → error.InvalidBaseUrl
// - timeout_ms < 1000 → error.InvalidTimeout
// - max_concurrent_requests == 0 → error.InvalidConcurrency

Services Reference

ServiceMethodsDescription
client.health()check, detailed, getSystem, pingSystem health (no auth)
client.products()create, createRequest, getByIvId, update, updateRequest, deleteByIvId, list, listAllProduct catalog
client.skus()create, createRequest, getByIvId, update, updateRequest, updateStock, updateStockRequest, deleteByIvId, list, listAllSKU inventory per branch
client.branches()create, createRequest, getByIvId, update, updateRequest, deleteByIvId, list, listAllPhysical locations
client.customers()create, getById, update, deleteById, getAddresses, createAddress, createGroup, list, listAllCustomer management
client.orders()getById, updateStatus, listOrder management
client.offers()create, getById, update, updateStatus, deleteById, list, bulkUpsertPromotions & discounts
client.brands()create, getById, update, deleteByIdBrand management
client.categories()create, getById, update, deleteByIdProduct categories
client.generics()create, getById, update, deleteByIdGeneric entities
client.integrations()getConfig, execute, enable, disablePlatform connections

Products

Create with JSON

var product = try client.products().create(
    \\{"ivId":"PROD-001","nameEn":"Premium Coffee","averagePrice":29.99}
);
defer product.deinit(allocator);

Create with Typed Request

var builder = linkit.ProductCreateBuilder{};
const request = try builder
    .withIvId("PROD-001")
    .withNameEn("Premium Coffee")
    .withNameAr("قهوة ممتازة")
    .withAveragePrice(29.99)
    .withEnabled(true)
    .build();

var product = try client.products().createRequest(request);
defer product.deinit(allocator);

List with Filters

var products = try client.products().list(.{
    .page = 1,
    .limit = 50,
    .enabled = true,
    .category_id = "beverages",
    .brand_id = "brand-01",
    .search = "coffee",
});
defer products.deinit(allocator);

Auto-Pagination Iterator

var iterator = client.products().listAll(.{ .limit = 100 });

while (try iterator.next()) |*page| {
    defer page.deinit(allocator);
    // Process page.body JSON
    std.debug.print("Page: {s}\n", .{page.body});
}

SKUs

Create with Builder

var sku_builder = linkit.SkuCreateBuilder{};
const sku_request = try sku_builder
    .withIvId("SKU-001")
    .inBranch("BR-RYD-001")
    .forProduct("PROD-001")
    .withPrice(29.99)
    .withQty(1000)
    .withMaxQty(5000)
    .withAvailable(true)
    .withBarcode("6281234567890")
    .build();

var sku = try client.skus().createRequest(sku_request);
defer sku.deinit(allocator);

Update Stock

var stock_builder = linkit.SkuStockUpdateBuilder{};
const stock_request = try stock_builder
    .withIvId("SKU-001")
    .inBranch("BR-RYD-001")
    .withQty(950)
    .withAvailable(true)
    .build();

var result = try client.skus().updateStockRequest("SKU-001", stock_request, "BR-RYD-001");
defer result.deinit(allocator);

List by Branch

var skus = try client.skus().list(.{
    .branch_iv_id = "BR-RYD-001",
    .available = true,
    .low_stock = false,
    .page = 1,
    .limit = 25,
});
defer skus.deinit(allocator);

Branches

Create with Builder

var branch_builder = linkit.BranchCreateBuilder{};
const branch = try branch_builder
    .withIvId("BR-RYD-001")
    .withNameEn("Riyadh Central")
    .withNameAr("الرياض سنتر")
    .withLocation(24.7136, 46.6753)
    .withActive(true)
    .withEmail("riyadh@example.com")
    .build();

var result = try client.branches().createRequest(branch);
defer result.deinit(allocator);

Paginate All Branches

var it = client.branches().listAll(.{ .limit = 100 });
while (try it.next()) |*page| {
    defer page.deinit(allocator);
    std.debug.print("Branches: {s}\n", .{page.body});
}

Customers

// Create
var customer = try client.customers().create(
    \\{"firstName":"Ahmad","lastName":"Hamdi","email":"ahmad@example.com","phone":"+966500000000"}
);
defer customer.deinit(allocator);

// Get addresses
var addresses = try client.customers().getAddresses("C-001");
defer addresses.deinit(allocator);

// Create address
var address = try client.customers().createAddress("C-001",
    \\{"label":"Home","address1":"123 Main St","city":"Riyadh","country":"SA"}
);
defer address.deinit(allocator);

// Create group
var group = try client.customers().createGroup(
    \\{"name":"VIP Customers"}
);
defer group.deinit(allocator);

Orders & Offers

// Update order status
var order = try client.orders().updateStatus("ORD-001",
    \\{"status":"shipped"}
);
defer order.deinit(allocator);

// Update offer status
var offer = try client.offers().updateStatus("OFR-001",
    \\{"status":"paused"}
);
defer offer.deinit(allocator);

// Bulk upsert offers
var bulk = try client.offers().bulkUpsert(
    \\[{"name":"Weekend 15%","offerType":"direct_discount","discountValue":15},
    \\{"name":"BOGO Deal","offerType":"bogo","buyQty":1,"getQty":1}]
);
defer bulk.deinit(allocator);

Integrations

// Get integration config
var config = try client.integrations().getConfig("hungerstation-orders-v2");
defer config.deinit(allocator);

// Execute integration
var result = try client.integrations().execute("hungerstation-orders-v2");
defer result.deinit(allocator);

// Enable / disable
var enable = try client.integrations().enable("hungerstation-orders-v2");
defer enable.deinit(allocator);

var disable = try client.integrations().disable("hungerstation-orders-v2");
defer disable.deinit(allocator);

Fluent Builders

The SDK provides four fluent builders with compile-time struct defaults and runtime validation:

BuilderRequired FieldsOptional Fields
SkuCreateBuilderivId, branchIvId, productIvIdcategoryId, barcode, gtin, price, qty, maxQty, buffer, reorderThreshold, available
SkuStockUpdateBuilderivId, branchIvIdqty, available
ProductCreateBuilderivIdnameEn, nameAr, descEn, descAr, brandId, categoryId, averagePrice, isEnabled, buffer
BranchCreateBuilderivId, nameEn, nameArlocation, active, email

Validation Errors

Calling .build() on an invalid builder returns typed errors:

var bad_sku = linkit.SkuCreateBuilder{};
_ = bad_sku.withIvId(""); // empty ID
const result = bad_sku.build();
// result == error.InvalidIvId

var bad_price = linkit.SkuCreateBuilder{};
_ = bad_price.withIvId("SKU-1").inBranch("BR-1").forProduct("P-1").withPrice(-5.0);
const result2 = bad_price.build();
// result2 == error.InvalidPrice

Error Handling

All service methods return !Response. Check response.ok() for success:

var response = try client.products().getByIvId("NONEXISTENT");
defer response.deinit(allocator);

if (!response.ok()) {
    std.debug.print("Error {d}: {s}\n", .{ response.status, response.body });
    // Parse status for specific handling:
    // 401 → authentication failed
    // 404 → not found
    // 429 → rate limited
    // 5xx → server error (will be retried automatically)
}

Retry Behavior

The client automatically retries on transient errors (429, 503, 504) with exponential backoff:

Attempt 0: immediate
Attempt 1: base_delay × 2^0 = 150ms
Attempt 2: base_delay × 2^1 = 300ms
Attempt 3: base_delay × 2^2 = 600ms

Non-transient errors (4xx except 429) propagate immediately.


Transport Interface

The SDK ships with zero HTTP dependencies — implement the Transport struct to inject any HTTP backend:

pub const SendFn = *const fn (
    ctx: *anyopaque,
    req: Request,
    allocator: std.mem.Allocator,
) anyerror!Response;

pub const Transport = struct {
    ctx: *anyopaque,
    send_fn: SendFn,

    pub fn send(self: Transport, req: Request, allocator: std.mem.Allocator) !Response {
        return self.send_fn(self.ctx, req, allocator);
    }
};

Request/Response Types

pub const HttpMethod = enum { GET, POST, PUT, PATCH, DELETE };

pub const Request = struct {
    method: HttpMethod,
    url: []const u8,           // Full URL with query params
    body: ?[]const u8 = null,  // JSON body (null for GET/DELETE)
    bearer_token: ?[]const u8 = null, // null for health routes
};

pub const Response = struct {
    status: u16,
    body: []u8,  // Caller-owned, must be freed

    pub fn ok(self: Response) bool {
        return self.status >= 200 and self.status < 300;
    }

    pub fn deinit(self: *Response, allocator: std.mem.Allocator) void {
        allocator.free(self.body);
    }
};

Memory Management

Allocator Discipline

Every Response returned by service methods owns its .body slice. You must call response.deinit(allocator) to free it. Use defer immediately after the call.

// ✅ Correct — defer immediately
var response = try client.products().getByIvId("P-1");
defer response.deinit(allocator);

// ❌ Leaks memory — no deinit
var response = try client.products().getByIvId("P-1");
// forgot deinit!

Core Features

  • Full 11-service parity with Linkit API domains
  • Canonical route mapping (/api/v1/products/iv/{id}, /api/v1/skus/iv/{id}/stock, etc.)
  • Health routes skip auth header attachment automatically
  • Iterator-based pagination via listAll(...) on Products, SKUs, Branches, Customers
  • Fluent builders with compile-time defaults and runtime validation
  • Exponential backoff retry for transient HTTP errors
  • Zig 0.16.0+ idiomatic std.ArrayList (unmanaged-by-default) and std.Io for realtime delays

Download

Archive builds are staged into:

/downloads/sdks/zig/