SDK

The key your app authenticates with, and the handful of calls that fetch a flow, show it, and report what happened.

What your app actually does

Ship one key. Call configure()once at launch, then ask for a flow by name where you want one. Speedy Deploy answers with whatever is published on that key's channel, renders it natively, and queues the events behind your numbers. There is no device session and nothing to log into — one GET, one header.

Connecting an app

There is a command-line tool for this, and its job is the part that is hard to see: it works out which of three project shapes yours is, raises both native build floors in whichever files your project actually reads, writes the key and channel together so they can never disagree, and then tells you whether any of it worked.

npm install @speedydeploy/react-native
npx speedydeploy login
npx speedydeploy init
npx speedydeploy doctor

Keys

You issue them under SDK keys in the console. A key carries two decisions made when you create it, and neither can be changed afterwards.

ChannelStaging or Production. It is bound to the key rather than to the request, so a build holding a staging key is served staging releases; a production release is refused even if the app asks for one. You can spot the wrong key in a log line: production keys begin sd_prod_, staging keys sd_stag_.

What it is forIn your app or On your server. An in-app key fetches flows and sends analytics. A server key never ships in an app; it reports purchases from RevenueCat, Adapty or your own backend. Each surface refuses the other kind: a server key is turned away by delivery and by event ingest, an in-app key is turned away by the purchase surface. Creating a server key takes the project admin role.

Revoking is immediate and cannot be undone — any app still using that key stops receiving flows at once. Roll a key by issuing the new one, shipping it, then revoking the old one. The revoked key stays in the list with its label and the date it was revoked, so the trail survives.

Wiring it up

// iOS
SpeedyDeploy.shared.configure(
    SpeedyDeployConfiguration(
        sdkKey: "sd_stag_…",
        projectId: "<your project id>",
        environment: .staging))

SpeedyDeploy.shared.setUserRef(purchases.appUserID)
SpeedyDeploy.shared.setUserAttributes(["plan": "pro", "credits": 12])
await SpeedyDeploy.shared.preload("onboarding")

let result = await SpeedyDeploy.shared.present("onboarding")
SpeedyDeploy.shared.track("activated")
// Android — present, preload, track, setUserRef and appOpened are suspend functions
SpeedyDeploy.configure(
    context,
    SpeedyDeployConfiguration(
        sdkKey = "sd_stag_…",
        projectId = "<your project id>",
        environment = SpeedyDeployConfiguration.Environment.STAGING,
    ),
)

SpeedyDeploy.setUserRef(purchases.appUserID)
SpeedyDeploy.setUserAttributes(mapOf("plan" to JsonPrimitive("pro")))
SpeedyDeploy.preload("onboarding")

val result = SpeedyDeploy.present("onboarding")
SpeedyDeploy.track("activated")
// React Native — the method is presentFlow, not present
SpeedyDeploy.configure({
  sdkKey: 'sd_stag_…',
  projectId: '<your project id>',
  environment: 'staging',
});

SpeedyDeploy.setUserRef(await Purchases.getAppUserID());
SpeedyDeploy.setUserAttributes({ plan: 'pro', credits: 12 });
SpeedyDeploy.preload('onboarding');

const result = await SpeedyDeploy.presentFlow('onboarding');
SpeedyDeploy.track('activated');

React Native needs nothing else — the flow presents itself, in a native surface above your RN root. (Integrating the iOS or Android SDK directly is where you host it yourself: wrap your SwiftUI root in SpeedyDeployHost, or call SpeedyDeployHost() in Compose.) environmentkeys the on-device cache so a staging build never reads a production build's entries — what you are served is decided by the key. preload fetches and parses without showing anything, so the next present has its first frame ready.

The string you pass names a placement — the address your app asks by, not a flow. The parameter is still called flowKey, because apps already in the field have that URL compiled in.

Rules worth knowing

One flow shows at a time. A second present while one is up fails fast rather than stacking two full-screen takeovers.

Dismissal is an outcome, not an error. A result is completed, dismissed, or failed, and failedis reserved for the SDK's own failures — offline with nothing cached, presenting before configure(). A user closing your onboarding is something to branch on, not something to catch.

appOpened() is what makes retention measurable. Every other event happens inside a flow session, and coming back is by definition outside one. configure() fires it for you on a cold launch; call it yourself when the app returns to the foreground if you want a day counted for a process that stayed in memory across midnight. Extra calls on the same day are ignored, so call it as often as you like.

track() names are matched exactly against a metric of kind event in your project. "activated" and "Activated" are two different things. Pick one spelling and keep it.

setUserAttributes() is what a Custom attribute rule reads. Pass what you know about the person — a plan tier, a cohort name, a credit balance — as strings, numbers or true/false. Write the keys bare (plan); the console spells the same rule user.plan, and either spelling is accepted. It replaces the whole set each time, so pass everything rather than a delta, and call it again whenever a value changes: the SDK refetches instead of serving what the previous attributes resolved to, so an upgrade takes effect at the next present rather than up to eleven minutes later. Set it before preload if you can — that fetch counts too.

A key is lowercase letters, digits and underscoresplan_tier, not planTier, up to 64 characters. Keys outside that are dropped rather than sent, because they are exactly the keys the console's Custom-attribute field cannot write: a rule against planTier is not a rule anyone can author, so sending it would cost a header on every request and buy an audience that matches nobody.

The type is compared as sent. A rule holds a string, a number or true/false and matches with none of the conversions a language would do for you, so credits: 12 is reached by a Number rule and never by a text one. The rule editor has a Text / Number / true-false control on the row for exactly this.

Prove it before you write native code

Try the delivery API sits under the key list. Paste a key, choose a Flow key, press Send request. It does exactly what a device does — a plain GET https://<your-api>/sdk/v1/p/{projectId}/flows/{flowKey} with your key in the x-sd-sdk-keyheader — and renders whatever comes back. If that panel shows the flow, your key and the placement are wired correctly. It does not model your app's SDK version or its targeting signals, so a device on an older build, or one that falls into a different audience, can still be served something else. Check health after you ship.

Related

Flows are what gets rendered and placements decide which one your key resolves to; audiences decide who qualifies. What your events add up to is defined in metrics, and health is where delivery failures land.