Docs / Contact / sdks/go/getting-started

Go SDK: Getting Started

Go from zero to publishing device data in under a minute. The SDK handles message batching, tag-based inbound routing, and — once you add a config file — BoltDB offline storage, automatic heartbeats, and connectivity monitoring.

Requirements: Go 1.25+ (the module declares go 1.25.0)

Install#

bash

go get github.com/tendrl-inc-labs/contact-go@latest
Need an API key?

Every client needs an entity API key. If you haven't created one yet, create your first entity and copy its API key from the Connection Instructions dialog.

Your First Message#

go


client, err := tendrl.NewClient(true, "your_api_key")
if err != nil {
    panic(err)
}
defer client.Stop()

client.PublishAsync(map[string]interface{}{"temperature": 23.5}, []string{"sensor"})

The message is queued, batched, and delivered. Tags route it to your flows and connectors.

Pointing at a different server#

By default the client talks to https://app.tendrl.com. Set TENDRL_APP_URL to point it at a staging environment or a stack on your laptop:

bash

TENDRL_APP_URL=http://localhost:8000 go run .

It accepts either a bare origin (http://localhost:8000) or a full base URL ending in /api, so the same variable works for the Python and JavaScript SDKs and the Nano Agent too. See Testing against a local or staging stack.

Automate with Tether#

Most IoT work is "collect data, publish, repeat." Tether does this in one call: give it a function and an interval, and the SDK handles the rest.

go

stop := client.Tether("sensors", func() (interface{}, error) {
    return map[string]interface{}{
        "temperature": readTemp(),
        "humidity":    readHumidity(),
        "pressure":    readPressure(),
    }, nil
}, []string{"sensor", "environment"}, 10*time.Second)
defer stop()

Every 10 seconds, your function runs and the result is published with the tags you specify. No goroutine management, no tickers, no manual publish calls.

Return an error to skip a cycle without stopping the tether:

go

stop := client.Tether("sensor", func() (interface{}, error) {
    value, err := readSensor()
    if err != nil {
        return nil, err // Skips this cycle, tries again next interval
    }
    return map[string]interface{}{"value": value}, nil
}, []string{"sensor"}, 5*time.Second)

Route Inbound Messages#

Handle messages sent back from Contact flows. Route by tag with client.On():

go

client.On(tendrl.MessageRoute{
    Tag: "diagnostic",
    Handler: func(msg tendrl.IncomingMessage) error {
        // run diagnostics and publish result
        return nil
    },
})

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

client.On(tendrl.MessageRoute{
    Tags: []string{"alert", "anomaly"},
    Handler: func(msg tendrl.IncomingMessage) error {
        fmt.Println("Alert:", msg.Data)
        return nil
    },
})

client.SetMessageCheckRate(3 * time.Second)

SetMessageCallback remains available as a catch-all fallback for unmatched messages.

Track Device State#

Every entity has a persistent state table, a key-value store accessible from the dashboard and other entities. The Go SDK reads it and does not write it: there is no UpdateState, GetState or ReplaceState.

Receive remote state changes with OnState(), which is polled at the same interval as messages:

go

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

// Or poll once, on your own schedule
if err := client.CheckState(); err != nil {
    log.Println("state poll failed:", err)
}

To write state, call the REST endpoints with the entity's API key: PATCH /api/entities/status-table to merge, PUT to replace. See State Table.

What You Get for Free#

When you create a managed client, the SDK automatically:

What needs a config file first#

Offline storage, offline retry, connectivity monitoring and automatic heartbeats are off unless a config file turns them on. Passing true to NewClient is not enough — with no config file present, all four are disabled and an undeliverable message is simply dropped.

Write ~/.tendrl/config.json (or /etc/tendrl/config.json) to enable them:

json

{
    "managed": true,
    "offline_storage": true,
    "offline_retry_enabled": true,
    "connectivity_check_enabled": true
}

"managed": true is required in the file itself: without it the whole managed feature set is zeroed even if you set the other keys. See Configuration for the full list and the exact defaults.

Complete Example#

go

package main

    "fmt"
    "os"
    "os/signal"
    "time"
    "github.com/tendrl-inc-labs/contact-go/tendrl"
)

func main() {
    client, err := tendrl.NewClient(true, "your_api_key")
    if err != nil {
        panic(err)
    }
    defer client.Stop()

    // Route inbound messages by tag
    client.On(tendrl.MessageRoute{
        Tag: "ai-response",
        Handler: func(msg tendrl.IncomingMessage) error {
            fmt.Printf("[%s] %v\n", msg.MsgType, msg.Data)
            return nil
        },
    })
    client.SetMessageCheckRate(3 * time.Second)

    // Collect and publish sensor data every 10 seconds
    stop := client.Tether("sensors", func() (interface{}, error) {
        return map[string]interface{}{
            "temperature": 23.5,
            "humidity":    60,
        }, nil
    }, []string{"sensor"}, 10*time.Second)
    defer stop()

    // Wait for interrupt
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, os.Interrupt)
    <-sig
}

Operating Modes#

Managed Mode (Default)#

go

client, err := tendrl.NewClient(true, "your_key")

Background goroutines handle batching, offline storage, heartbeats, and connectivity. Recommended for most use cases.

Headless Mode#

go

client, err := tendrl.NewClient(false, "your_key")

Direct HTTP calls only. No background processing. Best for simple tools or when you need full control.

What's Next#