Go SDK
The norq-go package provides a Go client for sending and linting notification templates.
Install
Coming soon. The Go module is not yet published. See norq.sh for updates.
The SDK runs entirely in-process — no norq binary or daemon is invoked at runtime.
Quick start
package main
import (
"context"
"fmt"
"github.com/suprsend/norq/sdks/go"
)
func main() {
client, err := norq.New(norq.WithProjectDir("./my-project"))
if err != nil {
panic(err)
}
ctx := context.Background()
// Send
result, err := client.Send(ctx, "transactional/order-shipped", norq.SendOpts{
To: &norq.Recipient{Email: "gaurav@example.com"},
Data: map[string]any{"user": map[string]any{"first_name": "Gaurav"}},
})
for _, r := range result.Results {
fmt.Printf("%s: success=%v\n", r.Channel, r.Success)
}
}Constructor options
client, _ := norq.New(
norq.WithProjectDir("./my-project"), // project root (required)
norq.WithStrict(true), // runtime template validation (default: true)
norq.WithRecipientResolver(myResolver), // user ID -> Recipient
)| Option | Type | Description |
|---|---|---|
WithProjectDir(dir) |
string |
Path to norq project root. Required — templates are read from this directory and compiled in-process. |
WithStrict(s) |
bool |
Reject sends that reference missing data or unknown pipes. Default true. |
WithRecipientResolver(r) |
RecipientResolver |
Resolves user IDs to *Recipient objects |
API
Send(ctx, id, opts)
Compile and deliver a notification.
result, err := client.Send(ctx, "transactional/welcome", norq.SendOpts{
To: &norq.Recipient{
Email: "user@example.com",
Cc: []string{"audit@example.com"}, // email-channel CC/BCC slices
},
Data: map[string]any{...},
Sample: "New user", // alternative to Data
Channels: []string{"email"}, // optional filter
DryRun: false,
IdempotencyKey: "order-shipped-ORD-123", // optional — see below
})IdempotencyKey is forwarded to providers that honour it (Resend, SuprSend) as the Idempotency-Key: header. When empty, the SDK generates a UUID v4 per send and applies it to every channel — supplying a stable, user-derived key extends dedup across retries and processes.
Recipient.Cc / Bcc apply to the email channel only; providers that support them (Resend) include them in the request body, others ignore.
SendOpts.Attachments is a slice of Attachment{Filename, Content []byte, ContentType, ContentID, Disposition}. The SDK base64-encodes Content automatically. ContentType is optional — falls back to filename-extension inference for the curated whitelist. Use Disposition: "inline" + a ContentID to embed images, then reference them as <img src="cid:logo">. Provider caps: Resend 40 MB total, SendGrid 30 MB. See the Attachments reference for the full whitelist.
Batch(ctx, id, channel)
Create a batch for sending a single channel template to many recipients. The template is compiled once, then rendered per-recipient. Requests auto-flush at the provider’s max batch size.
batch, err := client.Batch(ctx, "transactional/welcome", "email")
if err != nil {
panic(err)
}
// Add recipients (auto-flushes at provider's max batch size)
for _, user := range users {
err := batch.Add(ctx, user.Email, map[string]any{
"user": map[string]any{"first_name": user.Name},
})
if err != nil {
log.Printf("add failed: %v", err)
}
}
// Flush remaining and get all results
results := batch.Flush(ctx)Lint(ctx, id)
results, err := client.Lint(ctx, "") // all
results, err := client.Lint(ctx, "transactional/welcome") // one
for _, r := range results {
for _, d := range r.Diagnostics {
fmt.Printf("%s: %s [%s]\n", d.Severity, d.Message, d.Rule)
}
}RecipientResolver
To send by user ID instead of passing a full *Recipient, implement the RecipientResolver interface:
type MyResolver struct {
db *sql.DB
}
func (r *MyResolver) Resolve(ctx context.Context, userID string) (*norq.Recipient, error) {
user, err := r.db.GetUser(ctx, userID)
if err != nil {
return nil, err
}
return &norq.Recipient{
Email: user.Email,
Phone: user.Phone,
Slack: func() *norq.SlackRecipient {
if user.SlackID != "" {
return &norq.SlackRecipient{ChannelID: user.SlackID}
}
return nil
}(),
}, nil
}
client, _ := norq.New(
norq.WithProjectDir("./my-project"),
norq.WithRecipientResolver(&MyResolver{db: db}),
)
// Now send by user ID
client.Send(ctx, "transactional/welcome", norq.SendOpts{
To: "user-123", // resolved via RecipientResolver
Data: map[string]any{"user": map[string]any{"first_name": "Alice"}},
})Types
type Recipient struct {
Email string
Phone string
Slack *SlackRecipient
Push *PushRecipient
WhatsApp *WhatsAppRecipient
MsTeams *MsTeamsRecipient
}
type SendResult struct {
Success bool
Provider string
Channel string
MessageID string
Error string
}
type SendMultiResult struct {
Results []SendResult
Skipped []SkippedChannel
}Sending push notifications
Push delivery is per-token. A *PushRecipient carries every device the user has registered, possibly across ios, android, and web. The SDK fans the send out one HTTP call per token and emits one SendResult per token in result.Results.
Token shape
type PushRecipient struct {
Tokens []PushToken `json:"tokens,omitempty"`
}
type PushToken struct {
Token string `json:"token"` // FCM/APNs/Expo token, or Web Push endpoint URL
Platform string `json:"platform"` // "ios" | "android" | "web" — required
Provider string `json:"provider,omitempty"` // optional override (e.g. "expo")
Keys *PushVapidKeys `json:"keys,omitempty"` // required iff Platform == "web"
}
type PushVapidKeys struct {
P256dh string `json:"p256dh"`
Auth string `json:"auth"`
}APNs tokens additionally accept an environment field (sandbox | production) — pass it through the underlying JSON if you need per-token sandbox routing.
Mixed-platform recipient
result, err := client.Send(ctx, "transactional/order-shipped", norq.SendOpts{
To: &norq.Recipient{
Push: &norq.PushRecipient{
Tokens: []norq.PushToken{
{Token: "fcm-android-token", Platform: "android"},
{Token: "apns-ios-token", Platform: "ios"},
{
Token: "https://updates.push.services.mozilla.com/wpush/v2/...",
Platform: "web",
Keys: &norq.PushVapidKeys{P256dh: "BNc...", Auth: "tBHI..."},
},
{Token: "ExponentPushToken[xxx]", Platform: "ios", Provider: "expo"},
},
},
},
Data: map[string]any{"user": map[string]any{"first_name": "Gaurav"}},
Channels: []string{"push"},
})Per-token result fields
Each push SendResult carries two extra fields:
| Field | Meaning |
|---|---|
TokenIndex (with TokenIndexSet == true) |
Index into the original PushRecipient.Tokens slice — match results back to the token. |
TokenStatus |
Normalised cross-provider status: "invalid" (FCM UNREGISTERED, APNs BadDeviceToken, Web Push 404/410) or "transient" (5xx, rate limits). Empty string on success. |
Reaction guide
TokenStatus |
What it means | What to do |
|---|---|---|
"invalid" |
Token is permanently dead. | Drop from DB; don’t retry. |
"transient" |
Provider had a temporary failure. | Retry with backoff; token still good. |
"" on success |
Delivery accepted. | No action. |
for _, r := range result.Results {
if r.Channel != "push" || !r.TokenIndexSet {
continue
}
token := recipient.Push.Tokens[r.TokenIndex]
switch r.TokenStatus {
case "invalid":
db.DeletePushToken(ctx, userID, token.Token)
case "transient":
retryQueue.Enqueue(ctx, userID, token, time.Now().Add(time.Minute))
}
}See Push provider errors for the full set of per-token failure modes.
Codegen
norq codegen --lang go --out internal/norq/types.goGenerates typed Go structs for each notification’s data shape.