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 →

Integration reference

Everything you need to wire your game's events to ROLearn. This page is platform-agnostic — it documents the wire format that every embed client speaks. If you're starting with copy-paste snippets, see Install first.

Canonical event

Every event the embed clients emit conforms to this shape. The server validates it via backend/sdk/schema.py and rejects anything malformed (rejected events are visible to you in /sdk/dlq).

Event
{
  "event_id":   "evt_3f9c1a2b...",        // 8-40 chars, idempotency key
  "ts":         "2026-05-08T12:00:00Z",
  "platform":   "roblox" | "unity" | "steam" | "fortnite",
  "game_id":    "12345678",                // platform-native id
  "player_id":  "hashed_user_id",          // 128 chars, NEVER raw PII
  "session_id": "sess_xyz",                // optional
  "type":       "session_start" | "session_end" |
                "purchase" | "level" | "custom" |
                "ad_impression" | "performance" |
                "crash" | "error" |
                "brand_zone_enter" | "brand_zone_exit" |
                "equip" | "share",
  "payload":    { ...per-type schema — see /sdk/learn/events... }
}

Purchase payload

The purchase type has a strict schema — these fields drive the revenue dashboard. Other types (level,custom, session_*) accept open dicts.

payload (purchase)
{
  "sku":      "vip_pass",
  "name":     "VIP Pass",
  "currency": "ROBUX" | "USD" | "EUR" | "...",  // Roblox defaults to ROBUX (v0.4.0+)
  "price":    99,                               // Robux units OR fiat (matches currency)
  "quantity": 1,
  "source":   "iap" | "gamepass" | "devproduct" | "subscription" | "in_game"
}

Platform-specific integration you must wire yourself

The SDK auto-tracks everything each platform safely lets it — sessions, country, crashes, FPS, and (on Roblox) gamepass purchases. But every platform has surfaces that cannot be auto-hooked without breaking your game (e.g. Roblox's ProcessReceipt can only have one handler — yours). Those require a one-line call from your code. Below is the complete list, per engine.

Roblox (Lua)

pluginIf you installed via the Studio plugin, the generated init script already wires both gamepass and developer-product (ProcessReceipt) purchase tracking for you — so the "manual wiring" subsections below apply only to a hand-rolled install. You still wire in-game / soft-currency spend yourself either way.

Roblox has three monetization surfaces — gamepasses, developer products, and in-game Robux/soft-currency spending. With a manual install only gamepasses auto-track; the plugin's init script also covers developer products.

What's auto-tracked (zero code)

  • Gamepasses via MarketplaceService.PromptGamePassPurchaseFinished. Once SDK.init() runs, every gamepass purchase emits a purchase event with source="gamepass" and currency="ROBUX" automatically.
  • Sessions via Players.PlayerAdded / PlayerRemovingsession_start / session_end are auto-emitted.
  • Country via LocalizationService:GetCountryRegionForPlayerAsync (since v0.4.0) — cached once on player join, attached to every subsequent event for that player.
  • Crashes & errors via LogService.MessageOut — multi-line errors grouped as crash events, single-line as error events.
  • FPS performance sampled via RunService.Heartbeat — one snapshot emitted per session_end.

Manual wiring required — Developer Products

Roblox lets exactly one function be assigned to MarketplaceService.ProcessReceipt, and that function is responsible for granting the item AND returning PurchaseGranted. If the SDK overwrote it, your item-delivery code would never run. So you must add the RoLearnSDK.purchase() call inside your own ProcessReceipt:

ProcessReceipt — dev product wiring
local MarketplaceService = game:GetService("MarketplaceService")
local RoLearnSDK         = require(game.ServerScriptService.RoLearnSDK)

local function processReceipt(receipt)
    local player = game.Players:GetPlayerByUserId(receipt.PlayerId)
    if not player then return Enum.ProductPurchaseDecision.NotProcessedYet end

    -- 1. Grant the product to the player.
    --    ...your existing logic to give them coins / gems / items...

    -- 2. Tell ROLearn about it.
    RoLearnSDK.purchase(player, {
        sku      = tostring(receipt.ProductId),
        price    = receipt.CurrencySpent,
        currency = "ROBUX",
        source   = "devproduct",
    })

    return Enum.ProductPurchaseDecision.PurchaseGranted
end

MarketplaceService.ProcessReceipt = processReceipt

Manual wiring required — in-game spending

Custom shops, premium payouts, hard-currency gates — any place the player spends a value inside your game (vs. through Roblox's purchase prompt) also needs a manual call. Pass your soft-currency code if it's not Robux.

In-game spend tracking
-- Player spending hard or soft currency in your custom shop
RoLearnSDK.purchase(player, {
    sku      = "skin_purple_dragon",
    name     = "Purple Dragon Skin",
    price    = 75,           -- Robux (or in-game currency unit)
    currency = "ROBUX",      -- or your soft-currency code, e.g. "COINS"
    source   = "in_game",
})

Sanity test before adding real wiring

Fastest way to prove the full pipeline (SDK → ingest → DLQ → dashboard) works before you touch any game code. Paste into Studio's Command Bar:

Command Bar — sanity test
-- Paste into Studio's Command Bar with at least one test player connected.
local RoLearnSDK = require(game.ServerScriptService.RoLearnSDK)
local player     = game.Players:GetPlayers()[1]

RoLearnSDK.purchase(player, {
    sku      = "test_sanity_99",
    name     = "Sanity check",
    price    = 99,
    currency = "ROBUX",
    source   = "devproduct",
})
-- → event lands in /sdk/events within ~10s. If not, check /sdk/dlq.
gotchas
  • Currency default — since v0.4.0, omitting currency defaults to "ROBUX". In v0.3.x it incorrectly defaulted to "USD", which silently excluded every auto-tracked gamepass purchase from the revenue dashboard. Upgrade to v0.4.0+ if you're on the older version.
  • Server-side only — never require() RoLearnSDK from a LocalScript. The api_key field would be exposed to clients.
  • Studio test mode — country resolves to "XX" when running in Studio. Real ISO country codes appear only after the game is published and played by real users.
  • HTTP Requests — must be enabled in Game Settings → Security → "Allow HTTP requests". Without this, every event silently fails.
  • Place_id required — the game must be published. game.PlaceId is 0 in unpublished places and the ingest endpoint will 403 the events.

Unity (C#)

What's auto-tracked (zero code)

  • Sessions via Unity's lifecycle callbacks — session_start on Init(), session_end on OnApplicationQuit.
  • Unity IAP purchases via the shipped IAP listener — auto-tracks when your project has UNITY_PURCHASING enabled. Source is set to "iap", currency to the localized currency code from Product.metadata.
  • Crashes via Application.logMessageReceivedThreaded — Exception-level logs ship as crash events with the full stack.

Manual wiring required — non-Unity-IAP stores

If you ship to platforms outside Unity IAP's coverage (custom storefronts, Epic Online Services, direct Stripe, Steam via Steamworks.NET, etc.), the IAP listener has nothing to hook. Call RoLearnSDK.Purchase()at the point your code grants the entitlement:

Manual purchase call
// Custom store / Epic / Steam / non-Unity-IAP storefronts:
// call SDK.Purchase yourself at the point you grant the entitlement.
RoLearnSDK.Purchase(new PurchaseInfo {
    Sku       = "battle_pass_season_4",
    Name      = "Season 4 Battle Pass",
    Price     = 9.99,
    Currency  = "USD",
    Source    = "iap",
});

Manual wiring required — mobile lifecycle

On Android & iOS, players who background the app don't fire OnApplicationQuit. Without the hook below, those sessions stay "open" until the autosave timeout (~5 min), inflating average session length. Wire OnApplicationPause in your bootstrap MonoBehaviour:

Mobile lifecycle hook
// Mobile lifecycle — without this, backgrounded players show as
// "still in session" for the autosave timeout window (~5 min).
void OnApplicationPause(bool paused) {
    if (paused) RoLearnSDK.FlushAndPauseSession();
    else        RoLearnSDK.ResumeSession();
}

void OnApplicationQuit() {
    RoLearnSDK.FlushSync();   // blocks <= 2s while in-flight events drain
}
gotchas
  • IL2CPP stripping — Unity's IL2CPP build pipeline can strip SDK reflection paths. The shipped link.xml protects the public surface; do NOT add the SDK assembly to any custom strip exclusion list (it'll bloat your binary by ~400KB without benefit).
  • iOS privacy manifest — Apple now requires a PrivacyInfo.xcprivacy declaration for SDKs that read device identifiers. ROLearn ships one in the package — don't override it.
  • Editor vs. buildApplication.identifier is the package name in builds but defaults to com.DefaultCompany.<ProjectName> in the Editor. Use a stable GameId string in your config to avoid Editor sessions polluting your prod game's data.

Steamworks (C++)

What's auto-tracked (zero code)

  • Sessions via the SDK's SteamAPI_RunCallbacks heartbeat — session_start on init(), session_end on shutdown().
  • SteamID attachment — when the Steamworks SDK is detected, ISteamUser::GetSteamID() is attached to every event as player_id (hashed). No call needed beyond initializing Steam before ROLearn.
  • Live concurrent users via the ISteamUserStats hook (since v0.2.0) — periodic capture, ships as session_start beacons.

Manual wiring required — microtransactions

Steam's ISteamMicroTxn doesn't expose a "purchase finished" callback the SDK can listen to (unlike Roblox MarketplaceService). You poll order status server-side and emit the event on transition to Authorized or Approved:

ISteamMicroTxn polling
// ISteamMicroTxn doesn't give a "purchase finished" callback you can hook
// like Roblox MarketplaceService does. You poll OrderStatus and emit on
// transition to "Authorized" or "Approved". Pattern:

void OnOrderAuthorized(uint64_t order_id, const std::string& sku, double usd) {
    rolearn::SDK::instance().purchase({
        .sku      = sku,
        .price    = usd,
        .currency = "USD",
        .source   = "iap",
    });
}
gotchas
  • Steam-init order — call SteamAPI_Init() BEFORE rolearn::SDK::instance().init(). If ROLearn initializes first, SteamID attachment falls back to a generated UUID (still works, but cross-game identity won't link).
  • Thread safety — the SDK singleton is thread-safe (since v0.2.0), but the payload std::map you pass to track() is moved, not copied. Don't mutate it on another thread after the call.
  • Linker order — link order must be: your game → rolearn_sdklibcurlnlohmann/json. Reversing curl and SDK produces "undefined symbol: curl_easy_init" on Linux builds with -Wl,--as-needed.

Fortnite / UEFN (Verse)

What's auto-tracked (zero code)

  • Player joined / left via the standard creative_device.OnBegin pattern — emits session_start / session_end.
  • Island lifecycle — round-start and round-end events when your island uses the standard fort_playspace primitives.

Manual wiring — custom events

UEFN's Verse sandbox restricts what an SDK can observe. Custom events and player progress work; in-game V-Bucks tracking is NOT exposed by Epic's Verse API and cannot be auto-instrumented (Epic owns that data surface).

Custom event in Verse
# UEFN sandbox: the SDK exposes only what Verse permits.
# Custom events + session lifecycle work; in-game V-Bucks tracking
# is NOT exposed by Epic's Verse API and cannot be auto-instrumented.

RoLearn.Track("level_complete", props := {
    level := 3,
    score := 1200,
})
gotchas
  • UEFN version pinning — Verse modules pin to a UEFN engine version. When you upgrade UEFN, re-import the latest ROLearn Verse module from /sdk/learn/downloads.
  • No client-side events — Verse runs on the server-equivalent of UEFN; there's no client-script equivalent. All instrumentation is from the game-logic surface.
  • Limited monetization surface — by Epic's design, ROLearn cannot see V-Bucks spending. Use Epic's first-party analytics for monetization KPIs; use ROLearn for player behavior + retention.

Authentication

Each request carries an api_key field. Keys are team-scoped and prefixed rk_live_. Create + rotate them in /sdk/games (the SDK setup hub); rotation is zero-downtime with a 7-day grace window on the old key.

Keys are also game-scoped — every event in a batch must reference a game your team has linked. Events with unlinked game_id values are 403'd and copied to the DLQ for inspection.

Ingest endpoint

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

{
  "api_key": "rk_live_XXXX",
  "events":  [ /* up to 500 Event objects */ ]
}

Returns { accepted, deduped, rejected }. The X-RoLearn-SDK-Version header is optional but recommended — when present, the server enforces a minimum supported version and returns 426 Upgrade Requiredif your client is too old.

Idempotency

The server dedups on event_id with a rolling 7-day window. Retries with the same id are silently dropped — safe to retry on flaky networks. New ids past 7 days WILL count again, so use stable per-event UUIDs, not "retry counter" values.

Rate limits & quotas

Two limits stack:

  • Per-key rate limit — sliding minute window, default 6,000 events/min. Configurable per key.
  • Per-team monthly quota — driven by your SDK plan. See the table below.
SDK planEvents / monthLinked gamesAdvanced features
Free10,0001Aggregator + dashboard
Starter — $29 / mo100,0003+ Live event log
Growth — $199 / mo5,000,00010+ Funnel builder, retention
Scale — $999 / mo50,000,000100+ Live stream, data export, A/B testing, 99.9% SLA
Enterprise100M+unlimited+ A/B testing, 99.99% SLA, signed DPA, success manager

A/B testing · Scale + Enterprise plans

Phase 8.1 (shipped 2026-05-26) wires A/B testing end-to-end. You define experiments in /sdk/experiments — name, variants (with traffic weights), traffic %, status — and the embed clients call getVariant / GetVariantAsync / get_variant to retrieve a player's sticky deterministic assignment. The server uses SHA1 over (experiment_seed, player_id) so the same player always gets the same variant across servers and sessions until the experiment closes.

Results (per-variant DAU + ARPDAU + 2-proportion z-test + Welch t-test) are computed daily by the SDK aggregation pipeline and rendered in the same console.

Roblox

getVariant — Roblox
-- Phase 8.1: sticky deterministic variant assignment.
-- Per-server 1h cache keyed by (experimentId, playerId), so calling
-- getVariant per-event is safe — the assign endpoint is hit at most
-- once per player per hour.
local RoLearnSDK = require(game.ServerScriptService.RoLearnSDK)

game.Players.PlayerAdded:Connect(function(player)
    local variant = RoLearnSDK.getVariant(player, 42)   -- 42 = experiment_id
    if variant == "treatment" then
        -- ship the new shop layout
    elseif variant == "control" then
        -- ship the old shop layout
    else
        -- variant == nil → not in experiment (fall back to default)
    end
end)

Unity

GetVariantAsync — Unity
// Phase 8.1: GetVariantAsync returns Task<string>.
// 1h in-memory cache keyed by ($"{experimentId}:{playerId}").
var variant = await RoLearnSDK.GetVariantAsync(42, playerId);
if (variant == "treatment") {
    // ship the new tutorial
} else if (variant == "control") {
    // ship the old tutorial
} else {
    // variant == null → not in experiment / fetch failed
}

Steamworks

get_variant — Steamworks
// Phase 8.1: get_variant blocks on libcurl, mutex-guarded 1h cache.
// Returns empty string if the player is not in the experiment OR if
// the assign call fails — fall back to your default variant.
auto variant = rolearn::SDK::instance().get_variant(42, player_id);
if (variant == "treatment") {
    // ship the new pricing
} else if (variant == "control") {
    // ship the old pricing
} else {
    // not in experiment / fetch failed
}
how it works
  • Sticky — assignments are stored in sdk_experiment_assignment with a UNIQUE constraint on (experiment_id, player_id). The first call wins; every subsequent call for that player returns the same variant.
  • Deterministic — variant choice is sha1(experiment.seed + ":" + player_id) bucketed against cumulative traffic weights, so even a fresh server with an empty cache returns the same answer.
  • Cached client-side — every SDK caches the assignment for 1 hour in process memory. Per-event calls are safe; the network hit happens at most once per player per hour.
  • Fail-safe — network errors, an invalid api_key, or a non-running experiment all return nil / null / "". Always have a default branch.

Privacy & GDPR

The server rejects any payload containing fields namedemail, phone, ssn,credit_card, or other obvious PII shapes — defense in depth, since clients SHOULD never send those. Hash player ids before sending; the SDKs ship a before_send(event)hook for last-mile redaction.

A right-to-be-forgotten endpoint hard-deletes every event for a given player_id within your team's data:

GDPR delete
DELETE /api/sdk/data/players/{player_id}
limitationThe deletion purges raw events + DLQ rows. Pre-computed daily aggregates (DAU, retention cohorts) hold derived counts that already include the player's prior activity — these are not per-player and not personally identifying. The nightly aggregation pipeline naturally recomputes them as the rolling window advances.

Errors

StatusMeaningWhat to do
400Malformed payloadCheck the event schema; the response body lists which fields failed.
401Invalid api_keyVerify the key is active in /sdk/games.
403Game not linked / scope errorLink the game in /sdk/games.
422PII detected in payloadStrip PII fields client-side before sending.
426SDK client too oldUpgrade to the version listed in the response.
429Rate limit or quota exceededRetry after the seconds in Retry-After.
5xxServer errorRetries are safe (idempotent on event_id).