Server-side SDKs: Track Events

Send product data from your backend: who a user is (identify), what they did (track) and what account they belong to (group). Server events land in the same ProductEvent table as browser and app events, so they work in Events, Funnels, Retention, audiences and guide targeting (event rules) just like Guidely.track() calls from the SDK.

LanguagePackageAlso includes
Node.js (18+)npm install @guidelytech/node— (flags: @guidelytech/flags)
Java (11+)com.guidelytech:guidely-flags (com.guidelytech.track.GuidelyTrack)Feature flags (GuidelyFlags)
PHP (8.1+)composer require guidelytech/guidely-php (GuidelyTech\Track\GuidelyTrack)Feature flags (GuidelyFlags)

Use server events for things that happen outside the browser or that the browser can't be trusted with: a subscription renewed (Stripe webhook), an export finished (background job), an API call made with a customer's token, a plan change, account-level traits such as MRR or seat count.

Keys

Server events are authenticated with a secret key (gly_sk_…), the same kind of key server-side feature flags use. Create one in Feature flags → Settings → Server keys with the events:write scope (a key can carry flags:read too, so one key serves both). Secret keys are shown once and stored hashed; keep them in your secret manager and never ship them to a browser or app. Every request names the environment it writes to (development, staging or production).

HTTP API

POST /api/v1/sdk/server/batch?environment=production
Authorization: Bearer gly_sk_…
Content-Type: application/json
{
  "batch": [
    { "type": "identify", "messageId": "kq3x…", "timestamp": "2026-10-26T10:00:00Z", "userId": "u_42", "traits": { "email": "ada@example.com", "plan": "pro" }, "accountId": "acme" },
    { "type": "track", "messageId": "p0zl…", "timestamp": "2026-10-26T10:00:01Z", "userId": "u_42", "event": "Report Exported", "properties": { "format": "csv" } },
    { "type": "group", "messageId": "m2fa…", "accountId": "acme", "traits": { "name": "Acme Inc", "mrr": 4200 }, "userId": "u_42" }
  ],
  "sentAt": "2026-10-26T10:00:02Z",
  "sdk": { "name": "guidely-node", "version": "0.11.0" }
}
FieldRules
batch1–100 messages; the body is at most 512 KB
typeidentify, track or group
messageId8–64 characters, unique per message: the server ignores a repeated messageId, so retries never double-count
timestampISO 8601 with offset; optional (defaults to the time the server receives it); more than 5 minutes in the future is clamped to now
userIdyour user id (1–256 chars), the same one your frontend passes to identify(). Required for identify; track needs userId or anonymousId
anonymousIdoptional on track: the browser SDK's anonymous id (Guidely.anonymousId) for visitors who have not signed in
eventtrack only: 1–120 characters, stored as a custom event (custom:<event> in event pickers)
properties / traitsflat JSON objects; keys that look sensitive (password, token, card…) are dropped like in the browser SDK
accountIdidentify/track: links the user to that account. Required for group

identify merges traits into the user's traits (creating the user if needed); group merges traits into the account (name also becomes the account name) and, with userId, adds the user to it. Server calls never count as a visit or session.

Responses: 202 { "accepted": n, "duplicates": n }; 400 invalid or too large body (do not retry); 401/403 bad key or missing events:write scope (do not retry); 404 the project has no such environment; 429 rate limited (retry after Retry-After); 5xx retry. Events past the plan's analytics allowance are counted in overLimit and dropped, exactly like browser events.

SDK behaviour (all languages)

  • Calls never throw and never block on the network: they validate, enqueue and return. Invalid calls (empty userId, empty or over-long event, group without accountId) are dropped and reported to the error handler/logger.
  • Messages are batched: a batch is sent when 100 messages are queued (flushAt), every 5 seconds (flushInterval) and on flush(). The queue holds at most 10,000 messages; beyond that the oldest are dropped and counted.
  • Delivery retries network errors, 429 and 5xx up to 5 times after the first attempt (maxRetries) with exponential backoff (1 s, 2 s, 4 s, … capped at 30 s, ±20 % jitter; a whole-second Retry-After wins, capped at 60 s). Other 4xx (and 3xx) answers drop the batch. The same messageIds are resent, so retries are safe. PHP retries inline during the request, so its defaults are short: 3 retries of 0.1, 0.2 and 0.4 s, Retry-After capped at 1 s.
  • Limits checked in the SDK: a message over 32 KB is dropped; a batch holds at most 100 messages and 500,000 bytes. The shared vectors in sdks/conformance/track-vectors.json pin message building, validation, retry decisions and batching for all three SDKs.
  • Shutdown: call shutdown()/close() before your process exits; it stops the timer and flushes what is queued (bounded by shutdownTimeout, default 10 s). Node also flushes on beforeExit; Java registers an optional JVM shutdown hook; PHP flushes at the end of the request (destructor / register_shutdown_function), since a PHP process does not outlive the request.