Docs
Send events from anywhere that can speak HTTPS. Read these in order if you're new; jump to a section if you came here looking for one specific thing.
Using an AI assistant? Point it at owlsignal.dev/llms.txt — the whole integration brief
(wire format, naming conventions, server-computed signals, SDK APIs, privacy rules) as one plain-text
fetch, generated from the same source this page describes.
Quickstart
Three steps:
- Sign up — free, no card. Your first app + API key are generated by the onboarding wizard.
- Drop the SDK (or a curl) into your build, point it at the app's key, and ship a single event.
- Watch it appear in the dashboard within seconds. From there, define a loop or wait for users.
One-shot from a terminal
curl -X POST https://owlsignal.dev/api/v1/events \
-H "Authorization: Bearer pk_PASTE_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"events":[{
"build_version":"0.1.0",
"session_id":"00000000-0000-0000-0000-000000000001",
"player_hash":"0123456789abcdef0123456789abcdef",
"ts":"2026-05-06T12:34:56Z",
"type":"session",
"name":"Session:Start"
}]}' Wire format
POST /api/v1/events with a JSON body containing an events array (1–100 events per batch). Every event has:
| Field | Type | Notes |
|---|---|---|
build_version | string | 1–64 chars. Free-form (e.g. 1.4.0). |
session_id | uuid | Stable per session. Same id ties events to one session row. |
player_hash | string | 32–64 chars. Anonymous device hash. No PII. |
ts | ISO 8601 | Client clock. Server stamps separately too. |
type | enum | One of progression, design, resource, error, session. |
name | string | 1–100 chars. Convention: Verb:Subject e.g. Start:Run, CTA:Clicked. |
value | number? | Optional numeric (duration, score, currency delta). |
dim01..dim03 | string? | Optional dimensions (≤64 chars). Shown in Live and session views, and countable as distinct values in a funnel stage ("built 5 different units"). |
data | object? | Free-form key/value (string|number|boolean|null). Top-level only. |
Response is always 200 with { accepted, rejected, errors }. Bad events in a batch are rejected
but never fail the whole request — the SDK should never block the game.
Event types
Pick the one that best matches the moment. Type is purely a categorization signal — the
dashboard groups things by it, but you can model nearly anything with design if unsure.
sessionSession:Start/Session:End. Used to compute session duration. The SDK auto-fires these — you almost never call them by hand.progression- Phase markers:
Start:Run,Complete:Tutorial,Fail:Boss. These are what funnel stages match on. design- Gameplay or UX signals that aren't pure phase markers:
FirstTime:Combo,CTA:Clicked,Friction:DeadEnd. Default for "interesting moment, not sure what to call it." resource- Soft-currency tracking. Convention:
Source:Goldwith positive value,Sink:Goldwith positive value (the verb encodes direction). error- Exceptions / critical issues. Powers the Errors page. Optional
data.message,data.stack,data.scene,data.signature— signature groups identical errors.
Web SDK (JS/TS)
Install from npm, then initialize once near the root of your client bundle:
npm install @owlsignal/web
import { OwlsignalClient } from '@owlsignal/web';
OwlsignalClient.Initialize({
apiKey: 'pk_live_...',
buildVersion: '1.4.0',
// endpointUrl is optional — defaults to https://owlsignal.dev/api/v1/events
});
// Then anywhere a meaningful moment happens:
OwlsignalClient.TrackProgression('Complete:Tutorial');
OwlsignalClient.TrackDesign('CTA:Clicked', { dim01: 'pricing-pro' });
OwlsignalClient.TrackResource('Source:Coins', 50); // value is POSITIONAL
OwlsignalClient.TrackError('Error:Web:CheckoutFailed', { data: { stage: 'payment' } });
// Optional: identify the user (still anonymous — this is just a stable hash).
OwlsignalClient.IdentifyUser('user-abc');
// Optional: respect a user opt-out flag.
OwlsignalClient.SetOptOut(true);
Auto-events
Once initialized, the SDK fires these for you — never instrument them yourself:
Session:Starton init.Session:Endonpagehide(sent viafetch keepalive).Error:Web:Unhandledon a globalerror, andError:Web:UnhandledRejectionon an unhandled promise rejection.
The web SDK deliberately does not fire Client:Foregrounded / Client:Backgrounded —
browser tab focus fires constantly (alt-tab, dev tools) and drowns out the real signal. Those
are Unity-only. Turn the rest off with autoTrackLifecycle: false / autoTrackErrors: false.
Events are batched (every 30s or 30 events, whichever first). 5xx responses retry with exponential backoff; 4xx drops the batch.
Stateless mode (no consent banner)
By default the SDK keeps its anonymous hash in localStorage under owlsignal:player_hash, so a returning visitor is recognised across
sessions. Pass persistPlayerHash: false and it writes nothing to the device — the hash lives in memory for that page load only.
OwlsignalClient.Initialize({
apiKey: 'pk_live_...',
persistPlayerHash: false // store nothing on the visitor's device
}); Why you'd want it: the EU ePrivacy Directive (Art. 5(3)) governs storing or accessing information on a user's device. It isn't a rule about cookies
specifically, and it isn't a rule about personal data — so "it's only local storage" and "it's
anonymous" are both the wrong test, and an analytics ID in localStorage is in scope on the same terms a cookie would be. Writing
nothing is what actually takes you out of scope. That's how we run owlsignal.dev itself, and why this site has no consent banner.
What it costs: every page load looks like a new visitor, so cross-session
retention goes away — D1/D7 cohorts, SessionCount:N and ActiveDays:N stop being
meaningful. Anything inside one session — funnels, drop-off, errors — is unaffected. The opt-out
flag still uses localStorage either way: storing a choice someone made in
order to honour it is the textbook strictly-necessary case.
Not legal advice — your DPO's call. Defaults are chosen for measurement; this switch is there when your compliance posture asks for it.
Unity SDK
Install via the Package Manager: Window → Package Manager → + → Install package from git URL… and paste the URL below (append #v0.1.0-alpha.1 to pin a release — what
you want for a shipping build). Shape-equivalent to the web SDK, with C# named arguments:
https://github.com/zisomediadev/owlsignal-unity.git
using Owlsignal;
OwlsignalClient.Initialize(apiKey, endpointUrl, buildVersion);
OwlsignalClient.TrackProgression("Complete:Tutorial");
OwlsignalClient.TrackDesign("FirstTime:Action");
OwlsignalClient.TrackResource("Purchase:Made", value: 19.0); // value is a NAMED arg
OwlsignalClient.TrackError("Error:Unity:SaveCorrupt");
OwlsignalClient.Flush(); // force-send; also called automatically on quit
Auto-events
Session:Starton init,Session:Endon quit.Client:Foregrounded/Client:Backgroundedon focus change (Unity only).Error:Unity:Unhandledon an unhandled C# exception — carriesmessage,stack_top,scene.
Godot SDK
Grab the latest zip from owlsignal-godot releases, copy its addons/owlsignal/ into your project's addons/, then enable it (Project → Project Settings → Plugins) — that registers OwlsignalClient as an autoload, callable from any script.
OwlsignalClient.initialize({
"api_key": "pk_live_...",
"build_version": "1.4.0",
})
OwlsignalClient.track_progression("Complete:Tutorial")
OwlsignalClient.track_design("CTA:Clicked", { "dim01": "main-menu-play" })
OwlsignalClient.track_resource("Source:Coins", { "value": 50.0 }) # value in the opts Dictionary
OwlsignalClient.track_error("Error:Godot:SaveCorrupt")
OwlsignalClient.flush()
Auto-events
Session:Starton init,Session:Endon window close.Client:Foregrounded/Client:Backgroundedon focus change (backgrounding also flushes).- Current scene name attached to every event as
screen.
No automatic crash event. GDScript exposes no global hook for unhandled script
errors, so unlike Unity/Unreal there is no Error:Godot:Unhandled —
report failures you care about yourself with track_error("Error:Godot:…").
Unreal SDK
Clone it straight into your project's Plugins/ folder (or download a
zip from releases), let the editor compile it, enable it, then configure once from your GameInstance:
git clone --depth 1 https://github.com/zisomediadev/owlsignal-unreal.git Plugins/Owlsignal
#include "OwlsignalSubsystem.h"
UOwlsignalSubsystem* Owlsignal = GetGameInstance()->GetSubsystem<UOwlsignalSubsystem>();
FOwlsignalInitOptions Options;
Options.ApiKey = TEXT("pk_live_...");
Options.BuildVersion = TEXT("1.4.0");
Owlsignal->Configure(Options);
Owlsignal->TrackProgression(TEXT("Complete:Tutorial"), FOwlsignalTrackOptions());
Owlsignal->TrackResource(TEXT("Purchase:Made"), FOwlsignalTrackOptions::WithValue(19.0));
Owlsignal->TrackError(TEXT("Error:Unreal:SaveCorrupt"), FOwlsignalTrackOptions());
Auto-events
Session:Starton configure,Session:Endon GameInstance shutdown.Client:Foregrounded/Client:BackgroundedviaFCoreDelegates.Error:Unreal:Unhandled— the SDK watchesGLogfor Error/Fatal (where failed checks and ensures surface), rate-limited to one per second.- Current level name attached as
screen(PIE prefix stripped).
Server-computed signals
These look like event names but are derived server-side from a player's own timeline. You never fire them — use them as loop stage names and Owlsignal works them out from the events you already send.
Day:N:Returned- Came back on exactly day N after first play (classic N-day retention). Not the same as "within N days".
Day:N:ReturnedWithin- Came back on any day 1…N — use this for "did they return within a week?".
SessionCount:N- Reached N sessions.
SessionDuration:N:minutes/:hours- Total playtime ≥ N.
ActiveDays:N- Active on ≥ N distinct days.
There is no generic Error:Unhandled event — crashes arrive under the
real per-platform names above (Error:Unity:Unhandled, Error:Web:Unhandled). A stage can list several event names; they're
OR-matched.
Authentication
Send the API key in the Authorization header:
Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Each key is scoped to one app inside one tenant. Generate keys in Settings → API keys. We store only a hash — the raw key is shown once when you generate it. Lost keys can be revoked + regenerated; we can't recover the raw value.
Rate limits & tier behavior
If your tenant has exceeded its monthly events budget, ingest silently drops the batch and returns 200 with { accepted: 0, rejected: N, reason: "tier_limit" }. The SDK never
sees a 5xx for tier reasons; your game/app keeps working. The dashboard surfaces the cap; we
email at 80% and 100% of budget.
Server-side validation rejects bad events individually (the rest of the batch goes through). The response always lists which indexes were rejected and why.
Privacy & opt-out
Events carry an anonymous player_hash only — no email, no IP, no real
identity. Opt-out is a client-side contract: when the user toggles "telemetry off", call OwlsignalClient.SetOptOut(true) (web) or the Unity equivalent. The SDK then drops events at the source — the server can't detect opt-out
because there's no identity to recognize.
Anonymous is not the same as storing nothing. By default the web SDK keeps its hash in localStorage, which is storage on your visitor's device and therefore
yours to disclose — being anonymous doesn't exempt it. If that's a problem for your compliance
posture, stateless mode writes nothing at all.
Data residency: Postgres in eu-central-1 (Frankfurt), functions in fra1 (Frankfurt). See privacy for the full posture.