Docs / Contact / sdks/go/api-reference

Go SDK: API Reference

Complete method reference for the tendrl.Client type.

Constructors#

NewClient(managed bool, apiKey ...string) (*Client, error)#

Create a new client. Pass true for managed mode (recommended) or false for headless.

NewClientWithMode(managed bool) (*Client, error)#

Create a client using the TENDRL_KEY environment variable.

NewClientWithConfig(configPath string) (*Client, error)#

Create a client from a JSON config file.

NewClientWithConfigAndAPIKey(configPath, apiKey string) (*Client, error)#

Create a client from a config file with an explicit API key override.

All four constructors validate the API key before returning in managed mode, by calling /claims. An invalid or unreachable key is a constructor error, not a surprise later.

Lifecycle#

client.Stop()#

Gracefully stop the client. Flushes the message queue, stops background goroutines, and updates entity status to offline.

Publishing#

client.Publish(data interface{}, tags []string, entity string, waitResponse bool, timeout int) (string, error)#

Publish a message. When waitResponse is true, blocks until the server responds or the timeout (seconds) expires. Returns the message ID.

client.PublishAsync(data interface{}, tags []string) error#

Queue a message for batched delivery. Non-blocking. Returns an error if the queue is full.

client.PublishCrossAccount(data interface{}, destination string, tags []string) error#

Send a message to an entity in another account. The destination format is account:region:entity:name.

client.Tether(name string, fn func() (interface{}, error), tags []string, interval time.Duration) func()#

Start a periodic data collection function. Calls fn every interval and publishes the result. Returns a stop function.

Message Receiving#

Poll for incoming messages in managed mode. Route by tag or msg_type with client.On():

go

client.On(tendrl.MessageRoute{
    Tag: "ai-response",
    Handler: func(msg tendrl.IncomingMessage) error {
        fmt.Println(msg.Data)
        return nil
    },
})

client.On(route MessageRoute)#

Register a route handler. All non-empty fields in the route must match (AND semantics). Routes are checked in registration order; first match wins.

Field Type Description
MsgType string Match this message type
Tag string Match a single tag
Tags []string Match if message has any listed tag
TagsAll []string Match if message has all listed tags
Handler MessageCallback Handler function

client.OnDefault(fn func(IncomingMessage) error)#

Catch-all handler when no route matches.

client.SetMessageCallback(fn func(IncomingMessage) error)#

Catch-all fallback when no route or OnDefault handler matches. The callback receives:

go

type IncomingMessage struct {
    MsgType   string                 `json:"msg_type"`
    Source    string                 `json:"source"`
    Dest      string                 `json:"dest,omitempty"`
    Timestamp string                 `json:"timestamp"`
    Data      interface{}            `json:"data"`
    Context   IncomingMessageContext `json:"context,omitempty"`
    RequestID string                 `json:"request_id,omitempty"`
}

type IncomingMessageContext struct {
    Tags           []string               `json:"tags,omitempty"`
    DynamicActions map[string]interface{} `json:"dynamicActions,omitempty"`
}

Tags are on msg.Context.Tags, not on the message itself — there is no msg.Tags field. client.On route matching reads Context.Tags for you.

client.SetMessageCheckRate(d time.Duration)#

Set polling interval for incoming messages.

client.SetMessageCheckLimit(n int)#

Set maximum messages retrieved per poll.

client.CheckMessages() error#

Poll once for incoming messages and dispatch them to your registered handlers. It returns only an error — messages reach you through On, OnDefault or SetMessageCallback, never as a return value. A managed client already polls on its own every SetMessageCheckRate interval; call this to poll on your own schedule.

State Receiving#

Poll the state table at the same interval as messages. Handlers fire when the table changes:

go

client.OnState(func(state map[string]interface{}) error {
    if status, _ := state["status"].(string); status == "needs_maintenance" {
        log.Println("Maintenance required")
    }
    return nil
})

client.OnState(fn func(map[string]interface{}) error)#

Register a handler for remote state table changes detected by polling.

client.SetStateCallback(fn)#

Catch-all fallback when OnState is not used.

client.CheckState() error#

Manually poll the state table and dispatch handlers if it changed.

File Transfer#

Send and receive files between entities. Files are malware-scanned by Surface before delivery and deleted once downloaded. See File Transfer.

client.SendFile(path, dest string, tags []string, meta ...map[string]any) (*FileResult, error)#

Upload a file from disk, routed by dest (entity, fanout, or cross-account resource path) or tags (Strand automation). SendFileBytes(name, data, dest, tags) uploads raw bytes. A non-2xx response (402 credits, 403 not accepted, 415 type, 422 blocked) is returned as an error.

go

res, _ := client.SendFile("reading.csv", "gateway-01", nil)

client.CheckFiles(limit int) ([]map[string]any, error)#

List clean files addressed to this entity.

client.DownloadFile(transferID string) ([]byte, error)#

Download a clean file's bytes (consumes delete-on-download files).

client.RescanFile(transferID string) (*RescanResult, error)#

Re-scan a received cross-account file with this account's own Surface profile (billed to the recipient). Only the recipient may call it; a blocked result means the stricter profile flagged it.

State Table#

The Go SDK reads the state table but does not wrap writes. Use client.OnState or client.CheckState to read it, and call the REST endpoints directly to write:

Method Path Purpose
GET /api/entities/status-table Read current state
PATCH /api/entities/status-table Merge into existing state
PUT /api/entities/status-table Replace state entirely

Authenticate with the entity's API key. See REST protocol → State Table for full examples.

Heartbeat#

client.PublishHeartbeat(data HeartbeatData) error#

Send a manual heartbeat with system metrics.

go

type HeartbeatData struct {
    MemFree  uint64
    MemTotal uint64
    DiskFree uint64
    DiskSize uint64
}

client.GetSystemMetrics() SystemMetrics#

Get current system resource metrics, returned by value. In headless mode it returns a zero SystemMetrics, since metrics are only collected by the managed background loop.

client.PublishSensorData(sensorData interface{}, tags []string) error#

Convenience wrapper around PublishAsync for sensor readings.

client.GetOfflineStorageStats() OfflineStorageStats#

Return counts and byte totals for messages stored offline while disconnected.

Connectivity#

client.IsOnline() bool#

Check if the client has network connectivity.

client.GetConnectivityState() ConnectivityState#

Get detailed connectivity information including last check time and last online/offline transitions.

Configuration#

tendrl.GenerateExampleConfig() *ConfigFile#

Build a ConfigFile populated with every option at its documented default. It takes no arguments and writes nothing — pair it with SaveConfigFile to put it on disk:

go

if err := tendrl.SaveConfigFile(tendrl.GenerateExampleConfig(), tendrl.GetDefaultConfigPath()); err != nil {
    log.Fatal(err)
}

tendrl.LoadConfigFile() (*ConfigFile, error)#

Load configuration from disk. It takes no path: it searches ~/.tendrl/config.json first, then /etc/tendrl/config.json on non-Windows systems, and returns the first one it can read. If neither exists it returns an empty ConfigFile and a nil error — which is why offline storage, retry, connectivity monitoring and heartbeats are off by default. See Configuration.

To load from a specific path instead, construct the client with NewClientWithConfig(path).

tendrl.SaveConfigFile(config *ConfigFile, path string) error#

Write a ConfigFile to path, creating the directory if needed.

tendrl.GetDefaultConfigPath() string#

Return ~/.tendrl/config.json, or ./tendrl.json if the home directory cannot be determined.