go get github.com/Pariette-Inc/signalbird.sdk/v2signalbird - Go
It carries three surfaces: Radio (logs), Messaging (email/SMS/push) and Management (projects, chat inbox, apps).
Install
go get github.com/Pariette-Inc/signalbird.sdk/v2import "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
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:
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
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
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
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)
}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
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
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
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
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
type Result struct {
OK bool
Status int
Data json.RawMessage
Code string
Message string
}Use Into to decode the body:
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.
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):
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:
go get github.com/Pariette-Inc/signalbird.sdk/v2@latestError 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 == nilwhileresult.OK == falseis possible. Check both. - There is no retry. The decision to try again belongs to the caller.
contextis the first parameter of every method; cancelling the request is up to you.- In the query string
nilfields are skipped, slices go out askey[]=, and booleans are turned into the texttrue/false.