Session cookies keep you signed in and are always on. With your consent we also use analytics and advertising cookies. Decline and we keep only the session ones. See our Cookie Policy.

ROLearn/ Documentation / SDK
Get API key →SDK product →

Events reference

Every event the SDK accepts, the exact payload schema the server validates, and which dashboard surface each one feeds. Copy any snippet — these are the same shapes the ingest endpoint enforces via Pydantic.

canonical sourceThe schemas on this page are generated from backend/sdk/schema.py. The server rejects events that violate them; the dashboards rely on them. Extra fields you add are preserved on sdk_events.payload and queryable in the Event Explorer.

Event envelope

Every event — regardless of type — is wrapped in this envelope. The payload object is what changes per event type and is documented below.

Event
{
  "event_id":   "evt_3f9c1a2b...",  // string, 8-40 chars, idempotency key
  "ts":         "2026-05-24T12:00:00Z",  // ISO 8601 UTC; defaults to now if omitted
  "platform":   "roblox",           // roblox | unity | steam | fortnite
  "game_id":    "12345678",         // string, 1-64 chars, platform-native id
  "player_id":  "hashed_user_id",   // string,128 chars, NEVER raw PII
  "session_id": "sess_xyz",         // string,64 chars, optional
  "type":       "session_start",    // see "Standard events" below
  "payload":    { /* per-type schema */ }
}

Ingest request & response

Batch up to 500 events per request. The api_key may be provided in the JSON body or as a Bearer token — pick one. Async-mode deployments return 202 with the X-RoLearn-Ingest-Mode: async header; on backpressure they transparently fall back to sync ingest and return 200. Either way the body shape is identical.

Request
POST /api/sdk/events
Authorization: Bearer rk_live_XXXX
Content-Type: application/json
X-RoLearn-SDK-Version: 0.5.0

{
  "api_key": "rk_live_XXXX",
  "events":  [ /* 1-500 Event objects */ ]
}
Response
HTTP/1.1 202 Accepted
X-RoLearn-Ingest-Mode: async

{
  "accepted": 47,
  "rejected": 0,
  "deduped":  3,
  "errors":   []
}

Validation rules

  • Idempotency. event_id dedups against a Redis set — re-POSTing the same id is safe and reported in deduped.
  • PII guard. Any payload containing one of email, phone, phone_number, ssn, credit_card, or cc_number (case-insensitive) is rejected. Hash player_id client-side.
  • Unknown event types are rejected. Stick to the taxonomy below or use "type": "custom" with a name field.
  • Rate limit. Per-key burst + sustained, enforced in Redis. Hitting it returns 429.
  • Quota. Monthly per-team event cap by plan (10K Free → 100M Enterprise). Over-quota returns 503 + Retry-After: 15 — never silent.

Standard events

Thirteen event types across five groups. Field constraints in the per-event sections below; this table is the at-a-glance map.

TypeGroupPowers
session_startLifecycleDAU, MAU, new users, geo
session_endLifecyclePlaytime (avg / p50 / p90), session length
purchaseMonetizationRevenue, ARPU, ARPPU, paying users, per-country items
ad_impressionMonetizationARPDAU-ads, format / network split
levelGameplayFunnel builder, retention drivers
customGameplayEvent Explorer, custom funnels, segment DSL
performanceQualityFPS histogram, p5 / p50 / p95 by device
crashQualityCrashlytics-style signature groups, crash-free %
errorQualitySentry-lite warn / error / fatal rollup
brand_zone_enterBrand activationBrand-zone reach (distinct players × zone)
brand_zone_exitBrand activationBrand-zone dwell (median seconds in zone)
equipBrand activationRepeat-equip (players who equipped ≥ 2 days)
shareBrand activationShare funnel stage on the v2 dashboard
brand_survey_shownBrand LiftBrand Lift survey impressions (exposed vs control)
brand_survey_responseBrand LiftBrand Lift — attitude lift (exposed vs control favorable %)

Lifecycle

Auto-emitted on Roblox via PlayerAdded and PlayerRemoving. On Unity and Steamworks, call them from your app lifecycle hooks.

session_start — player joined

Powers: DAU, MAU, new users, geo

payload (session_start)
{
  "device":     "PC",           // optional, free-form
  "os":         "Windows 11",   // optional
  "country":    "US",           // optional, ISO 3166-1 alpha-2
  "membership": "premium"       // optional, e.g. Roblox Premium tier
}

Fields:

  • device (string, optional) — free-form device fingerprint (e.g. "PC", "Phone")
  • os (string, optional) — operating system label
  • country (string, optional) — ISO 3166-1 alpha-2; cached so later events can omit it
  • membership (string, optional) — subscription tier label (e.g. Roblox "premium")

session_end — player left

Powers: Playtime aggregates (avg / p50 / p90), session length distribution

payload (session_end)
{
  "duration_s": 312.4,          // optional, seconds since session_start
  "reason":     "player_left",  // optional, free-form
  "country":    "US"            // optional, propagated from session_start
}

Fields:

  • duration_s (number, optional) — seconds elapsed since the matching session_start
  • reason (string, optional) — why the session ended (free-form)
  • country (string, optional, ≤8) — propagated from session_start cache by the client

Monetization

purchase — player paid for something

Powers: Revenue, ARPU, ARPPU, paying users, per-country × item rollup

payload (purchase)
{
  "sku":      "vip_pass",                 // required, your item id
  "name":     "VIP Pass",                 // optional, display name
  "currency": "USD",                      // default "USD"; use "ROBUX" for Robux
  "price":    4.99,                       // required,0
  "quantity": 1,                          // default 1,1
  "source":   "gamepass"                  // iap | gamepass | devproduct | subscription
  // "country" optional, propagated from session_start
}

Fields:

  • sku (string, required) — your stable item id (used for SKU breakdown chart)
  • name (string, optional) — human-readable display name
  • currency (string, default "USD") — currency code; use "ROBUX" for Robux
  • price (number, required, ≥ 0) — price in the listed currency (sub-cent OK)
  • quantity (integer, default 1, ≥ 1) — units purchased
  • source (enum, default "iap") — "iap" | "gamepass" | "devproduct" | "subscription"
  • country (string, optional, ≤8) — ISO country; propagated from session cache

ad_impression — ad shown to player (ILRD)

Powers: ARPDAU-ads, ad-revenue by format / network / country

payload (ad_impression)
{
  "format":      "rewarded",      // banner | interstitial | rewarded | native
  "network":     "applovin_max",  // required, 1-64 chars (mediation network name)
  "placement":   "level_complete",// optional,64 chars (your surface name)
  "revenue_usd": 0.0123,          // required,0 (publisher revenue for THIS impression)
  "ecpm":        12.30,           // optional,0
  "country":     "US"             // optional
}

Fields:

  • format (enum, required) — "banner" | "interstitial" | "rewarded" | "native"
  • network (string, required, 1-64) — mediation network name (e.g. "applovin_max")
  • placement (string, optional, ≤64) — your surface name (e.g. "level_complete")
  • revenue_usd (number, required, ≥ 0) — publisher revenue in USD for THIS impression
  • ecpm (number, optional, ≥ 0) — reported eCPM if the network surfaces it
  • country (string, optional, ≤8) — ISO country

Gameplay

level — level / round outcome

Powers: Funnel builder, retention drivers, segment DSL conditions

payload (level)
{
  "level":  3,             // required, integer ≥ 0
  "score":  1200,          // optional
  "result": "complete"     // win | loss | quit | complete (default "complete")
}

Fields:

  • level (integer, required, ≥ 0) — level number
  • score (integer, optional) — final score for this attempt
  • result (enum, default "complete") — "win" | "loss" | "quit" | "complete"

custom — anything else

Powers: Event Explorer, ad-hoc funnels, segment DSL (event_count + payload-attr filters)

payload (custom)
{
  "name": "tutorial_skipped",  // required — your event name
  // any additional fields you want, e.g.:
  "step":      4,
  "elapsed_s": 12.7
}

Fields:

  • name (string, required) — your event name — this is what you query on
  • (any) — add any other fields; they’re preserved on the event row

Quality

Auto-captured on Roblox via RunService.Heartbeat + LogService.MessageOut when you enable capture_performance / capture_crashes in SDK.init. On Unity these are emitted by the SDK's built-in profiler at 1 Hz.

performance — FPS / frame-time / memory sample

Powers: FPS histogram, p5 / p50 / p95 series, per-device buckets

payload (performance)
{
  "fps_avg":          58.4,          // required, 0-1000
  "fps_p5":           42.0,          // optional, 0-1000
  "frame_time_ms_p95": 22.3,         // optional, 0-10000
  "memory_mb":        612.4,         // optional,0
  "gpu":              "NVIDIA RTX 4060", // optional,128 chars
  "device":           "PC"           // optional,64 chars (buckets the histogram)
}

Fields:

  • fps_avg (number, required, 0-1000) — average FPS over the sample window
  • fps_p5 (number, optional, 0-1000) — 5th-percentile FPS (the bad-frame floor)
  • frame_time_ms_p95 (number, optional, 0-10000) — p95 frame time in ms
  • memory_mb (number, optional, ≥ 0) — resident memory in MB
  • gpu (string, optional, ≤128) — GPU label, if available
  • device (string, optional, ≤64) — device class — buckets the histogram

crash — hard crash with stack

Powers: Crashlytics-style signature groups (top-5 frames hashed), crash-free %

payload (crash)
{
  "stack_trace": "ScriptError: attempt to index nil with 'Position'\n    at Module.update (Server/Combat.lua:142)\n    ...",
  "signature":   "Combat.lua:142:nil_index",  // optional pre-grouping key
  "build":       "1.4.2",                     // optional
  "os":          "Windows 11"                 // optional
}

Fields:

  • stack_trace (string, required, 1-16000) — raw multi-line stack; aggregator hashes the top-5 normalized frames
  • signature (string, optional, ≤255) — client-side pre-grouping key (e.g. error code); skipped if absent
  • build (string, optional, ≤64) — your build / version string
  • os (string, optional, ≤64) — OS label

error — non-fatal warn / error / fatal

Powers: Severity rollup (warn / error / fatal counts), top error sources

payload (error)
{
  "severity": "error",                              // warn | error | fatal
  "message":  "Datastore SetAsync failed: 502",     // required, 1-2000 chars
  "source":   "PlayerData.save"                     // optional,128 chars
}

Fields:

  • severity (enum, required) — "warn" | "error" | "fatal"
  • message (string, required, 1-2000) — error message; aggregated by (severity, source, message-prefix)
  • source (string, optional, ≤128) — subsystem / module label

Brand activation

Power the brand-zone reach, dwell, repeat-equip, and share-funnel tiles on the CMO-flavored /studio/dashboard. Until your game starts emitting these, the dashboard falls back to proxies (treat every player as engaged for reach, average session length as dwell, paying / DAU ratio as repeat-equip).

brand_zone_enter — player entered a branded zone

Powers: Brand-zone reach (distinct players × zone × country)

payload (brand_zone_enter)
{
  "zone_id": "main_stage",   // required, 1-64 chars (your zone id)
  "country": "US"            // optional
}

Fields:

  • zone_id (string, required, 1-64) — your zone id (e.g. "lobby_hat_shop", "main_stage")
  • country (string, optional, ≤8) — ISO country

brand_zone_exit — player left a branded zone

Powers: Brand-zone dwell (median seconds per zone)

payload (brand_zone_exit)
{
  "zone_id":       "main_stage",  // required, must match the enter event
  "dwell_seconds": 47.2,          // required, 0-86400 (time in zone)
  "country":       "US"           // optional
}

Fields:

  • zone_id (string, required, 1-64) — must match the matching brand_zone_enter
  • dwell_seconds (number, required, 0-86400) — time spent in the zone (client tracks this)
  • country (string, optional, ≤8) — ISO country

equip — player equipped a branded item

Powers: Repeat-equip — players who equipped the same item on ≥ 2 distinct UTC days

payload (equip)
{
  "item_id":   "branded_hat_07",         // required, 1-64 chars
  "item_name": "Sponsor Cap — Red",      // optional,128 chars
  "country":   "US"                      // optional
}

Fields:

  • item_id (string, required, 1-64) — stable item id
  • item_name (string, optional, ≤128) — human-readable name
  • country (string, optional, ≤8) — ISO country

share — player shared content externally

Powers: Share stage on the v2 dashboard funnel

payload (share)
{
  "share_platform": "twitter",       // required, 1-32 chars (destination)
  "item_id":        "branded_hat_07",// optional — tie to a branded asset
  "country":        "US"             // optional
}

Fields:

  • share_platform (string, required, 1-32) — destination — "twitter", "discord", etc.
  • item_id (string, optional, ≤64) — tie the share to a specific branded asset
  • country (string, optional, ≤8) — ISO country

brand_survey_shown — a Brand Lift survey card was shown to a player

Powers: Brand Lift survey impressions, split by exposed / control cohort

payload (brand_survey_shown)
{
  "survey_id": "42",        // required — the Brand Lift study id
  "cohort":    "exposed",   // "exposed" (entered the zone) | "control"
  "country":   "US"         // optional
}

Fields:

  • survey_id (string, required, 1-64) — the Brand Lift study id
  • cohort (string, optional, ≤16) — "exposed" (entered the brand zone) or "control"
  • country (string, optional, ≤8) — ISO country

brand_survey_response — a player answered a Brand Lift survey

Powers: Brand Lift — exposed-vs-control favorable-rate lift, significance-tested, fed into OMNI-EMV DBI

payload (brand_survey_response)
{
  "survey_id": "42",        // required — the study id
  "answer":    "likely",    // required — the chosen answer key (fixed-choice)
  "favorable": true,        // whether that answer is a positive brand outcome
  "cohort":    "exposed",   // "exposed" | "control"
  "country":   "US"         // optional
}

Fields:

  • survey_id (string, required, 1-64) — the study id
  • answer (string, required, 1-64) — the chosen answer key (fixed-choice — never free text)
  • favorable (bool, optional) — whether that answer counts as a positive brand outcome
  • cohort (string, optional, ≤16) — "exposed" | "control"
  • country (string, optional, ≤8) — ISO country

These two are emitted automatically by the runtime's Brand Lift survey flow — you don't call them yourself. You wire the exposed side with RoLearn.trackBrandZoneEnter(player, { zone_id = "…" }); the control arm and the response capture happen on their own.

Custom events — when to use them

Use "type": "custom" for anything outside the taxonomy above. The name field is what you'll filter on in the Event Explorer and reference in the segment DSL. A few rules of thumb:

  • Don't reinvent standard events. A custom event called "bought_item" won't show up in revenue charts — purchase will.
  • Keep name stable. It's the join key for every segment, funnel, and dashboard tile that references it. Rename it and historical analytics break.
  • Keep cardinality bounded. A name that embeds player_id or a timestamp will balloon the index. Put high-cardinality values in payload fields, not in the name.
  • Add fields freely. Anything in the payload is preserved and queryable. The PII guard is the only restriction.

Roblox helper recipes

The Roblox SDK exposes SDK.purchase() and a generic SDK.track(type, payload, player?). Lifecycle and quality events fire automatically when enabled. Equivalent Unity (RoLearnSDK.Track) and Steamworks (rolearn::SDK::instance().track) APIs follow the same shape — see Install for syntax.

ServerScriptService/your-script
local SDK = require(game.ServerScriptService.RoLearnSDK)

-- Lifecycle events fire automatically on PlayerAdded / PlayerRemoving.
-- You only need to call these helpers for the rest:

-- Purchase (recommended: also call from MarketplaceService.ProcessReceipt)
SDK.purchase(player, {
    sku = "vip_pass", price = 99, currency = "ROBUX", source = "gamepass",
})

-- Custom gameplay event
SDK.track("level", { level = 3, score = 1200, result = "win" }, player)

-- Ad impression (call inside your reward callback)
SDK.track("ad_impression", {
    format = "rewarded", network = "applovin_max",
    placement = "level_complete", revenue_usd = 0.0123,
}, player)

-- Brand-zone pairing (enter on touch, exit on leave with dwell timing)
SDK.track("brand_zone_enter", { zone_id = "main_stage" }, player)
-- ... when player leaves the zone:
SDK.track("brand_zone_exit", { zone_id = "main_stage", dwell_seconds = 47.2 }, player)

-- Equip / share (fire from your existing equipment + share UI)
SDK.track("equip", { item_id = "branded_hat_07", item_name = "Sponsor Cap" }, player)
SDK.track("share", { share_platform = "twitter", item_id = "branded_hat_07" }, player)

-- Performance + crash are auto-captured via Heartbeat + LogService when
-- you enable them in SDK.init({ capture_performance = true, capture_crashes = true }).

Next steps

  • Integration reference — auth scoping, quotas, GDPR delete, key rotation.
  • Event Explorer — search every event you've sent, filter by type / player / game.
  • Segments — build player segments from event_count and payload-attribute conditions.
  • Dead-letter queue — inspect (and replay) anything the ingest endpoint rejected.