# SDK reference (/docs/reference/sdk)

Setup is covered in [Install in Roblox Studio](/docs/getting-started/install). The Luau module is for your game. The browser and Node clients are for everything around it: your game's website, a Discord bot, a backend.

## Luau [#luau]

The module runs on the server only. Require it from a Script in ServerScriptService and call `init` once.

### init [#init]

`RoChart.init(config)`

<TypeTable
  type={{
    token: { type: 'string', required: true, description: 'Your project token (rc_…).' },
    host: { type: 'string', default: "'https://app.rochart.co'", description: 'Where to send data.' },
    debug: { type: 'boolean', default: 'false', description: 'Print every batch and failed request in the server output.' },
    flushInterval: { type: 'number', default: '5', description: 'Seconds between sends. A send also starts once 100 events are queued.' },
    heartbeatInterval: { type: 'number', default: '60', description: 'Seconds between server heartbeats (players, FPS, memory, ping).' },
    feedbackButton: { type: 'boolean', default: 'false', description: 'Show a small Feedback button on the right edge of the screen: a 1–5 rating and a text box, sent to Feedback.' },
  }}
/>

### Functions [#functions]

| Function | What it does |
| --- | --- |
| `track(player, event, props?)` | Send an event for a player. Attached to their UserId and current session. |
| `trackServer(event, props?)` | Send an event for this server, not a player (distinct id `"$server:" .. game.JobId`). |
| `setUserProperties(player, props)` | Set properties on the player's profile, such as `level` or `class`. |
| `processReceipt(handler)` | Use instead of setting `MarketplaceService.ProcessReceipt`. Calls your handler with `receiptInfo` and records a `$purchase` when it returns `PurchaseGranted`. |
| `getFlag(player, key)` | A flag's value for the player: `true`, a variant key such as `"test"`, or `nil` when the flag is off or unknown. Records an exposure. |
| `isFeatureEnabled(player, key)` | `true` when the flag is on or returns any variant for the player. Records an exposure. |
| `getConfig(player, key)` | Live config: the payload of the flag's variant for the player (any JSON value), or `nil`. Records an exposure like `getFlag`. |
| `economy(player, { flow, currency, amount, balance?, transactionType?, itemSku? })` | Currency gained (`flow = "source"`) or spent (`"sink"`), like `AnalyticsService:LogEconomyEvent`. `amount` must be above 0. Sends `$economy`. |
| `funnelStep(player, funnel, step, stepName?, funnelSessionId?)` | A step of a funnel, starting at 1. Pass a `funnelSessionId` for funnels a player can go through again, such as a shop visit. Sends `$funnel_step`. |
| `feedback(player, message, { rating?, category?, board? }?)` | Send player feedback to **Feedback** (board `general` unless you say otherwise). At most 3 per player per minute. Returns `true` when it was accepted for sending. |

`getFlag`, `isFeatureEnabled` and `getConfig` yield the first time they're called for a player, then use a 60-second cache. The other functions never yield and never error.

Props are plain tables, sent as JSON. Events are queued and sent in batches. Joins, leaves, server heartbeats with server health, script errors, shutdowns and game pass purchases are sent without any code; see [what it sends on its own](/docs/getting-started/install#what-it-sends-on-its-own) and the [event contract](/docs/game-analytics#where-the-numbers-come-from).

## Browser [#browser]

Every browser method is safe to call before `init`, during server-side rendering, or after opting out: it simply does nothing.

### init options [#init-options]

`rochart.init(token, options?)`

<TypeTable
  type={{
    host: { type: 'string', default: "'https://app.rochart.co'", description: 'Where to send data.' },
    autocapturePageviews: { type: 'boolean', default: 'true', description: 'Capture pageviews on load and on navigation, and page leaves when the tab is closed.' },
    persistence: { type: "'localStorage' | 'cookie' | 'memory'", default: "'localStorage'", description: 'Where the anonymous id and session are kept. memory stores nothing, so every page load is a new visitor.' },
    flushAt: { type: 'number', default: '20', description: 'Send as soon as this many events are queued.' },
    flushInterval: { type: 'number', default: '2000', description: 'Longest time (ms) an event waits before it is sent.' },
    debug: { type: 'boolean', default: 'false', description: 'Log every event and failed request to the console.' },
  }}
/>

### Methods [#methods]

| Method | What it does |
| --- | --- |
| `capture(event, properties?)` | Send an event. |
| `register(properties)` · `unregister(key)` | Add a property to every later event · remove it. |
| `identify(id, $set?, $set_once?)` | Link the visitor to an id, such as their Roblox UserId. See [Players and properties](/docs/getting-started/identify). |
| `setPersonProperties($set, $set_once?)` | Update the current person's properties. |
| `reset()` | Start over as a new anonymous visitor. Call it on logout. |
| `getDistinctId()` · `getSessionId()` | The current ids. |
| `group(type, key, properties?)` | Attach this user and every later event to a group. |
| `resetGroups()` · `getGroups()` | Stop attaching groups · the current groups. |
| `getFeatureFlag(key, { sendEvent? })` | A flag's value. Records an exposure unless `sendEvent: false`. |
| `isFeatureEnabled(key, { sendEvent? })` | Whether a flag is on (or any variant). |
| `getFeatureFlagPayload(key)` | The payload for the user's value. |
| `getAllFlags()` | Every flag value. |
| `onFeatureFlags(callback)` | Runs whenever flags load. Returns an unsubscribe function. |
| `reloadFeatureFlags()` | Fetch flags again. |
| `overrideFeatureFlags(flags \| false)` | Force values locally for testing. `false` clears them. |
| `feedback({ message, board?, rating?, category?, email?, url?, properties?, anonymous? })` | Send feedback. Resolves with its id. See [Feedback](/docs/feedback). |
| `optOut()` · `optIn()` · `hasOptedOut()` | Stop or resume capturing for this browser. |
| `flush()` | Send everything queued now. |
| `shutdown()` | Flush and stop this instance (tests, hot reload). |

For a second project on the same page, create another instance: `new RoChart().init('rc_other_token', { … })`.

## Node / Bun [#node--bun]

`new RoChart({ token, host, … })`

<TypeTable
  type={{
    token: { type: 'string', required: true, description: 'Your project token.' },
    host: { type: 'string', default: "'https://app.rochart.co'", description: 'Where to send data.' },
    flushAt: { type: 'number', default: '50', description: 'Send as soon as this many events are queued.' },
    flushInterval: { type: 'number', default: '5000', description: 'Longest time (ms) an event waits before it is sent.' },
    maxInFlight: { type: 'number', default: '1', description: 'Requests sent at once. Raise it for big imports.' },
    onError: { type: '(error, events) => void', description: 'Called when events are dropped or a flag request fails.' },
  }}
/>

| Method | What it does |
| --- | --- |
| `capture({ distinctId, event, properties?, groups?, $set?, $set_once?, timestamp?, uuid? })` | Send an event. `timestamp` for imports, `uuid` to make re-sends safe. |
| `identify({ distinctId, anonDistinctId?, properties?, $set_once? })` | Set person properties, and optionally link a browser's anonymous id. |
| `setPersonProperties({ distinctId, $set, $set_once? })` | Update person properties. |
| `groupIdentify({ groupType, groupKey, properties? })` | Create or update a group. |
| `getFeatureFlag({ distinctId, key, groups?, personProperties?, groupProperties?, sendEvent? })` | A flag's value. Records an exposure unless `sendEvent: false`. |
| `isFeatureEnabled(…)` · `getFeatureFlagPayload(…)` | Same inputs as `getFeatureFlag`. |
| `getAllFlags({ distinctId, groups?, … })` | Every flag value. |
| `feedback({ message, board?, distinctId?, rating?, category?, groups?, … })` | Send feedback right away. Resolves with its id. |
| `flush()` · `shutdown()` | Send what's queued · send and stop. Await one before the process exits. |

`pending`, `sent` and `dropped` count events, which is handy in import scripts.

## HTTP endpoints [#http-endpoints]

All three use your public project token. The Luau module uses `/i/batch` and `/i/flags`; you can call them from anything that speaks HTTP, including `HttpService`.

### POST /i/batch [#post-ibatch]

Send up to 500 events.

<TypeTable
  type={{
    token: { type: 'string', required: true, description: 'Project token.' },
    batch: { type: 'Event[]', required: true, description: '1–500 events.' },
    sent_at: { type: 'ISO datetime', description: 'When you sent the request, by the device clock. RoChart uses it to fix wrong device clocks. Leave it out for imports.' },
  }}
/>

Each event:

<TypeTable
  type={{
    uuid: { type: 'uuid', required: true, description: 'Unique per event. Reuse it on retries: the same uuid is stored once.' },
    event: { type: 'string', required: true, description: 'Event name, up to 200 characters.' },
    distinct_id: { type: 'string', required: true, description: 'Who did it, up to 200 characters. For Roblox players, the UserId as a string.' },
    timestamp: { type: 'ISO datetime', required: true, description: 'When it happened, with a time zone (Z or ±hh:mm).' },
    properties: { type: 'object', description: 'Any JSON. Use $groups for groups, e.g. { "$groups": { "clan": "c_42" } }.' },
    $set: { type: 'object', description: 'Person properties to overwrite.' },
    $set_once: { type: 'object', description: 'Person properties to set only if missing.' },
  }}
/>

Returns `{ "ok": true, "accepted": n }`. A `400` means the payload is invalid and the whole batch was rejected. A `401` means the token is wrong. Retry `429`, `5xx` and network errors. The body can be up to 5 MB.

Special events work as they do in the SDKs:

- `$identify` with `properties.$anon_distinct_id` links an anonymous id to `distinct_id`.
- `$groupidentify` with `$group_type`, `$group_key` and `$group_set` creates or updates a group.
- `$feature_flag_called` with `$feature_flag` and `$feature_flag_response` records a flag exposure.

### POST /i/flags [#post-iflags]

```json
{ "token": "rc_…", "distinct_id": "123456789", "person_properties": { "platform": "Phone" } }
```

Returns `{ "flags": { "short-tutorial": "short", "double-xp-weekend": true }, "payloads": {} }` with every flag that's on. `groups` is needed for flags that match by group. `person_properties` and `group_properties` add to the stored properties for this check only. This doesn't record exposures.

### POST /i/feedback [#post-ifeedback]

```json
{ "token": "rc_…", "message": "The boss fight is too hard on mobile", "distinct_id": "123456789", "rating": 5, "category": "difficulty" }
```

Returns `{ "ok": true, "id": "…", "board": "general" }`. All fields are on the [Feedback](/docs/feedback#send-feedback) page.
