Skip to main content

Unity SDK Integration

The Unity SDK wraps the native Joryio iOS and Android SDKs. It adds no tracking logic of its own: every call forwards to the native SDK, so behaviour, batching and offline handling are identical to a native integration.

Requirements​

  • Unity 2022.3 LTS or newer. Unity 2021.3 and 2022.1 build Android with JDK 8 and cannot compile against the native Android SDK (Java 11 bytecode).
  • iOS 14.0+ / Android 6.0+ (API 23)
  • Android: Target API Level 34 or higher (Player Settings). The native SDK's AndroidX libraries require compileSdk 34, and Unity's Android build fails below it.
  • Android: External Dependency Manager for Unity (EDM4U). iOS does not use it (see Installation).
  • iOS: Xcode 15 or newer, with network access to github.com when the exported project is first built - Xcode downloads the native SDK as a Swift package.
  • For Android push: Firebase Cloud Messaging in the game - the Firebase Unity SDK's Messaging package and your google-services.json. The native Android SDK compiles against firebase-messaging but does not bring it; without it, analytics and in-app messaging work and push stays off.
Platform support

iOS and Android only. On other platforms - the Editor, desktop, consoles - every method exists and does nothing, and the SDK logs one warning per session. Your game code never needs #if UNITY_IOS || UNITY_ANDROID around a Joryio call.

Installation​

Add the package (com.joryio.sdk) from Git: in the Package Manager choose + → Add package from git URL and enter the URL with the release tag:

https://github.com/joryio/joryio-unity-sdk.git#1.3.3

To update, change the tag to the new version; always pin one, because without a tag the Package Manager follows the newest commit.

Or download joryio-unity-<version>.unitypackage from the GitHub releases page and import it with Assets → Import Package → Custom Package. The SDK lands under Assets/JoryioSDK; to update, delete that folder and import the new version.

Use one install method, not both. The Git URL package and Assets/JoryioSDK carry the same assemblies (Joryio.Runtime, Joryio.Editor) and the same asset GUIDs, so a project with both does not compile. To switch, remove the one you have first: delete Assets/JoryioSDK, or remove the Joryio package in the Package Manager. The .unitypackage brings the licence terms (LICENSE), README.md and CHANGELOG.md into Assets/JoryioSDK as well.

The package is not on OpenUPM, which lists open-source packages only; the Joryio SDKs are licensed commercially.

The package ships no prebuilt native binaries, so shared libraries are reconciled with whatever else your project already uses. It brings the UI-free native SDKs - tracking, identity and push, but not the native in-app message views (see In-app messages):

  • Android: let EDM4U resolve io.joryio:joryio-android through Gradle, from the package's dependency manifest (Editor/JoryioDependencies.xml).
  • iOS: nothing to resolve in Unity. When you build for iOS, the package adds the native SDK to the exported Xcode project as a Swift package (https://github.com/joryio/joryio-ios-sdk, at exactly the package's version) and links its Joryio product into UnityFramework. Xcode downloads and resolves it the first time it opens or builds the project. Neither CocoaPods nor EDM4U is involved on iOS.
Upgrading from a version that used CocoaPods

Earlier versions took the iOS SDK from CocoaPods (the Joryio/Core pod), and CocoaPods trunk stops accepting new versions on 2 December 2026. After upgrading, build the iOS player with Replace rather than Append once, so the exported project no longer carries the old pod, and delete any <iosPod name="Joryio/..."> line from your own EDM4U dependency files.

Quick start​

Each platform has its own SDK key (jry_sdk_ios_..., jry_sdk_android_...), from Settings → Apps in the dashboard. Use the API host shown for your workspace.

using Joryio.Runtime;

void Awake()
{
#if UNITY_IOS
const string sdkKey = "jry_sdk_ios_YOUR_IOS_KEY";
#else
const string sdkKey = "jry_sdk_android_YOUR_ANDROID_KEY";
#endif
JoryioSDK.Initialize(sdkKey, "api-eu1.joryio.com");
}

Then track what players do:

JoryioSDK.Track("level_complete", new Dictionary<string, object> {
{ "level", 7 },
{ "duration_seconds", 143.5 },
});

JoryioSDK.Identify("player-1234");
JoryioSDK.SetAttribute("favourite_class", "mage");
JoryioSDK.IncrementAttribute("games_played");

// Another identifier for the same player - works before Identify too
JoryioSDK.AddAlias("steam_id", "76561198000000000");

Values keep their type across the boundary: { "level", 7 } is stored as the number 7, not "7". Segments compare numerically, so a stringified number would match nobody. Every numeric type is kept, nested dictionaries and lists stay objects and arrays, DateTime is sent as ISO-8601 in UTC, and inside event properties NaN or infinity becomes null. As an attribute value (SetAttribute, SetAttributes, IncrementAttribute) NaN or infinity is ignored with a warning and the attribute is left unchanged - it is not unset.

No call throws into your game: a call before Initialize, a null required argument or a native failure is logged and ignored. A null attribute value is not an error: SetAttribute("clan", null) unsets the attribute, the same as UnsetAttribute("clan"), on iOS and Android alike.

The exception is consent: OptOut(), OptIn() and UpdateConsent() work before Initialize. The decision is applied when the SDK starts, before anything is sent. IsUserOptedOut() also answers before Initialize - see Consent. WipeData() and GetTrackingConsent() work before Initialize too, and so do SetSdkAuthenticationToken and SetSdkAuthenticationErrorHandler - see SDK authentication. So do subscription changes and UnregisterPush(): SetSubscription, AddToSubscriptionGroup, RemoveFromSubscriptionGroup and UnregisterPush() made before Initialize are held, kept on the device across a restart, and sent once the SDK starts. A newer change of the same channel or group replaces an older one, so the player's last choice is the one sent. A withdrawal (an unsubscribe, leaving a group, UnregisterPush()) is always sent: an OptOut(), a Denied consent or WipeData() before Initialize discards held subscribes, never withdrawals, and a withdrawal made before a WipeData() still goes out for the player who made it, not for the new identity the wipe creates. A RegisterPushToken before Initialize is not sent, and it does not cancel a held UnregisterPush(). A subscribe, an opt-in or a group join made before Initialize while the player is opted out or consent is Denied (stored from an earlier launch or set in this one) is not held at all: it is dropped with a warning, and a later OptIn() or Granted does not send it.

Configuration​

Every setting is optional and mirrors the native SDKs' option of the same name. Durations are seconds and say so in their name, because the natives disagree (iOS takes seconds, Android milliseconds) - the bridges convert.

JoryioSDK.Initialize(sdkKey, "api-eu1.joryio.com", new JoryioConfig {
Debug = UnityEngine.Debug.isDebugBuild,
FlushIntervalSeconds = 10,
AnonymousId = existingDeviceId, // only if you already have one
});
PropertyTypeDefaultDescription
DebugboolfalseVerbose native logging. The native log level follows it: debug when true, errors only otherwise.
BatchSizeint50Events per batch before an automatic flush.
FlushIntervalSecondsdouble5Seconds between automatic flushes.
SendImmediatelyboolfalseSend each event as soon as it is tracked, without batching (not recommended for production).
MaxQueueSizeint1000Most events the local queue holds while flushes fail; beyond it the oldest are dropped.
RequestTimeoutSecondsdouble10Per-request HTTP timeout.
SessionTimeoutSecondsdouble1800Seconds of inactivity before a new session starts.
TrackSessionStartbooltrueEmit a session start event automatically.
AnonymousIdstringgeneratedYour own anonymous id for this device. Used for this run and stored only when nothing is stored yet, so an id set once survives the game dropping the option later.
RequestPushPermissionAtLaunchboolfalseAsk for the push permission during Initialize. See below for why the default is off.
MaxRetriesint3Retries per request before it waits for the next flush.
RetryBackoffMsint1000Base delay between retries, in milliseconds (the native SDKs' name).
TrackingConsentTrackingConsent?null (not set)Your consent screen's answer at launch. Not set keeps the answer the SDK remembered (a stored Denied stays denied) and otherwise tracks normally. Granted tracks and replaces a stored denial; Pending sends nothing until UpdateConsent(TrackingConsent.Granted), for this launch only; Denied collects nothing and is remembered. See Consent.
OptOutboolfalseStart opted out. An opt-out is stored, so false does not undo an earlier OptOut().
NotificationChannelNamestringnull ("Notifications")Android only. The notification channel's name in the system's notification settings (Android 8+); pass it localised. iOS has no channels and ignores it.
EnableSdkAuthenticationboolfalseSend a signed token with requests that name a user. See SDK authentication.
SdkAuthenticationTokenstringnoneThe token to start with, if you already have one.
AllowHtmlJsInAppMessagesboolfalseLet the native UI draw HTML messages. Scripts in them do not run; the native SDK's bridge handles buttons and forms. No effect on messages your own handler renders.
HoldInAppMessagesForHandlerboolfalseSince 1.3.2. Your game draws in-app messages with its own handler: hold messages that are ready before SetInAppMessageHandler is called. See Before your handler is set.

Debug sets both the native log level and the native debug mode on iOS and Android, which also unlocks the testing calls ResetDisplayedCampaigns and EvaluateInAppCampaigns.

Push notifications​

If your game has no push stack yet, the SDK registers the device itself - on iOS always, and on Android when Firebase Cloud Messaging is in the game (see Requirements). Without Firebase on Android there is no FCM token, so nothing is registered and push stays off; analytics and in-app messaging are unaffected. Ask for the permission when the player has a reason to say yes - a prompt in the first seconds of a game is the one most often denied, and a denial is much harder to reverse than a delay.

JoryioSDK.RequestPushPermission(granted => {
Debug.Log(granted ? "Push enabled" : "Player declined");
});

The callback runs on the main thread, exactly once. On iOS the SDK then receives the APNs device token by itself - there is no app delegate to edit.

On Android the prompt is shown from your game's activity: GameActivity (the Unity 6 default) or the classic UnityPlayerActivity (earlier versions, or a Unity 6 project that selects it). Both report the player's answer once the system dialog closes. Below Android 13 there is no runtime prompt, and the callback answers at once with whether notifications are enabled.

If you would rather the SDK ask during Initialize - a soft-launch build, a title where notifications are the point, or just to see the flow - turn it on in the config:

JoryioSDK.Initialize(sdkKey, "api-eu1.joryio.com", new JoryioConfig {
RequestPushPermissionAtLaunch = true,
});

It is off by default rather than unavailable. The prompt is one-shot on both platforms, so the default protects the common case; this is here for when you have decided otherwise.

If you already use Firebase or Unity Mobile Notifications​

Most games do, and two push stacks competing for the same token is the usual way an integration breaks. Keep your existing one and hand us the token - the APNs device token on iOS, the FCM registration token on Android. On Android that token comes from Firebase Messaging (or your own FCM wrapper): Unity Mobile Notifications does not produce an FCM token on Android - it schedules local notifications there - so it can supply the token on iOS only.

JoryioSDK.RegisterPushToken(tokenFromYourExistingStack);

Add a callback to hear how it went - isRetryable means a transient failure (offline, DNS, 5xx) worth registering again, isUnauthorized means the SDK key is wrong:

JoryioSDK.RegisterPushToken(tokenFromYourExistingStack, result =>
{
if (!result.success && result.isRetryable)
{
// transient (offline, DNS, 5xx): register again when the network is back
}
});

On Android the SDK then never touches the FCM service. On iOS two things still happen, so nothing surprises you:

  • Initialize makes the SDK the UNUserNotificationCenter delegate and forwards every callback to the delegate that was installed before it. Initialize Joryio after your other push plugin sets its delegate (Unity Mobile Notifications does so at launch, so Awake is fine). A plugin that sets the delegate later replaces Joryio's, and open tracking then depends on your TrackPushClick call.
  • The SDK receives the APNs token from Unity's app controller itself. Passing the same token to RegisterPushToken as well is harmless - unless the player has turned push off, because RegisterPushToken lifts an UnregisterPush() (see below).

RegisterPushToken turns push back on for this device: it lifts an earlier UnregisterPush(). Firebase's OnTokenReceived fires by itself at every start, so if you forward it, skip it while the player has turned push off in your settings, and call RegisterPushToken when they turn it back on. Otherwise a token refresh, not the player, undoes their choice.

UpdateBadgeCount(count) and ClearBadge() set the app icon badge on iOS. They do nothing on Android, which has no badge API of its own.

In-app messages​

The dependency manifest pulls the UI-free native SDKs, so out of the box nothing draws in-app messages: until you install a handler or add the native UI, campaigns reach the device and are not shown. Choose one of the two.

Render them in-engine with your own art (recommended for games) - install a handler:

JoryioSDK.SetInAppMessageHandler(message => {
// Title / Body / ImageUrl / Buttons are the authored content.
// Name is the campaign's dashboard label - never copy to show a player.
myCanvas.Show(message.Title, message.Body, message.ImageUrl);
foreach (var button in message.Buttons)
myCanvas.AddButton(button.Text, () => {
message.MarkClicked(button);
if (button.Action == "url" || button.Action == "deep_link")
Application.OpenURL(button.Url);
else if (button.Action == "request_push_permission")
JoryioSDK.RequestPushPermission(granted => { });
myCanvas.Hide();
message.MarkDismissed(); // the tap closed the message
});
message.MarkDisplayed();
}, canRenderHtml: false);
You are now responsible for reporting

Installing a handler stops the SDK drawing, and from that point it cannot see what happened. Call MarkDisplayed() when the message appears, MarkClicked(button) when a button is tapped and MarkDismissed() when the message closes - a tap that closes it is reported as both, clicked then dismissed, as the native SDKs do when they draw it. MarkDisplayed() reports once per message however often it is called, and MarkClicked(button) sends the button's Id as the click's elementId so the click map can tell buttons apart. A button without an id sends no elementId - never its label, which is display copy.

A message that is never reported counts as never shown: no display in reporting, nothing for revenue attribution to join on, and frequency caps that never count it. The campaign then reads as delivered to nobody, which looks like a targeting problem rather than an integration one. The SDK logs a warning if a message is discarded unreported.

To hand rendering back to the native SDK - when your renderer is destroyed, or a scene has no UI to draw on - remove the handler:

JoryioSDK.ClearInAppMessageHandler(); // the same as SetInAppMessageHandler(null)

The native SDK then draws messages with its own views again (if its UI artifacts are in the build - see below) and reports its own capabilities to the server. This is the native SDKs' clearInAppMessageCallback.

From a component that may not be the only renderer - a renderer per scene, say - pass the handler you installed. It is removed only if it is still the installed one, never a handler another script installed after it:

void OnDestroy() => JoryioSDK.ClearInAppMessageHandler(myHandler);

canRenderHtml describes your renderer. A plain Unity canvas cannot display HTML content, which needs a web view. This is not cosmetic: campaigns are targeted on what the device reports, and an undeclared capability is read as "cannot", not "unknown". Claiming HTML you cannot draw sends campaigns to players who will never see them; omitting HTML you can draw withholds campaigns you could have shown.

If your web view renders an HTML message with a <form data-jry-form>, submit it through the SDK so the values are written to this player's profile. Values are strings, or a list of strings for a checkbox group:

JoryioSDK.SubmitInAppForm(message.Id, new Dictionary<string, object> {
{ "email", "[email protected]" },
{ "topics", new List<string> { "events", "offers" } },
}, result => {
if (!result.ok) Debug.Log(result.message);
});

The callback always runs once; any failure, including no network, is ok = false.

Before your handler is set​

Since 1.3.2. SetInAppMessageHandler can only be called after Initialize, and the first sync can finish before it - the native SDK, if its UI is in the build, then draws that message itself. Set HoldInAppMessagesForHandler = true in JoryioConfig to prevent that: a message that is ready before your handler is set is held - not drawn, not reported, nothing counted against a frequency cap - and handed to your handler the moment you set it. ClearInAppMessageHandler() instead ends the hold and the native SDK draws the held message.

Or let the native SDK draw them​

To have the native SDK draw messages itself - an OS-styled overlay over your game, reported for you, no code - add its UI artifacts.

Android: add the UI artifact in an EDM4U dependencies file of your own, for example Assets/Editor/JoryioUIDependencies.xml, keeping the version equal to the package's:

<dependencies>
<androidPackages>
<androidPackage spec="io.joryio:joryio-android-ui:1.3.3" />
</androidPackages>
</dependencies>

iOS: add the scripting define symbol JORYIO_IOS_UI under Player Settings → iOS → Other Settings → Scripting Define Symbols. The iOS build then links the Swift package's JoryioUI product as well, at the package's version.

A handler installed with SetInAppMessageHandler still takes over from the native views.

What a message carries​

The handler receives everything the server sent, under the same field names the React Native SDK uses (it used to receive only Id and Name, which left a game nothing to draw but the campaign's internal label):

FieldSet whenMeaning
Id, Name, TypealwaysCampaign id, dashboard name (not copy to show), and modal / banner / slideup / fullscreen / custom.
Kindalwaysnative or html - which of the groups below is populated. IsNative / IsHtml are shortcuts.
Title, Body, ImageUrlnativeThe authored text and image. Title and ImageUrl may be null; Body is always set.
ButtonsnativeNever null. Each has Id, Text, Action (dismiss, url, deep_link, request_push_permission) and Url for the two link actions. The SDK cannot act on a button you draw: open the URL yourself, call RequestPushPermission for the primer action, and report the tap with MarkClicked(button).
CloseButton, BackdropDismissiblenativeWhether the author asked for a close affordance and whether tapping outside should dismiss.
StylenativeThe author's presentation overrides, or null when the campaign set none. BackgroundColor, TextColor, PrimaryButtonColor, PrimaryButtonTextColor (CSS colour strings), CornerRadius, FontSize (body size - scale the headline from it), TitleWeight (regular / medium / semibold / bold), TextAlign (auto / start / center / end - auto follows the message's own language, not the device locale) and FontFamily. Every one is optional and an absent one means inherit your game's own styling; apply the ones that are present and do not substitute defaults for the rest. FontFamily is best effort - apply it only if your game ships that font. Same fields and names as the React Native SDK's message.style.
Html, CsshtmlThe document. A uGUI canvas cannot render it - only declare canRenderHtml: true if your game hosts a web view.

What Unity cannot render, stated plainly. HTML content needs a web view the engine does not provide. Images are URLs: download them yourself (UnityWebRequestTexture). The author's style overrides arrive in Style, but nothing applies them for you - a canvas has no theme to inherit from, so your renderer reads the fields it supports and keeps its own styling for the rest.

Start from the sample​

The package ships an In-app renderer sample: a scene, a restylable prefab and the script that wires them together. Import it from the Package Manager (select the Joryio package, open Samples, click Import), put your SDK keys on the JoryioInApp object in JoryioInAppSample.unity, and build to an iOS or Android device. Messages arrive only in a device build: in the Editor the SDK is a no-op, so pressing Play shows nothing.

Installed from the .unitypackage? There is no Package Manager entry to import from: download joryio-unity-<version>-sample.unitypackage from the same releases page and import it the same way. It lands in Assets/Samples/Joryio/<version>/In-app renderer, where the Package Manager puts the sample, and needs com.unity.ugui as well.

The prefab is the part to make yours - swap the colours, the font, drop in your own art. The renderer draws whatever is in its Panel Prefab field (JoryioInAppPanel.prefab in the sample scene) and fills in four children found by name: Title and Body (Text), PrimaryButton (copied once per campaign button) and SecondaryButton (the close button). Author style overrides are applied on top where the campaign set them. With the field empty it builds a plain panel in code.

uGUI buttons need an EventSystem, so the renderer creates one when the scene has none - with the Input System's UI module when that is your active input handling, otherwise the legacy one. The script shows the one thing to keep: reporting each message's lifecycle. It requires com.unity.ugui, which the SDK itself does not.

When a message counts as displayed​

A message counts as displayed - in_app.displayed is reported and the campaign's frequency cap is spent - only once the person could have seen it. The time it must be on screen is IN_APP_DISPLAY_DWELL_MS (1000 ms), the same constant on every Joryio SDK.

  • Messages the native SDK draws follow the iOS and Android rule: shown only on a settled screen in the foreground, and counted after 1 second on screen with the app in the foreground. A tap or a close before then counts as seen; a message that leaves the screen sooner is not counted and stays eligible.
  • If the game draws messages itself, call MarkDisplayed() by the same rule - JoryioSDK.IN_APP_DISPLAY_DWELL_MS: once the message has been visible that long, or right before MarkClicked / MarkDismissed when the player acts sooner. Do not call it for a message that left the screen first (a scene change, the app paused).

Two independent switches, the same on every Joryio SDK:

  • Tracking consent - your consent screen's answer: Granted, Denied or Pending.
  • Opt-out - the player's own choice: OptOut() / OptIn().

Data is collected only when consent is granted and the player has not opted out; either "no" wins. Neither switch changes the other: UpdateConsent(TrackingConsent.Granted) does not lift an opt-out (only OptIn() does), and OptOut() does not change the consent.

If the player has not answered your consent screen yet, start with consent Pending: nothing is sent - no session start, no in-app sync, no push registration - and events wait on the device (up to MaxQueueSize) until the answer arrives.

JoryioSDK.Initialize(sdkKey, apiHost, new JoryioConfig {
TrackingConsent = TrackingConsent.Pending,
});
// ...once the player answers
JoryioSDK.UpdateConsent(TrackingConsent.Granted); // sends what was queued
// or
JoryioSDK.UpdateConsent(TrackingConsent.Denied); // drops it, collects nothing
  • Denied is remembered across restarts. It is lifted only by UpdateConsent(TrackingConsent.Granted) or by an explicit TrackingConsent = TrackingConsent.Granted in the config.
  • Leaving JoryioConfig.TrackingConsent unset (the default) keeps a stored denial; with nothing stored the SDK tracks normally, so a game without a consent screen needs no setting.
  • Pending applies to the current launch only and never overrides a stored Denied.
  • A value outside the enum (a bad cast) is ignored with a warning; in the config it is treated as not set.
JoryioSDK.OptOut(); // stop collecting; queued events and pending attribute writes are discarded
JoryioSDK.OptIn(); // resume
JoryioSDK.WipeData(); // erase everything stored on the device (the opt-out is kept)

An opt-out is stored and survives restarts. To start a session already opted out, set JoryioConfig.OptOut = true.

IsUserOptedOut() reflects the opt-out switch only, not the consent. GetTrackingConsent() returns the consent switch: Granted, Denied or Pending. Both answer before Initialize from what is stored on the device: an opt-out or a Denied remembered from an earlier launch is reported as such, not as a default, and an OptOut() / OptIn() made earlier in the same launch wins over the stored opt-out. GetTrackingConsent() returns Granted only when nothing has been decided, which is also how the SDK behaves then.

When the opt-out changes, the SDK records it on the player's profile as the boolean attribute tracking_opted_out, so segments can exclude opted-out players. GetTrackingConsent() reports the consent the native SDK holds, including a Denied remembered from an earlier launch.

SDK authentication​

The token proves who an identified user is: requests that name a user ID must carry a valid one. Anonymous visitors have no token to show, so their requests are accepted without it, unless the workspace also turns on strict identity.

For workspaces with SDK authentication turned on, every request carries a token your server signs:

JoryioSDK.Initialize(sdkKey, apiHost, new JoryioConfig { EnableSdkAuthentication = true });
JoryioSDK.SetSdkAuthenticationToken(tokenFromYourServer); // after login

JoryioSDK.SetSdkAuthenticationErrorHandler(error => {
// error.reason: missing, invalid, expired, sub_mismatch or unknown
// error.refreshExhausted: the SDK has stopped retrying this request
FetchFreshToken(token => JoryioSDK.SetSdkAuthenticationToken(token));
});

SetSdkAuthenticationToken and SetSdkAuthenticationErrorHandler work before Initialize too. The native SDK starts with that token, ahead of its first request, and keeps it through a WipeData() made before Initialize, so an unsubscribe held for the player being erased still carries their token. You can also pass the first token as JoryioConfig.SdkAuthenticationToken.

The handler runs on the main thread. It is called for an authentication 401 on the current player's requests, including before any token was set (reason missing) - never for the leftovers of a player who has logged out, so it never makes you sign out the player who is playing now. The current player's refused events and writes are kept and retried once you set a fresh token. Events go out in batches of one player at a time, and events and writes of a player who has logged out are dropped on any authentication 401 (missing, invalid, expired or sub_mismatch), so they never hold back the next player's. Reset() takes the token out of the session; it stays with the outgoing player's leftovers, and the next player's requests never carry it - set their token with SetSdkAuthenticationToken once they sign in. Events and writes the outgoing player had already queued still go out with that player's token, so a normal logout loses nothing.

E-commerce​

Use the typed tracker rather than Track for commerce. It applies the canonical event names that revenue reporting and attribution read; a hand-typed name such as "Added to Cart" is stored as a different event and matches no report.

var item = new EcommerceCartItem { ProductId = "gems_500", Name = "500 gems", Price = 4.99, Quantity = 1 };
JoryioSDK.Ecommerce.AddToCart(item, cartValue: 4.99);
JoryioSDK.Ecommerce.Purchase(new EcommerceOrder {
OrderId = "order-1042",
Items = new List<EcommerceCartItem> { item },
Value = 4.99,
Currency = "USD",
});

The tracker has the same twenty events as the React Native SDK, from ViewProduct to SubmitReview.

A call missing a required argument logs a warning and sends nothing - never an empty or zero event: a product without a ProductId, an order without an OrderId or Items, an empty order or coupon id, a cart call without its item list. An amount that is NaN or infinite (Value, Price, cartValue, Shipping, Tax, Discount, refundAmount, discountAmount) is refused the same way, rather than recorded as 0.

Identity and session​

Send phone numbers with + and the country code

Always send phone numbers in full international format (E.164): +, the country code, then the number - for example +972501234567. A number without + is read using the workspace's default country for phone numbers; if none is set, or the digits can be read two ways, it is saved as unverified and not used for messages. How phone numbers are handled

var (userId, anonymousId) = JoryioSDK.GetIdentity(); // userId is null when anonymous
var (sessionId, sessionStart, duration) = JoryioSDK.GetSessionInfo(); // duration in seconds

Reset() logs the player out: a new anonymous id, no user id.

API reference​

Every public member of JoryioSDK, with its parameter names. All are static and safe to call before Initialize (they log and do nothing, and a callback still answers once), except the consent calls, WipeData() and the SDK authentication calls, which take effect, and the subscription calls and UnregisterPush(), which are held until it.

MethodWhat it does
Initialize(sdkKey, apiHost, config = null)Start the SDK. A second call is ignored. A key for another platform (iOS keys start with jry_sdk_ios_, Android keys with jry_sdk_android_) is refused with an error and the SDK stays off. See Configuration.
IsInitializedWhether Initialize succeeded. True on an iOS launch before the device's first unlock too: the native SDK finishes starting at the unlock, and calls made meanwhile wait for it. If calls held by an earlier launch are still on the device, iOS keeps them unreadable until that unlock too: the SDK collects nothing and every call waits until they can be read, then sends them first and the calls made since after them, in order. Meanwhile GetTrackingConsent() and IsUserOptedOut() report the decisions this launch made (calls and JoryioConfig). Without one, GetTrackingConsent() reports Pending, because the stored consent cannot be read yet, and IsUserOptedOut() answers from the native SDK, which may not see an opt-out stored by an earlier launch until the unlock. When the calls are sent, the consent applied is the one this launch set explicitly (UpdateConsent before Initialize, else JoryioConfig.TrackingConsent); without one, the decision saved on the device applies, read after the unlock, the same as the native SDKs: a stored Denied stays Denied, and with none stored the SDK is Granted, as on any other launch. Other getters, such as GetUserId(), GetAnonymousId() and GetSessionId(), still answer from the native SDK and do not yet reflect the calls that are waiting.
SdkVersionThe SDK version, "1.3.3".
Track(eventName, properties = null)Track an event.
TrackScreen(screenName, properties = null)Track a screen view.
Flush()Send queued events now.
Identify(userId)Identify the player. While the player is opted out or consent is Denied, the id is only remembered on the device (nothing is sent), so an unsubscribe made meanwhile reaches the right player.
Reset()Log out: a new anonymous id, no user id.
AddAlias(label, name)Attach another identifier, such as ("steam_id", "7656..."). Works for an anonymous player.
GetUserId() / GetAnonymousId()The current ids ("" when absent).
GetIdentity()(userId, anonymousId); userId is null when anonymous.
SetAttributes(attributes)Set several attributes. A null value unsets that attribute.
SetAttribute(key, value)Set one attribute, keeping its type. A null value unsets it; NaN or infinity is ignored.
IncrementAttribute(key, by = 1)Add to a numeric attribute. NaN or infinity is ignored.
UnsetAttribute(key)Remove an attribute.
UpdateConsent(consent)TrackingConsent.Granted, Denied or Pending. Never changes the opt-out. Works before Initialize. See Consent.
OptOut() / OptIn()Stop or resume collecting. Stored across restarts; never changes the consent. Work before Initialize.
IsUserOptedOut()Whether the player is opted out (the opt-out switch only). Answers before Initialize too, from the stored decision.
GetTrackingConsent()The current consent: TrackingConsent.Granted, Denied or Pending (the consent switch only). Answers before Initialize too, from the stored decision.
WipeData()Erase everything stored on the device. The opt-out is kept.
SetSubscription(channel, status)Marketing subscription for one channel. See Marketing Subscription.
AddToSubscriptionGroup(groupId, channel) / RemoveFromSubscriptionGroup(groupId, channel)Join or leave a subscription group.
RequestPushPermission(onResult)Ask for the notification permission; onResult(bool granted).
GetPushPermissionStatus(onResult)"granted", "denied", "notDetermined" or "unavailable".
IsPushEnabled(onResult)Whether notifications are enabled; onResult(bool).
RegisterPushToken(token, onResult = null)Hand over a token from your own push stack; onResult(PushRegistrationResult).
ReportPushDelivered(trackingId)Report that a push arrived (your own push stack).
TrackPushClick(trackingId)Report that a notification was opened.
UnregisterPush()Stop push to this device. Stays off across restarts until RegisterPushToken (after Initialize) or a permission request. Held when called before Initialize.
GetDeviceToken()The push token the SDK holds ("" when none).
UpdateBadgeCount(count) / ClearBadge()iOS app icon badge; no-ops on Android.
GetSessionId()The current session id.
GetSessionInfo()(sessionId, sessionStart, duration); duration in seconds.
SetSdkAuthenticationToken(token)Supply a signed token. See SDK authentication.
SetSdkAuthenticationErrorHandler(handler)Hear when a request is rejected under SDK authentication; null stops listening.
EcommerceThe typed e-commerce tracker. See E-commerce.
SetInAppMessageHandler(handler, canRenderHtml = false)Render in-app messages yourself. Null hands rendering back to the native SDK.
ClearInAppMessageHandler()Hand in-app rendering back to the native SDK.
ClearInAppMessageHandler(handler)The same, only if handler is still the installed one. A null handler clears whatever is installed.
TrackInAppImpression(campaignId, action, elementId = null)Report displayed, clicked or dismissed; elementId is the tapped button's id. Prefer the message's Mark... methods.
SubmitInAppForm(campaignId, values, onResult)Submit a form from an HTML message you render; onResult(InAppFormSubmitResponse).
SyncInAppCampaigns()Fetch the latest campaigns now.
EvaluateInAppCampaigns() / ResetDisplayedCampaigns()Testing helpers; ignored unless Debug is on.

On the message your handler receives: MarkDisplayed(), MarkClicked(button = null) and MarkDismissed().

Building​

The SDK configures the exported projects automatically. Nothing to do by hand:

  • Xcode: the native SDK as a Swift package (joryio-ios-sdk, linked into UnityFramework), Swift version, Swift runtime embedding, the push capability, the remote-notification background mode, and UNITY_USES_REMOTE_NOTIFICATIONS in Classes/Preprocessor.h - without it Unity compiles out the callback that delivers the APNs token. The push capability is added to the target's existing entitlements file when it has one, so other plugins' entitlements are kept.
  • Android: keep rules for the Java bridge are appended to the generated proguard-unity.txt, so a minified release build keeps the methods C# calls by name.

If notifications never arrive on a build that otherwise works, check that the Push Notifications capability is present on the target: it is the one setting whose absence produces no error anywhere.

Marketing Subscription​

Whether this person may be messaged on a channel. A different consent from OptOut() above, which is whether they may be observed - neither implies the other, and someone can decline tracking and still want the newsletter.

This is what an in-app preference centre writes.

JoryioSDK.SetSubscription("email", "unsubscribed");

Channels: email, sms, whatsapp, push, viber. Statuses: optedIn, subscribed, unsubscribed.

Subscription groups are lists a person can join or leave per channel:

JoryioSDK.AddToSubscriptionGroup("list_abc123", "email");
JoryioSDK.RemoveFromSubscriptionGroup("list_abc123", "email");

An identifier is required - the call is skipped, with a logged error, if there is neither a user id nor an anonymous id.