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.
| Language | Package | Also 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" }
}
| Field | Rules |
|---|---|
batch | 1–100 messages; the body is at most 512 KB |
type | identify, track or group |
messageId | 8–64 characters, unique per message: the server ignores a repeated messageId, so retries never double-count |
timestamp | ISO 8601 with offset; optional (defaults to the time the server receives it); more than 5 minutes in the future is clamped to now |
userId | your user id (1–256 chars), the same one your frontend passes to identify(). Required for identify; track needs userId or anonymousId |
anonymousId | optional on track: the browser SDK's anonymous id (Guidely.anonymousId) for visitors who have not signed in |
event | track only: 1–120 characters, stored as a custom event (custom:<event> in event pickers) |
properties / traits | flat JSON objects; keys that look sensitive (password, token, card…) are dropped like in the browser SDK |
accountId | identify/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-longevent,groupwithoutaccountId) 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 onflush(). The queue holds at most 10,000 messages; beyond that the oldest are dropped and counted. - Delivery retries network errors,
429and5xxup 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-secondRetry-Afterwins, capped at 60 s). Other4xx(and3xx) answers drop the batch. The samemessageIds 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-Aftercapped 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.jsonpin 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 byshutdownTimeout, default 10 s). Node also flushes onbeforeExit; 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.