Skip to content

Go SDK

Zero-dependency Go SDK for the Synapse API. Published as github.com/pyrx-tech/pyrx-synapse-go. Uses only Go stdlib (net/http, crypto/hmac, encoding/json).

Requires Go 1.21+.


Installation

bash
go get github.com/pyrx-tech/pyrx-synapse-go

Quick Start

go
package main
 
import (
"fmt"
"log"
 
synapse "github.com/pyrx-tech/pyrx-synapse-go"
)
 
func main() {
client, err := synapse.NewClient(synapse.Config{
APIKey: "psk_live_your_api_key",
WorkspaceID: "your_workspace_id",
})
if err != nil {
log.Fatal(err)
}
 
// Track an event
result, err := client.Track(synapse.TrackParams{
ExternalID: "user_123",
EventName: "purchase_completed",
Attributes: map[string]any{
"order_id": "ord_456",
"amount": 99.99,
"currency": "USD",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.EventID) // "evt_8f14e45f-..."
 
// Identify a contact
contact, err := client.Identify(synapse.IdentifyParams{
ExternalID: "user_123",
Email: "[email protected]",
FirstName: "Jane",
LastName: "Doe",
Properties: map[string]any{"plan": "pro", "signup_source": "website"},
Tags: []string{"paying", "beta-tester"},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(contact.Email) // "[email protected]"
 
// Send a transactional email
sendResult, err := client.Send(synapse.SendParams{
TemplateSlug: "order-confirmation",
To: map[string]any{
"user_id": "user_123",
"email": "[email protected]",
"first_name": "Jane",
},
Attributes: map[string]any{
"order_id": "ord_456",
"items": []map[string]any{{"name": "Widget", "price": 99.99}},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(sendResult.Status) // "sent"
}
Tip

Get your API key and workspace ID from the dashboard at Settings > API Keys.


Configuration

go
client, err := synapse.NewClient(synapse.Config{
APIKey: "psk_live_xxx", // Required. API key from workspace settings.
WorkspaceID: "ws_xxx", // Required. Your workspace ID.
BaseURL: "https://...", // Default: https://synapse-api.pyrx.tech
Timeout: 30 * time.Second, // Default: 30s
MaxRetries: 3, // Default: 3. Set to 0 to disable retries.
})
if err != nil {
log.Fatal(err)
}
ParameterTypeDefaultDescription
APIKeystringrequiredYour Synapse API key (psk_live_* or psk_test_*)
WorkspaceIDstringrequiredYour workspace identifier
BaseURLstringhttps://synapse-api.pyrx.techAPI base URL
Timeouttime.Duration30sRequest timeout
MaxRetriesint3Retry count for 429/5xx errors. Set to 0 to disable.

NewClient returns (*Client, error) -- it validates config and returns an error if APIKey or WorkspaceID are empty. All API methods return (result, error) -- idiomatic Go error handling.

Retry behavior: The SDK automatically retries on 429, 500, 502, 503, and 504 with exponential backoff and jitter (capped at 30s). On 429, uses the Retry-After header when present. Network errors are also retried. Client errors (400, 401, 403, 404, 422) are never retried.


Track Events

Single Event

go
result, err := client.Track(synapse.TrackParams{
ExternalID: "user_123",
EventName: "purchase_completed",
Attributes: map[string]any{
"order_id": "ord_456",
"amount": 99.99,
"currency": "USD",
},
Contact: map[string]any{
"email": "[email protected]",
"first_name": "Jane",
},
IdempotencyKey: "purchase_ord_456", // optional, prevents duplicate processing
OccurredAt: "2026-04-29T10:30:00Z", // optional, defaults to now
})
if err != nil {
log.Fatal(err)
}
 
fmt.Println(result.EventID) // "evt_8f14e45f-..."
fmt.Println(result.Status) // "accepted"
ParameterTypeRequiredDescription
ExternalIDstringYesYour unique user identifier
EventNamestringYesEvent name (e.g., purchase_completed)
Attributesmap[string]anyNoArbitrary key-value event data
Contactmap[string]anyNoContact fields to upsert alongside the event
IdempotencyKeystringNoPrevents duplicate processing (7-day TTL)
OccurredAtstringNoISO 8601 timestamp. Defaults to server time.

Batch Events

Track up to 50 events in a single request.

go
result, err := client.TrackBatch(synapse.TrackBatchParams{
Events: []synapse.TrackParams{
{ExternalID: "user_1", EventName: "page_view", Attributes: map[string]any{"page": "/pricing"}},
{ExternalID: "user_2", EventName: "page_view", Attributes: map[string]any{"page": "/docs"}},
{ExternalID: "user_1", EventName: "button_clicked", Attributes: map[string]any{"button": "upgrade"}},
},
})
if err != nil {
log.Fatal(err)
}
 
fmt.Println(result.Accepted) // 3
fmt.Println(result.Rejected) // 0

Identify Contacts

Single Contact

Create or update (upsert) a contact by ExternalID.

go
contact, err := client.Identify(synapse.IdentifyParams{
ExternalID: "user_123",
Email: "[email protected]",
FirstName: "Jane",
LastName: "Doe",
Phone: "+1234567890",
Timezone: "America/New_York",
Locale: "en-US",
Properties: map[string]any{"plan": "pro", "signup_source": "website"},
Tags: []string{"paying", "beta-tester"},
})
if err != nil {
log.Fatal(err)
}
 
fmt.Println(contact.ID) // UUID
fmt.Println(contact.ExternalID) // "user_123"
fmt.Println(contact.Email) // "[email protected]"

Batch Identify

Upsert up to 1,000 contacts in a single request.

go
result, err := client.IdentifyBatch(synapse.IdentifyBatchParams{
Contacts: []synapse.IdentifyParams{
{ExternalID: "user_1", Email: "[email protected]", FirstName: "Alice"},
{ExternalID: "user_2", Email: "[email protected]", FirstName: "Bob"},
},
OnConflict: "merge", // "merge" | "skip" | "replace"
})
if err != nil {
log.Fatal(err)
}
 
fmt.Println(result.Total) // 2
fmt.Println(result.Created) // 1
fmt.Println(result.Updated) // 1

Send Transactional Email

Send a one-off email using an NLT template, without a flow.

go
result, err := client.Send(synapse.SendParams{
TemplateSlug: "otp-verification",
To: map[string]any{
"user_id": "user_123",
"email": "[email protected]",
"first_name": "Jane",
},
Attributes: map[string]any{
"otp_code": "847293",
"expiry_minutes": 10,
},
IdempotencyKey: fmt.Sprintf("otp_user_123_%d", time.Now().Unix()),
})
if err != nil {
log.Fatal(err)
}
 
fmt.Println(result.Status) // "sent" or "suppressed"
fmt.Println(result.EmailLogID) // "el_8f14e45f-..."
Note

Requires a data-scoped API key (or higher). The template must exist in your workspace.


Contact Management

The client.Contacts sub-client provides full CRUD operations. Requires a management or full scoped API key.

List Contacts

go
result, err := client.Contacts.List(synapse.ListContactsParams{
Search: "jane",
Page: 1,
PerPage: 25,
SortBy: "created_at",
SortOrder: "desc",
})
if err != nil {
log.Fatal(err)
}
 
fmt.Println(result.Meta.Total) // 142
fmt.Println(result.Meta.TotalPages) // 6
 
for _, c := range result.Data {
fmt.Println(c.Email, c.FirstName)
}

Get a Contact

go
contact, err := client.Contacts.Get("contact_uuid")

Update a Contact

go
updated, err := client.Contacts.Update("user_123", synapse.ContactUpdateParams{
Email: "[email protected]",
AddTags: []string{"vip"},
RemoveTags: []string{"trial"},
})

Delete a Contact

go
err := client.Contacts.Delete("user_123")

Template Management

The client.Templates sub-client manages email templates. Requires a management or full scoped API key.

List Templates

go
templates, err := client.Templates.List()

Get a Template

go
template, err := client.Templates.Get("welcome-email")

Create a Template

go
template, err := client.Templates.Create(synapse.TemplateCreateParams{
Name: "Welcome Email",
Slug: "welcome-email",
Subject: "Welcome, [first name of contact]!",
BodyHTML: "<h1>Welcome!</h1><p>Thanks for joining.</p>",
SenderName: "PYRX Team",
FromEmail: "[email protected]",
})

Update a Template

go
template, err := client.Templates.Update("welcome-email", synapse.TemplateUpdateParams{
Subject: "Welcome aboard, [first name of contact]!",
})

Preview with Sample Data

go
preview, err := client.Templates.Preview("welcome-email", synapse.TemplatePreviewParams{
Contact: map[string]any{"first_name": "Jane", "email": "[email protected]"},
TriggerEvent: map[string]any{"order_id": "ord_123"},
})
if err != nil {
log.Fatal(err)
}
 
fmt.Println(preview.Subject) // Rendered subject
fmt.Println(preview.HTML) // Rendered HTML
fmt.Println(preview.Suppressed) // false
fmt.Println(preview.SuppressedReason) // ""

Delete a Template

go
err := client.Templates.Delete("old-template")

Webhook Verification

Verify incoming webhook signatures to ensure requests are authentically from Synapse. This is a package-level function -- no client instance needed.

go
package main
 
import (
"fmt"
"io"
"net/http"
"os"
 
synapse "github.com/pyrx-tech/pyrx-synapse-go"
)
 
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
headers := map[string]string{
"svix-id": r.Header.Get("svix-id"),
"svix-timestamp": r.Header.Get("svix-timestamp"),
"svix-signature": r.Header.Get("svix-signature"),
}
secret := os.Getenv("SYNAPSE_WEBHOOK_SECRET") // e.g. "whsec_..."
 
event, err := synapse.VerifyWebhook(payload, headers, secret, false)
if err != nil {
// Invalid signature, expired timestamp, or missing headers
http.Error(w, "Webhook rejected: "+err.Error(), http.StatusBadRequest)
return
}
 
fmt.Println(event["type"]) // e.g. "email.delivered"
w.WriteHeader(http.StatusOK)
}

The verification checks:

  • All three svix-* headers are present
  • The timestamp is within 5 minutes (replay attack protection)
  • The HMAC-SHA256 signature matches (supports multiple signatures for key rotation)

The fourth parameter (false) controls strict mode. Set to true to reject timestamps outside the tolerance window.


Error Handling

The SDK provides typed error structs for every failure mode. Use errors.As to check error types.

go
import (
"errors"
"fmt"
 
synapse "github.com/pyrx-tech/pyrx-synapse-go"
)
 
_, err := client.Track(synapse.TrackParams{
ExternalID: "u1",
EventName: "test",
})
if err != nil {
var planErr *synapse.SynapsePlanLimitError
var rateErr *synapse.SynapseRateLimitError
var valErr *synapse.SynapseValidationError
var authErr *synapse.SynapseAuthError
var apiErr *synapse.SynapseError
 
switch {
case errors.As(err, &planErr):
fmt.Printf("Plan limit: %s (%d/%d)\n", planErr.LimitType, planErr.Current, planErr.Maximum)
fmt.Printf("Current plan: %s\n", planErr.Plan)
case errors.As(err, &rateErr):
fmt.Printf("Rate limited. Retry after %ds\n", rateErr.RetryAfter)
case errors.As(err, &valErr):
for _, e := range valErr.Errors {
fmt.Printf("%s: %s\n", e.Field, e.Message)
}
case errors.As(err, &authErr):
fmt.Printf("Authentication failed: %s\n", authErr.Message)
case errors.As(err, &apiErr):
fmt.Printf("API error %d: %s\n", apiErr.Status, apiErr.Message)
default:
fmt.Printf("Unexpected error: %v\n", err)
}
}

Error Types

Error TypeHTTP StatusFieldsWhen
*synapse.SynapseErrorAnyStatus, Message, Code, RequestIDBase type for all API errors
*synapse.SynapseAuthError401, 403MessageInvalid or expired API key, scope mismatch
*synapse.SynapseValidationError422Errors[] with Field + MessageRequest body validation failed
*synapse.SynapseRateLimitError429RetryAfter (seconds)Rate limit exceeded
*synapse.SynapsePlanLimitError403LimitType, Current, Maximum, PlanPlan limit reached

Environment Variables

For production deployments, load credentials from environment variables.

go
client, err := synapse.NewClient(synapse.Config{
APIKey: os.Getenv("SYNAPSE_API_KEY"),
WorkspaceID: os.Getenv("SYNAPSE_WORKSPACE_ID"),
})
if err != nil {
log.Fatal(err)
}
bash
export SYNAPSE_API_KEY=psk_live_a1b2c3d4e5f67890abcdef1234567890
export SYNAPSE_WORKSPACE_ID=your_workspace_id

Full Method Reference

MethodDescriptionRequired Scope
client.Track(params)Track a single eventdata
client.TrackBatch(params)Track up to 50 eventsdata
client.Identify(params)Upsert a single contactdata
client.IdentifyBatch(params)Upsert up to 1,000 contactsdata
client.Send(params)Send a transactional emaildata
client.Contacts.List(params)List contacts with paginationmanagement
client.Contacts.Get(id)Get a single contactmanagement
client.Contacts.Update(id, data)Update a contactmanagement
client.Contacts.Delete(id)Delete a contactmanagement
client.Templates.List()List all templatesmanagement
client.Templates.Get(slug)Get a template by slugmanagement
client.Templates.Create(params)Create a templatemanagement
client.Templates.Update(slug, params)Update a templatemanagement
client.Templates.Preview(slug, data)Preview rendered templatemanagement
client.Templates.Delete(slug)Delete a templatemanagement
synapse.VerifyWebhook(payload, headers, secret, strict)Verify webhook signature--

Framework Examples

net/http

go
package main
 
import (
"encoding/json"
"net/http"
"os"
 
synapse "github.com/pyrx-tech/pyrx-synapse-go"
)
 
var client, _ = synapse.NewClient(synapse.Config{
APIKey: os.Getenv("SYNAPSE_API_KEY"),
WorkspaceID: os.Getenv("SYNAPSE_WORKSPACE_ID"),
})
 
func signupHandler(w http.ResponseWriter, r *http.Request) {
var body struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Plan string `json:"plan"`
}
json.NewDecoder(r.Body).Decode(&body)
 
// Identify the new user
client.Identify(synapse.IdentifyParams{
ExternalID: body.UserID,
Email: body.Email,
Properties: map[string]any{"plan": body.Plan},
Tags: []string{"new-signup"},
})
 
// Track the signup event (triggers flows)
client.Track(synapse.TrackParams{
ExternalID: body.UserID,
EventName: "user_signed_up",
Attributes: map[string]any{"plan": body.Plan, "source": "web"},
})
 
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
 
func main() {
http.HandleFunc("/signup", signupHandler)
http.ListenAndServe(":8080", nil)
}