pkg.go.dev ↗
Installgo get github.com/Pariette-Inc/signalbird.sdk/v2

signalbird - Go

It carries three surfaces: Radio (logs), Messaging (email/SMS/push) and Management (projects, chat inbox, apps).

Install

bash
go get github.com/Pariette-Inc/signalbird.sdk/v2
go
import "github.com/Pariette-Inc/signalbird.sdk/v2/src/go/signalbird"

Go 1.21+ is required. It has no dependencies: the standard library only.

Keys

Surface Type Key
Radio signalbird.Client sb_secret_live_…
Messaging signalbird.Messaging sb_…
Management signalbird.Management sb_… + scopes

Radio - logs and events

go
radio, err := signalbird.NewClientFromEnv()   // SIGNALBIRD_DOMAIN_KEY / _URL / _SOURCE
if err != nil {
    log.Fatal(err)
}

radio.Critical(ctx, "critical", "the payment service is not responding", map[string]any{
    "service": "iyzico",
    "attempt": 3,
})

radio.Info(ctx, "info", "ahmet@x.com created a new account", nil)

Your own configuration:

go
radio, err := signalbird.NewClient(signalbird.Config{
    DomainKey:       os.Getenv("SIGNALBIRD_DOMAIN_KEY"),
    Source:       "api-01",
    Timeout:      5 * time.Second,   // the default
    ThrowOnError: false,             // default: silent failure
})

Five levels: LevelDebug, LevelInfo, LevelWarn, LevelError, LevelCritical. If you do not pass a level, the channel's own default applies.

Batch sending

go
result, err := radio.Batch(ctx, []signalbird.Event{
    {Channel: "deploy", Message: "v2.4.0 is live", Level: signalbird.LevelInfo},
    {Channel: "critical", Message: "the queue filled up"},
})

At most 100 records; partial success is normal and failed rows are not retried.

HTTP middleware pattern

go
func SignalbirdRecovery(radio *signalbird.Client) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if recovered := recover(); recovered != nil {
                    // Shipping a log must not hold the request: the context is copied
                    radio.Critical(context.WithoutCancel(r.Context()), "critical",
                        fmt.Sprint(recovered), map[string]any{
                            "path":   r.URL.Path,
                            "method": r.Method,
                        })

                    http.Error(w, "internal error", http.StatusInternalServerError)
                }
            }()

            next.ServeHTTP(w, r)
        })
    }
}

Messaging - email, SMS, push

go
send, err := signalbird.NewMessaging(signalbird.KeyConfig{
    DomainKey: os.Getenv("SIGNALBIRD_DOMAIN_KEY"),
})

result, err := send.SendEmail(ctx, map[string]any{
    "to":      "ahmet@example.com",
    "class":   "transactional",     // REQUIRED, there is no default
    "subject": "Your order has shipped",
    "body":    "<p>Tracking number: 1234</p>",
})

if err != nil || !result.OK {
    log.Printf("signalbird: %s - %s", result.Code, result.Message)
}
go
send.SendSms(ctx, map[string]any{"to": "+905551112233", "class": "transactional", "body": "Your code: 123456"})
send.PreviewSms(ctx, "Hello, your order is ready.")   // segment maths, spends no quota
send.SendPush(ctx, map[string]any{"to": "contact:42", "class": "transactional", "subject": "Order", "body": "It has shipped"})

Contacts

go
send.ListContacts(ctx, signalbird.Query{"q": "ahmet", "page": 1})
send.CreateContact(ctx, map[string]any{"email": "ahmet@example.com"})
send.UpdateContact(ctx, contactID, map[string]any{"phone": "+905551112233"})
send.DeleteContact(ctx, contactID)

// chunks of 1000, SEQUENTIALLY
send.BulkContacts(ctx, map[string]any{
    "contacts": rows,          // []any or []map[string]any
    "list_id":  42,
})

Campaigns

go
send.CreateCampaign(ctx, map[string]any{
    "name":    "August newsletter",
    "channel": "email",
    "list_id": 42,
    "subject": "What happened this month?",
    "body":    "<h1>Hello</h1>",
})

// Walks the pages; stops when fn returns false
send.IterateCampaignMessages(ctx, batchID, nil, func(row map[string]any) bool {
    reconcile(row["external_ref"], row["status"])
    return true
})

Webhook verification

go
func handleWebhook(w http.ResponseWriter, r *http.Request) {
    raw, _ := io.ReadAll(r.Body)

    // On the RAW body: json.Unmarshal + Marshal breaks the signature
    if !signalbird.VerifyWebhook(raw, r.Header.Get("X-Signalbird-Signature"), secret) {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }

    var event struct {
        ID   string `json:"id"`
        Type string `json:"type"`
    }
    json.Unmarshal(raw, &event)

    // Deduplicate redeliveries by `id`
    if seen(event.ID) {
        w.WriteHeader(http.StatusOK)
        return
    }
}

Management - set up your own project

go
admin, err := signalbird.NewManagement(signalbird.KeyConfig{
    DomainKey: os.Getenv("SIGNALBIRD_DOMAIN_KEY"),
})

created, _ := admin.CreateModuleKey(ctx, "logger", map[string]any{"title": "Payment errors"})

var payload struct {
    Project struct{ ID int } `json:"project"`
    Secret  string           `json:"secret"`   // returned HERE ONLY
}
created.Into(&payload)

admin.CreateRadioChannel(ctx, payload.Project.ID, map[string]any{
    "key":         "payment",
    "name":        "Payment",
    "level":       "critical",
    "notify_push": true,
})

// The chat inbox
inbox, _ := admin.ListConversations(ctx, signalbird.Query{"status": "open"})

admin.Reply(ctx, conversationID, map[string]any{"body": "Hello! We will be right with you."})
admin.SetConversationStatus(ctx, conversationID, "resolved")

Your key must carry the radio:*, chat:* and apps:* scopes. Details: the Management document.


Result shape

go
type Result struct {
    OK      bool
    Status  int
    Data    json.RawMessage
    Code    string
    Message string
}

Use Into to decode the body:

go
var project struct {
    Project struct {
        ID   int    `json:"id"`
        Name string `json:"name"`
    } `json:"project"`
}

result.Into(&project)

With ThrowOnError: true the methods return a *signalbird.Error as the second value (carrying Code, Status, Message, Body).

Visitor identity verification - identityHash

The chat widget and the app SDK introduce a signed-in user by external_id. Because the public key sits in the page, Signalbird trusts that identity only when it comes with a valid hash generated on your server (2.7.0). Without it the visitor is not linked to the contact record, the device is not bound to that user as a push target, and the channel agent's tools see the identity only under visitor.unverified.

go
client, _ := signalbird.NewClientFromEnv()
hash := client.IdentityHash(user.ID)

It lives on the Radio client, which already holds the secret key; no extra setup. The algorithm (same output in every language):

text
identity_hash = hex( HMAC-SHA256( hex(SHA-256(sb_secret_live_…)), external_id ) )

The key is the SHA-256 digest of the secret key (the server never stores the key in plain text). Usage: Widget - identity verification, App SDK.

Version notice

The SDK reports its version on every request: X-Signalbird-Sdk: go/<version>. When a newer version is released the API responds with Signalbird-Sdk-Status: outdated (or unsupported once it is no longer supported) and the SDK writes a warning once per process (log.Printf). The warning is not an error: the request result does not change.

In the panel, Domains → [domain] → Keys shows which SDK version each key is used with; keys on an old version also get a banner at the top of the panel and a notification. To update:

bash
go get github.com/Pariette-Inc/signalbird.sdk/v2@latest

Error codes

Code Meaning
ErrNoKey / ErrWrongKeyType At construction time (a NewClient error)
INVALID_KEY The key is missing, wrong, or the project is inactive
API_KEY_SCOPE The key lacks the required scope
MODULE_DISABLED Your package does not include this module
LIMIT_REACHED Your monthly limit is full
VALIDATION_ERROR 422
NETWORK_ERROR / TIMEOUT Network (status 0)

Rules of behaviour

  • Silent failure is the default: err == nil while result.OK == false is possible. Check both.
  • There is no retry. The decision to try again belongs to the caller.
  • context is the first parameter of every method; cancelling the request is up to you.
  • In the query string nil fields are skipped, slices go out as key[]=, and booleans are turned into the text true/false.