We use cookies only to manage your session — no third-party tracking or advertising. Learn more in our Privacy Policy.

RoLearn

Unlock Pro features

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 /sdk/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.