Skip to main content

What this lets you do

Send pageviews and revenue conversions to Traceten from your Go backend, over server-to-server HTTP that survives ad-blockers and privacy browsers. Use it where the browser snippet cannot run: server-rendered pages, background jobs, webhook handlers, and headless flows.

Before you start

  • Go 1.21 or newer.
  • Your Traceten site id, from Sites → Install in the dashboard.
  • Your ingest host URL, for example https://ingest.traceten.com.
  • A VisitorID for each event. The SDK runs on your server, so there is no Traceten cookie to read. You supply the identifier from your own request context. It is either a UUID or an h:<64-hex> identify hash. See supplying identifiers.

Install

Zero dependencies. The SDK uses the Go standard library only.

Minimal example

How it behaves

Page and Track enqueue the event and return immediately, never blocking on the network. A background goroutine batches, retries, and flushes: when the events queue reaches the flush threshold, on the flush interval, on an explicit Flush, and on Close. Neither method returns a delivery error. Delivery failures reach your WithOnError callback and are also returned by Flush. Both methods do return an error synchronously for a programmer mistake, so you catch a mis-shaped call before it enqueues:
  • Track called without a VisitorID
  • an event name that fails its pattern
  • a URL that is not a valid URL
  • a bad Currency or id charset

Configuration

host must be an absolute http(s) URL. A trailing slash is stripped. There is no default host: each deployment has its own ingest URL. host must use https unless it is a loopback address. The API key is sent as a Bearer token on every request, so a plaintext http host would transmit a live credential in cleartext; the SDK refuses one at construction. WithAPIKey is required. See The API key below.

The API key

WithAPIKey is required. Create a key in the dashboard under Settings, API keys, or use the key shown once when you created the site. The key must carry the Send events permission (ingest:write). New keys are created read-only by default, so tick that box when you create one, or the API will reject your calls. The key created automatically with a new site already has it. See permissions. Keep it on your server. It is a secret: never put it in client-side code, a mobile app, or a public repository. It is not the same value as the site id, which is public and already embedded in your pages. The key does two things:
  1. It gets you in. The SDK posts to {host}/v1/server/events, {host}/v1/server/conversions and {host}/v1/server/payments, which return 401 without a valid key. The open /v1/events and /v1/conversions endpoints stay open for the browser snippet, which cannot hold a secret.
  2. It gets you your own quota. Authenticated traffic is rate-limited on a bucket tied to the key, separate from the shared per-site bucket. the site id is public, so anyone who can read your page source can send events under it. With a key, that traffic cannot exhaust your allowance and 429 your conversion calls.
New validates the key’s shape and returns ErrMissingAPIKey or a *ValidationError before the client starts. An integration that silently drops every event is worse than one that fails on its first line. To rotate a key: create the new one, deploy it, then revoke the old one. Revocation normally takes effect at the edge within about a minute. If our database is unreachable at that moment, an edge location that was already using the key may keep honouring it for up to about fifteen minutes more, so that a database blip cannot silently drop your events.

API reference

New(siteID, host string, opts ...Option) (*Client, error)

Returns ErrMissingSiteID, ErrMissingHost, ErrInvalidHost, ErrMissingAPIKey, or a *ValidationError for a bad siteID charset or a malformed API key.

Page(ev PageEvent) error

Enqueues a pageview to POST {host}/v1/server/events (camelCase wire fields). Returns an error only for a programmer or validation mistake, never for delivery. Which user agent gets recorded. The UserAgent you pass is what Traceten stores on the event and classifies on. Omit it and the event falls back to the User-Agent header the SDK’s own HTTP client sent. For Go that is the standard library’s Go-http-client/1.1, which describes your server rather than the visitor. That substitution applies only to requests authenticated with an API key; the browser snippet cannot override its own User-Agent this way.

Track(name string, ev TrackEvent) error

Enqueues a conversion to POST {host}/v1/server/conversions (snake*case wire fields: event_name, value_cents, visitor_id). name must match ^[a-z]a-z0-9*]\*$(no hyphen).VisitorID is required. Both are validated synchronously: a bad call returns an error and enqueues nothing. ValueCents is in minor units. For $49.00, set it to 4900. The field is spelled out as ValueCents (not Value) to prevent the dollars-versus-cents mistake. It is a pointer so a real 0 is distinguishable from unset.
Keep VisitorID opaque: a random first-party id or a salted hash you control. Never a bare sha256(email) or any hash of an email or phone number (that is re-identifiable PII). The SDK enforces the shape (UUID or h:<64-hex>) but cannot see what you hashed. Keep Properties to ids and enums such as {"plan": "team"}. Do not put emails, names, or phone numbers in them.

Goal(name string, ev TrackEvent) error

Records a goal completion. Same TrackEvent, same endpoint, and same payload as Track, with one difference: it returns a *ValidationError on the reserved names, which belong to the Stripe and Shopify integrations, and enqueues nothing. Reserved: payment, free_trial, trial_started, trial_converted, subscription_started, subscription_upgraded, subscription_downgraded, subscription_renewed, subscription_cancel_scheduled, subscription_reactivated, subscription_ended. Track still accepts them, because that is how those events are legitimately sent. Goal property keys and values are both stored, and both are read back: GET /v1/goals/{name}/properties returns each key and its most common values, and the dashboard renders the same breakdown. Keep them to ids and enums. See what happens to property keys and values.

Payment(ctx context.Context, p Payment) (PaymentResult, error)

Records a payment from any payment processor via POST {host}/v1/server/payments. See the Payment API reference for the full contract. Unlike Page and Track, this one blocks until the server answers and returns what it did with the payment. Amount is the major unit, the opposite of Track’s ValueCents. For $49.99 pass 49.99, and for ¥5000 pass 5000. It is the number you read off your processor. If Currency isn’t one Traceten has a reference rate for, set SettlementAmount/SettlementCurrency to the processor’s own conversion of the payment into a currency it does price (for example, a payout figure), and the payment is recorded off that instead of being dropped. Both must be set together; if only one is set, neither is sent.
Partial refunds do not go through this method. Refunded: true takes back everything still outstanding on the transaction, and this SDK has no field for a smaller figure. To refund part of a payment, POST to /v1/server/payments with refunded_amount, which is the total refunded against the transaction so far.
Calling it again with the same TransactionID returns "duplicate" and creates nothing. That is why the SDK retries a 5xx here. It returns a *ValidationError for an invalid field, and a delivery error when the payment could not be sent after every retry. It is the only method in this SDK that returns a delivery failure to the caller, because a dropped payment is missing revenue rather than a missing pageview.
Do not send payments here for a processor you have also connected natively. Traceten would record the payment twice and overstate your revenue. See avoiding duplicate payments.

Flush(ctx context.Context) error

Drains both queues and blocks until every batch has been attempted, with retries. Returns the first delivery error, if any. Honors ctx cancellation.

Close() error

Flushes, stops the background goroutine, and marks the client closed. Idempotent.

Batching and retries

  • Pageviews are sent to /v1/server/events in batch envelopes of up to 50 events. If you enqueue 120 pageviews, the SDK sends them as batches of 50, 50, and 20.
  • Conversions are sent to /v1/server/conversions one body per event.
  • On a 5xx, a 429, or a network or timeout error, the batch is retried with 200ms * 2^attempt full-jitter backoff, up to WithMaxRetries times.
  • On any other 4xx the payload is malformed. The SDK drops it and calls WithOnError. It does not retry, because retrying a rejected payload forever is a self-inflicted denial of service.

Flush on exit

Go has no reliable atexit. Close is the flush-on-exit mechanism. Call it, usually defer client.Close(), before the process exits so buffered events are delivered. Data loss on a hard kill -9 is expected. Data loss on a graceful shutdown is not, provided you called Close.

Errors

  • New, Page, and Track return an error synchronously for programmer mistakes: ErrMissingSiteID, ErrMissingHost, ErrInvalidHost, ErrMissingVisitorID, ErrClosed, and *ValidationError (bad URL, event name, currency, or ids).
  • Delivery failures never come back from Page or Track. They reach WithOnError and are also returned by Flush. The OnError callback may run on multiple goroutines, so keep it concurrency-safe.

Verify it worked

  1. Send one Page call from your backend.
  2. Open Sites → Events in the Traceten dashboard for that site.
  3. The event appears in the live event feed within a few seconds.
If nothing arrives, register WithOnError to see why.

Verify webhook signatures

If you subscribe to Traceten’s outbound webhooks (AI-session and conversion events), verify every delivery before you act on it. VerifyWebhook recomputes the X-Traceten-Signature HMAC in constant time (hmac.Equal), enforces the replay window, and returns the parsed *WebhookEvent, or an error wrapping ErrSignatureVerification.

Verify the raw body, not a re-serialized one

The single most common verification bug: you decode the JSON, re-encode it, and the bytes no longer match what Traceten signed (key order, whitespace, and unicode escaping all differ). Read the body with io.ReadAll before any JSON decoding, and only act on the event after verification succeeds.
Use traceten.WithTolerance(seconds) to change the 300-second replay window (pass math.Inf(1) only when testing against the fixed conformance vector), and traceten.WithNow(seconds) to inject the clock in tests.

Troubleshooting

Events never appear. Register WithOnError. The most common causes are a wrong host, a site id typo, or a firewall blocking outbound HTTPS. Error from Track. You omitted VisitorID, or the event name has a hyphen. Conversion names allow only a-z, 0-9, and _. Error from Page. The URL is not a valid absolute URL, or the EventName does not match ^[a-z][a-z0-9_-]*$. A short-lived program exits before sending. Ensure defer client.Close() runs, or call client.Flush(ctx) before returning from main.

Next