# Owlsignal > Privacy-first, EU-hosted product and game analytics. Anonymous by design: no > PII, no emails, no IPs. Ship events over HTTPS from an SDK or plain curl, then > build funnels ("loops") and retention from them. Docs: https://owlsignal.dev/docs Ingest: POST https://owlsignal.dev/api/v1/events Auth: Authorization: Bearer pk_... (one key per app; Settings -> API keys) ## Wire format POST /api/v1/events with {"events": [...]} — 1-100 events per batch, max 256 KB. Every event: build_version string 1-64 chars (e.g. "1.4.0") session_id uuid stable for one session player_hash string 32-64 chars, anonymous device hash. NEVER put PII here. ts string ISO 8601 client timestamp type enum progression | design | resource | error | session name string 1-100 chars, colon-hierarchical (see conventions below) value number optional (score, duration, currency delta) dim01,dim02,dim03 string optional, <=64 chars — put DYNAMIC values here screen string optional, <=256 chars (SDKs auto-capture scene/pathname) data object optional, primitive values only (string|number|bool|null) The response is ALWAYS 200: { accepted, rejected, errors }. A bad event never fails the batch and ingest never returns 5xx for quota — over budget you get 200 { accepted: 0, rejected: N, reason: "tier_limit" } and events are dropped silently. Never block the game on analytics. ## Event naming conventions (events YOU send) - Progression uses Action:Object[:Detail], verb FIRST — Start:X / Complete:X / Fail:X (Start:Tutorial, Complete:Run, Fail:Match). - First-time activation: FirstTime:X (FirstTime:Action). - Monetization: Purchase:Made, Purchase:Premium. - Negative signals: Friction:X. Manual/expected errors the dev reports themselves: Error:: (e.g. Error:Web:CheckoutFailed). - Auto-fired by the SDK — NEVER ask the dev to instrument these: Session:Start, Session:End (all SDKs); Client:Foregrounded, Client:Backgrounded (Unity, Godot and Unreal — web deliberately skips them, browser tab focus is too noisy); crashes as Error:Unity:Unhandled, Error:Unreal:Unhandled, and Error:Web:Unhandled + Error:Web:UnhandledRejection. Godot has NO auto crash event (GDScript exposes no global error hook) — a Godot dev reports failures manually as Error:Godot:X. - There is NO generic "Error:Unhandled" event. For a "did players hit a crash?" stage, OR-match the real per-platform names above (a stage matches its eventNames with OR). - Colon-separated PascalCase segments, ≤5 segments. Put DYNAMIC values (biome, level id, score) in properties (dim01–03 / value), NEVER in the event name — that keeps cardinality governable. ## Server-computed signals (DO NOT instrument these) These look like event names but are derived server-side from the player's own timeline. Never fire them from an SDK — use them only as funnel stage names. - Day:N:Returned — came back on EXACTLY day N after first play (classic N-day retention). NOT "within N days". - Day:N:ReturnedWithin — came back on ANY day 1..N. Use this whenever the dev says "within N days" / "within a week". - SessionCount:N — reached N sessions. - SessionDuration:N:minutes or SessionDuration:N:hours — total playtime ≥ N. - ActiveDays:N — active on ≥ N distinct days. ## Web SDK (JS/TS) — published npm install @owlsignal/web import { OwlsignalClient } from '@owlsignal/web'; OwlsignalClient.Initialize({ apiKey: 'pk_live_...', buildVersion: '1.4.0' }); OwlsignalClient.TrackProgression('Complete:Tutorial'); OwlsignalClient.TrackDesign('CTA:Clicked', { dim01: 'pricing-pro' }); OwlsignalClient.TrackResource('Purchase:Made', 19.0); // value is POSITIONAL OwlsignalClient.TrackError('Error:Web:CheckoutFailed', { data: { stage: 'pay' } }); OwlsignalClient.IdentifyUser('user-123'); // still anonymous; stored alongside OwlsignalClient.SetOptOut(true); // honour your telemetry toggle await OwlsignalClient.Flush(); // force-send before a redirect Auto-fired (write no code): Session:Start, Session:End, Error:Web:Unhandled, Error:Web:UnhandledRejection. Web does NOT fire Client:Foregrounded / Client:Backgrounded — browser tab focus is too noisy to be a useful signal. Stateless mode — store nothing on the visitor's device: OwlsignalClient.Initialize({ apiKey: 'pk_live_...', persistPlayerHash: false }); Default is true: the anonymous hash is kept in localStorage under owlsignal:player_hash so returning visitors are recognised. With false it lives in memory for the page load only and nothing is written. WHY: ePrivacy Art. 5(3) covers storing/accessing information on a device. It is not a rule about cookies, and not a rule about personal data — so neither "it's only localStorage" nor "it's anonymous" exempts you. Writing nothing does. owlsignal.dev runs this way, which is why it shows no consent banner. COST: no cross-session retention (D1/D7 cohorts, SessionCount:N, ActiveDays:N). Same-session funnels, drop-off and errors are unaffected. The opt-out flag still uses localStorage in either mode — storing a choice in order to honour it is strictly necessary. ## Unity SDK (C#) — UOwlsignal package Install: Unity Package Manager -> + -> "Install package from git URL": https://github.com/zisomediadev/owlsignal-unity.git Append #v0.1.0-alpha.1 to pin a release (do this for shipping builds). 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(); // also called automatically on quit Auto-fired (write no code): Session:Start, Session:End, Client:Foregrounded, Client:Backgrounded, Error:Unity:Unhandled. ## Godot SDK (GDScript) — autoload named OwlsignalClient Install: copy addons/owlsignal/ from https://github.com/zisomediadev/owlsignal-godot into your project's addons/, then enable it in Project Settings -> Plugins. OwlsignalClient.initialize({ "api_key": "pk_live_...", "build_version": "1.0.0" }) OwlsignalClient.track_progression("Complete:Tutorial") OwlsignalClient.track_design("FirstTime:Action") OwlsignalClient.track_resource("Purchase:Made", { "value": 19.0 }) # value in the opts Dictionary OwlsignalClient.track_error("Error:Godot:SaveCorrupt") OwlsignalClient.flush() Auto-fired (write no code): Session:Start, Session:End, Client:Foregrounded, Client:Backgrounded. IMPORTANT: Godot has NO auto crash event — GDScript exposes no global unhandled-error hook. Report failures yourself with track_error("Error:Godot:X"). ## Unreal SDK (C++) — UOwlsignalSubsystem Install: git clone --depth 1 https://github.com/zisomediadev/owlsignal-unreal.git Plugins/Owlsignal then let the editor compile it and enable it under Edit -> Plugins -> Analytics. #include "OwlsignalSubsystem.h" UOwlsignalSubsystem* Owlsignal = GetGameInstance()->GetSubsystem(); FOwlsignalInitOptions Options; Options.ApiKey = TEXT("pk_live_..."); Options.BuildVersion = TEXT("1.0.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()); Owlsignal->Flush(); Auto-fired (write no code): Session:Start, Session:End, Client:Foregrounded, Client:Backgrounded, Error:Unreal:Unhandled (observed from Error/Fatal logs). ## Privacy rules an integration MUST follow - Never send names, emails, IPs, social handles, or any reversible identifier. player_hash is an anonymous device hash; keep it that way. - Put dynamic values (level id, biome, score) in value / dim01-dim03 / data — never interpolate them into the event name (it makes cardinality ungovernable). - Opt-out is a client-side contract: when the user disables telemetry, call SetOptOut(true) and the SDK stops sending at the source. - Data at rest lives in eu-central-1 (Frankfurt); functions run in fra1.