# BDK Native > Reference documentation for the @bdk/native SDK — bring native iOS/Android features to any web app. Covers only the SDK's supported, customer-facing surface (the namespaced browser helpers and server helpers); internal/native-only commands are intentionally omitted. --- # Get started URL: https://docs.thebdk.com/getting-started/introduction ::::ref-section{title="Create the client"} Create the client once with `createBdkNative()` and reuse it everywhere. ::callout Always create the client with `createBdkNative()`, never `new BdkNativeClient()`. It's ready the moment it returns. :: #code :::code-group ```ts [Create the client] import { createBdkNative } from "@bdk/native/browser"; // One instance, shared across your app. export const bdk = createBdkNative(); const info = await bdk.ready(3000); console.log(info ? `Native: ${info.deviceOS}` : "Plain web page"); ``` ::: :::: ::::ref-section{title="Detect the native app"} Use `bdk.ready(timeoutMs)` to wait for device info and `bdk.isNative()` to branch your code. In a plain browser there's no app, so render a web-only fallback. - `bdk.ready(0)` (the default) resolves immediately — cached `BdkDeviceInfo` if it arrived, else `null`. - `bdk.ready(3000)` waits up to the timeout (ms) for the `deviceInfo` event. ::callout Browser method options are typed — pass the option names each method documents, and your editor will catch mismatches. :: #code :::code-group ```ts [Detect the shell] const info = await bdk.ready(3000); if (bdk.isNative()) { // Cached device info: playerId, pushToken, deviceOS, bdkRelease, ... console.log(bdk.getDeviceInfo()?.deviceOS); } else { // No native shell — render web fallbacks. } ``` ::: :::: ::::ref-section{title="Get a command's result"} Legacy dispatch commands resolve a [`NativeCommandResult`](/reference/objects) — branch on `!result.triggered`. New-generation namespaced helpers resolve an Action-result envelope that always has `ok` — branch on `!result.ok`. See [Handling results](/getting-started/interaction-models) for the full patterns. ::callout{type="warn"} Match the event to the command: `bdk.media.capturePhoto()` emits `photoCaptured`, `bdk.media.pickPhoto()` emits `photoSelected`. Both deliver a `MediaResult` (`{ fileUrl, dataUri, data, contentType }`). :: #code :::code-group ```ts [Dispatch + event] // 2) Dispatch a command, then receive its result via an event. const off = bdk.on("photoCaptured", (result) => { console.log("Captured:", result.fileUrl, result.contentType); off(); // unsubscribe when you are done }); const dispatch = await bdk.media.capturePhoto(); console.log(dispatch.triggered, dispatch.queued); // dispatch status, not the photo ``` ```ts [Dispatch-and-forget] // 1) Navigation just dispatches — there is no follow-up event. const result = await bdk.navigation.navigate({ url: "/dashboard" }); console.log(result.command, result.triggered); ``` ```ts [Listen for errors] // Listener failures surface as a BdkError (code BDK_LISTENER_ERROR) // via both onError config and the "error" event; they do not recurse. const off = bdk.on("error", (error) => { console.error(error.code, error.message); }); ``` ::: :::: ::::ref-section{title="Browser and server SDKs"} Use `@bdk/native/browser` (or the package root; CDN at `dist/cdn/bdk-native.global.js`) in your web app. Use `@bdk/native/server` or a focused `/server/*` entry on your backend — `await` returns the typed result. ::callout{type="warn"} Never import `@bdk/native/server/*` into a browser bundle — those modules are server-only and may carry secrets. Only the root and `/browser` entry points are browser-safe. :: #code :::code-group ```ts [Server SDK (Node)] // Server-only. Awaiting resolves to the real, typed outcome. import { sendPushNotification } from "@bdk/native/server/onesignal"; const result = await sendPushNotification({ title: "New message", message: "You have a new reply", playerIds: ["a-onesignal-player-id"] }); console.log(result.sentSuccessfully, result.numberOfRecipients); ``` ::: :::: ::::ref-section{title="Start an in-app purchase"} Dispatch a purchase with `bdk.iap.purchaseIos()`, identifying the product with `{ id, type }` where `type` is `"product"` or `"subscription"`. Read the outcome from the `purchaseSuccess` or `purchaseFailed` event. #code :::code-group ```ts [Start a purchase] bdk.on("purchaseSuccess", ({ platform, data }) => { console.log("Purchased on", platform, data); }); // Native payload contract: { id, type: "product" | "subscription" } await bdk.iap.purchaseIos({ id: "com.example.pro", type: "subscription" }); ``` ::: :::: --- # Install and import URL: https://docs.thebdk.com/getting-started/installation ::::ref-section{title="Install the package"} Add `@bdk/native` with your package manager. Server code needs Node.js 18 or newer. #code :::code-group ```bash [npm] npm install @bdk/native ``` ```bash [pnpm] pnpm add @bdk/native ``` ```bash [yarn] yarn add @bdk/native ``` ::: :::: ::::ref-section{title="Import for app code"} Import from `@bdk/native` for a single, obvious import path in app code. Use `createBdkNative()` to create a ready-to-use client. ::callout The root and `/browser` entries resolve to the same module. Pick whichever import path reads best. :: #code :::code-group ```ts [Root import] import { createBdkNative } from "@bdk/native"; export const bdk = createBdkNative(); ``` ```ts [Equivalent /browser import] import { createBdkNative, BdkNativeClient } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Import in the WebView"} Use `@bdk/native/browser` for code that runs inside the WebView or a browser. It exports `createBdkNative`, `BdkNativeClient`, the error helpers, and the browser types (`BdkNativeConfig`, `NavigateOptions`, `LoadingScreenOptions`, `OpenLinkOptions`, `UrlParam`, `PermissionName`). Pass a `BdkNativeConfig` to tune behavior — the fields are `removeLoading` (`"automatic"` or `"manual"`), `pageFit`, `assumeWebWithoutSeed` (see [Web fallback](/getting-started/non-native-browser)), and an `onError` callback that receives a `BdkError`. Browser method options are typed — pass the option names each method documents. ::callout{type="warn"} Awaiting a browser namespace method resolves when the call is sent, not when the native action finishes. For real resolved results, use the server helpers below. :: #code :::code-group ```ts [Basic] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ```ts [With config] const bdk = createBdkNative({ removeLoading: "automatic", onError: (error) => console.error(error.code, error.message) }); ``` ```ts [Typed options] // `url` / `baseUrl` are SDK-typed fields (NavigateOptions). await bdk.navigation.navigate({ url: "https://example.com" }); ``` ::: :::: ::::ref-section{title="Import on the server"} Use the `@bdk/native/server` entries for Node code — push notifications, receipt validation, deep links. These helpers are fully typed and return real resolved results. Import the aggregate `@bdk/native/server`, or a single concern to keep your bundle lean: - `@bdk/native/server/onesignal` — push notifications (e.g. `sendPushNotification`) - `@bdk/native/server/iap` — receipt validation (e.g. `verifyIosReceipt`, `verifyAndroidReceipt`, `consumeAndroidPurchase`) - `@bdk/native/server/branch` — Branch deep links - `@bdk/native/server/chottulink` — ChottuLink deep links - `@bdk/native/server/firebase-dynamic-links` — Firebase Dynamic Links ::callout{type="warn"} Never import a `@bdk/native/server/*` module into browser code — it pulls in Node-only dependencies and breaks the build. :: #code :::code-group ```ts [Aggregate import] import { sendPushNotification } from "@bdk/native/server"; const result = await sendPushNotification({ oneSignalAppId: process.env.ONESIGNAL_APP_ID, oneSignalApiKey: process.env.ONESIGNAL_API_KEY, title: "Hello", message: "Your order shipped" }); ``` ```ts [Single subpath] import { verifyIosReceipt } from "@bdk/native/server/iap"; const validation = await verifyIosReceipt({ receipt: base64Receipt, sharedSecret: process.env.APPLE_SHARED_SECRET, useSandboxFallback: true }); if (validation.isValid) { // grant entitlement } ``` ::: :::: ::::ref-section{title="Use without a bundler (CDN)"} Drop in the prebuilt IIFE bundle to use the SDK without a bundler. It is published to unpkg and jsdelivr and attaches a `BdkNative` global to `window`, so `createBdkNative` is available as `BdkNative.createBdkNative`. Server helpers are not included. #code :::code-group ```html [unpkg] ``` ```html [jsdelivr] ``` ::: :::: ::::ref-section{title="Browser-safe vs server-only"} The root, `/browser`, and the CDN global build are browser-safe. Every `/server` entry and subpath is server-only. ::callout{type="warn"} The single rule that prevents most build failures: never import `@bdk/native/server/*` into anything that ships to the browser. :: #code :::code-group ```ts [Safe in the browser] import { createBdkNative } from "@bdk/native"; // root import { BdkNativeClient } from "@bdk/native/browser"; // browser ``` ```ts [Server-only — do NOT bundle for the browser] import { sendPushNotification } from "@bdk/native/server"; import { verifyAndroidReceipt } from "@bdk/native/server/iap"; ``` ::: :::: --- # Set up the client URL: https://docs.thebdk.com/getting-started/initializing-the-client ::::ref-section{title="Create the client"} Create the client once at app startup and reuse the same instance everywhere. Import from `@bdk/native/browser` or the package root — both work. #code :::code-group ```ts [Basic] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ```ts [From the root] // Identical — the package root is browser-safe too import { createBdkNative } from "@bdk/native"; export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Configure the client"} Every option is optional with a safe default, so most apps pass nothing. Set `removeLoading: "manual"` to dismiss the native splash screen yourself, and pass `onError` to centralize error reporting (it receives a `BdkError`). #code :::code-group ```ts [With options] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative({ removeLoading: "automatic", // "automatic" | "manual" pageFit: "normal", // "normal" | "cover" onError: (error) => { console.error(error.code, error.message); } }); ``` ::: :::: ::::ref-section{title="Wait for the app to be ready"} Wait for device info at startup before using native features. Pass a timeout to wait that many milliseconds; inside the app it resolves as soon as device info arrives, and in a plain browser it resolves to `null` when the timeout elapses. It never rejects, and it doesn't block rendering — only the code after the `await`. #code :::code-group ```ts [Wait up to 3s] const info = await bdk.ready(3000); if (info) { console.log("Running natively on", info.deviceOS, "release", info.bdkRelease); } else { console.log("No native shell — running as a normal web page"); } ``` ::: :::: ::::ref-section{title="Detect native vs. browser"} Branch between native-only features and web fallbacks. `bdk.isNative()` returns `true` once device info has arrived; `bdk.getDeviceInfo()` returns the cached `BdkDeviceInfo` or `null`. For an instant answer, `bdk.environment()` reports `"native"` synchronously on current app builds — treat `"unknown"` as "wait" and use `ready()`. ::callout{type="warn"} `isNative()` reads `false` until device info arrives — at page load that's the wrong answer inside the app. Always `await bdk.ready(...)` before branching. :: #code :::code-group ```ts [Branch on native] await bdk.ready(3000); if (bdk.isNative()) { const info = bdk.getDeviceInfo(); console.log(info?.playerId, info?.pushToken, info?.versionName); } else { // Render a web-only experience } ``` ```ts [Instant environment check] if (bdk.environment() === "native") { // App shell confirmed — still await bdk.ready() before reading device info. } ``` ::: :::: ::::ref-section{title="Clean up in tests"} Tear down the client between instances, such as in tests or when hot-reloading a single-page app. You rarely need this in production. #code :::code-group ```ts [Cleanup] bdk.dispose(); ``` ::: :::: --- # Handling results URL: https://docs.thebdk.com/getting-started/interaction-models ::::ref-section{title="The three ways a call returns"} How you get a result depends on the call. There are three patterns. 1. **Fire-and-forget.** UI and navigation calls (`bdk.navigation.navigate`, `bdk.ui.showBanner`) just do something. `await` confirms the call went out; there's nothing else to read. 2. **Result on an event.** Interactive calls (media, location, pickers, biometrics, purchases) finish later. Subscribe with `bdk.on(...)` to get the data. 3. **Returned value.** Server helpers (`@bdk/native/server/*`) run on your backend and `await` returns the real value. Models 1 and 2 run in the browser. Legacy dispatch commands resolve a `NativeCommandResult`; namespaced envelope helpers resolve an Action-result union that always has `ok`. Model 3 returns a typed value. ::callout{type="warn"} Browser command `options` are typed — pass the option names each method documents (e.g. `{ id, type }` for purchases). :: #code :::code-group ```ts [The three at a glance] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // 1. Dispatch-and-forget — result is just the dispatch ack, no event follows. await bdk.navigation.navigate({ url: "/profile" }); // 2. Dispatch + result event — start the interaction here... await bdk.media.capturePhoto(); // ...and read the outcome here. bdk.on("photoCaptured", (photo) => console.log(photo.fileUrl)); // 3. Resolved promise with the real value — server only. // import { verifyIosReceipt } from "@bdk/native/server/iap"; // const result = await verifyIosReceipt({ receipt }); ``` ::: :::: ::::ref-section{title="Fire-and-forget calls"} Use these when the app just does something visible — navigate, show a toast, vibrate. `await` resolves to a `NativeCommandResult` confirming the call went out. There's no event to listen for. ::callout Outside the app these calls resolve with `triggered: false` instead of throwing — `pending` in a browser tab, `skipped` with no `window` — so your web build keeps working. :: #code :::code-group ```ts [Navigation] const result = await bdk.navigation.navigate({ url: "/checkout" }); // { command: "navigate", queued: true, triggered: true, skipped: false } // Nothing else arrives — navigation has no result event. ``` ```ts [UI commands] await bdk.ui.showBanner({ title: "Saved" }); await bdk.ui.showBanner({ title: "Welcome", description: "Glad you're here" }); await bdk.device.vibrate(); // All dispatch-and-forget: resolve on dispatch, no follow-up event. ``` ::: :::: ::::ref-section{title="Get a result from an event"} Use this when the user has to act — take a photo, pick a date, approve Face ID, confirm a purchase. Subscribe to the event **before** you make the call; the data arrives on the listener, not on the `await`. Event names don't always match the method: - `bdk.media.capturePhoto()` emits `photoCaptured`. - `bdk.media.pickPhoto()` emits `photoSelected`. - `bdk.location.getCurrentPosition()` emits `location`. - `bdk.ui.pickDateTime()` emits `datePicked`. - `bdk.auth.authenticateBiometrics()` emits `biometricResult`. - `bdk.iap.purchaseIos()` / `purchaseAndroid()` emit `purchaseSuccess` or `purchaseFailed`. ::callout{type="warn"} `capturePhoto` emits `photoCaptured`, not `photoSelected` (which belongs to `pickPhoto`). Listening on the wrong one is the top reason a result "never arrives". :: ::callout `bdk.on(event, listener)` returns an unsubscribe function — call it to stop listening. :: #code :::code-group ```ts [Capture a photo] // Subscribe first — the result arrives later, on the event. const off = bdk.on("photoCaptured", (photo) => { console.log(photo.fileUrl, photo.dataUri, photo.contentType); off(); // unsubscribe when you're done }); // Dispatch. This resolves on dispatch, NOT when the photo is taken. await bdk.media.capturePhoto(); ``` ```ts [Get location] bdk.on("location", (coords) => { console.log("device location:", coords); }); await bdk.location.getCurrentPosition(); ``` ```ts [In-app purchase] bdk.on("purchaseSuccess", ({ platform, data }) => { console.log(`purchased on ${platform}`, data); }); bdk.on("purchaseFailed", ({ platform, data }) => { console.warn(`purchase failed on ${platform}`, data); }); // Native payload key is { id, type } — type is 'product' | 'subscription'. // (Do NOT use product_id — that's a legacy property name.) await bdk.iap.purchaseIos({ id: "com.example.pro", type: "subscription" }); ``` ```ts [Biometrics] bdk.on("biometricResult", ({ platform, status, data }) => { if (status === "success") unlockApp(); }); await bdk.auth.authenticateBiometrics(); ``` ::: :::: ::::ref-section{title="Server calls that return a value"} Use these on your backend for receipt verification, link creation, and push notifications. They're fully typed and `await` returns the real result — no event needed. For example, `verifyIosReceipt` returns an `IosReceiptValidationResult` with `isValid`, `resultData`, `errorData`, and `raw`. ::callout{type="warn"} Never import `@bdk/native/server/*` into a browser bundle — those modules use credentials and Node APIs. The browser-safe entry points are the package root and `@bdk/native/browser`. :: #code :::code-group ```ts [Verify a receipt (server)] import { verifyIosReceipt } from "@bdk/native/server/iap"; // await gives the real result — no event, no NativeCommandResult. const result = await verifyIosReceipt({ receipt }); if (result.isValid) { grantEntitlement(result.resultData); } ``` ```ts [Send a push (server)] import { sendPushNotification } from "@bdk/native/server/onesignal"; const outcome = await sendPushNotification({ message: "Your order shipped", playerIds: [playerId] }); console.log(outcome); // the actual PushNotificationResult ``` ::: :::: ::::ref-section{title="The NativeCommandResult shape"} Legacy dispatch commands resolve a `NativeCommandResult`. Use it to confirm the call went out, or to detect that you're running on the web. Namespaced envelope helpers resolve an Action-result union that always has `ok` — branch on `!result.ok`. The common case is `{ triggered: true }`. Outside the app `triggered` is `false` — `pending` in a browser tab, `skipped` with no `window` — so branch on `!triggered` to degrade gracefully. ::callout A few commands are platform-specific (e.g. `bdk.iap.purchaseIos` is iOS only). On a device that can't run one, the call rejects with a `BdkError` — wrap it in `try/catch`, or check `bdk.isNative()` after `await bdk.ready(...)` has resolved (it reads `false` until device info arrives). :: #code :::code-group ```ts [NativeCommandResult] interface NativeCommandResult { command: string; // the native command name that was dispatched queued: boolean; // true once the command is scheduled triggered: boolean; // was it actually handed to the app? skipped: boolean; // was it dropped without triggering? pending?: boolean; // true while waiting outside the app reason?: string; // why it was skipped / pending, when applicable } ``` ```ts [Branch on the result] const res = await bdk.ui.showBanner({ title: "Hi" }); if (res.triggered) { // dispatched to the app } else { // outside the app — pending in a browser tab, skipped with no window. // Either way it didn't run natively, so degrade gracefully. } ``` ::: :::: ::::ref-section{title="Why await isn't the result"} **Await to confirm dispatch, subscribe to receive data.** #code :::code-group ```ts [Wrong — the await has no payload] // ❌ capturePhoto resolves a NativeCommandResult, not a photo. const result = await bdk.media.capturePhoto(); console.log(result.fileUrl); // undefined — wrong model ``` ```ts [Right — listen for the event] // ✅ Subscribe for the data, await only to dispatch. bdk.on("photoCaptured", (photo) => { console.log(photo.fileUrl); // the real result }); await bdk.media.capturePhoto(); ``` ```ts [Right — promise-wrap an interaction if you need one await] function takePhoto() { return new Promise((resolve) => { const off = bdk.on("photoCaptured", (photo) => { off(); resolve(photo); }); void bdk.media.capturePhoto(); }); } const photo = await takePhoto(); ``` ::: :::: --- # Web fallback URL: https://docs.thebdk.com/getting-started/non-native-browser ::::ref-section{title="Set up once, run anywhere"} `createBdkNative()` returns a client that works inside the native app and in any plain browser. Outside the app, native commands resolve cleanly with `triggered: false` instead of throwing, so the same build is safe to load everywhere. ::callout In a browser tab outside the app, commands resolve `pending` (`reason: "waiting_for_agent"`). With no `window` at all (SSR, Node, a Web Worker) they resolve `skipped` (`reason: "not_native"`). Either way `triggered` is `false` — branch on that. :: #code :::code-group ```ts [Same code, every environment] import { createBdkNative } from "@bdk/native/browser"; // Safe to call on localhost, in a preview, or inside the native app. export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Check if you're in the app"} `bdk.isNative()` returns a `boolean` synchronously. Use it inline to gate any native-only feature and fall back to a web equivalent. ::callout{type="warn"} `isNative()` can read `false` for a moment at startup before device info arrives. If you need certainty on the first tick, await `ready()` (next section) instead. :: #code :::code-group ```ts [Guarding a native feature] if (bdk.isNative()) { // Inside the app: use the native photo picker. await bdk.media.pickPhoto(); } else { // Plain browser: fall back to a normal file input. document.querySelector("#file")?.click(); } ``` ```ts [Reading cached device info] // Returns the cached BdkDeviceInfo, or null when no native shell is present. const info = bdk.getDeviceInfo(); const platform = info?.deviceOS ?? "web"; ``` ::: :::: ::::ref-section{title="Check the environment synchronously"} `bdk.environment()` returns `"native"`, `"web"`, or `"unknown"` without waiting. On current app builds `"native"` is trustworthy immediately — even at page load — though device info may still be a moment away, so `await ready()` before reading it. Treat `"unknown"` as "wait" and fall back to `ready(timeoutMs)`. ::callout Pass `assumeWebWithoutSeed: true` to `createBdkNative()` once every installed build of your app is current. A plain browser then reports `"web"`, and `ready()` resolves `null` instantly instead of waiting out its timeout. :: Environment signals come from the client and can be spoofed — use them to shape UX, never for auth or entitlements. #code :::code-group ```ts [Instant check, then wait if unsure] if (bdk.environment() === "native") { showNativeChrome(); // shell confirmed; await bdk.ready() before reading device info } else { const info = await bdk.ready(3000); // "unknown": wait for the handshake info ? showNativeChrome() : renderWebHome(); } ``` ::: :::: ::::ref-section{title="Wait for the native handshake"} `bdk.ready(timeoutMs = 0)` resolves to `BdkDeviceInfo | null` and never rejects. A `null` result means you're outside the native app, so use it to branch your UI. - `ready()` / `ready(0)` resolves immediately: the cached `BdkDeviceInfo` if present, otherwise `null`. - `ready(timeoutMs)` waits up to that many milliseconds for the device to report in, then resolves to `null` if it never does. #code :::code-group ```ts [Wait, then branch on null] const info = await bdk.ready(3000); // wait up to 3s for the native handshake if (info === null) { // No native shell — render the web experience. renderWebHome(); } else { renderNativeHome(info); // info.deviceOS, info.versionName, info.playerId, ... } ``` ```ts [Immediate check, no waiting] const info = await bdk.ready(); // ready(0): resolves now, null if not cached yet console.log(info ? `native: ${info.deviceOS}` : "web fallback"); ``` ::: :::: ::::ref-section{title="Check whether a command ran"} Every native command resolves a `NativeCommandResult` — inspect it to know whether the app received the command. `triggered === true` means it ran; anything else means there's no native shell, so branch on `!result.triggered` for a web fallback. See [Objects](/reference/objects) for the full shape. Outside the app a command resolves with `triggered: false`: in a browser tab it's `pending` with `reason: "waiting_for_agent"`; with no `window` at all it's `skipped` with `reason: "not_native"`. A rejected command instead throws a `BdkError` with a code like `BDK_NATIVE_UNAVAILABLE`, `BDK_UNSUPPORTED_VERSION`, or `BDK_UNSUPPORTED_PLATFORM`. ::callout{type="warn"} Commands that return data (photos, location, pickers, biometrics, IAP) deliver it on an event — `capturePhoto` emits `photoCaptured`, `pickPhoto` emits `photoSelected`. Those events never fire in the browser, so always give such calls a non-native path and never block your UI on an event that can't arrive. :: #code :::code-group ```ts [Inspect the result] const result = await bdk.ui.showBanner({ title: "Saved" }); if (!result.triggered) { // No native shell: pending (browser tab) or skipped (no window). // result.reason tells you which ("waiting_for_agent" vs "not_native"). console.warn(`Native toast didn't run: ${result.reason}`); showWebToast("Saved"); // your own DOM toast } ``` ```ts [Branch on triggered] const result = await bdk.location.getCurrentPosition(); if (!result.triggered) { // No native shell — use the browser Geolocation API instead } else { // dispatched to the app; the actual coordinates arrive on the "location" event } ``` ::: :::: ::::ref-section{title="Build a web fallback"} Decide native vs. web once at startup with `ready()`, then route every native-only call through an `isNative()` guard with a browser equivalent behind it. Listeners are safe to attach everywhere — in the browser they simply stay dormant, and a listener that throws is surfaced as `BDK_LISTENER_ERROR` through `onError` rather than crashing the page. ::callout Never import `@bdk/native/server/*` into a browser bundle — those modules are server-only. Use `@bdk/native/browser` (or `@bdk/native`) on the client. :: #code :::code-group ```ts [A reusable capability helper] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); export async function bootstrap() { // One handshake at startup. null === running outside a native shell. const device = await bdk.ready(3000); return { isNative: device !== null, device }; } export async function pickAvatar() { if (bdk.isNative()) { // Native path: the file arrives on the "photoSelected" event. const off = bdk.on("photoSelected", (media) => { uploadAvatar(media.fileUrl ?? media.dataUri); off(); }); await bdk.media.pickPhoto(); return; } // Web path: a standard file input, no native dependency. openWebFilePicker(); } ``` ```ts [Error-safe listener] const bdk = createBdkNative({ onError: (err) => { // BDK_LISTENER_ERROR, BDK_NATIVE_UNAVAILABLE, etc. — never throws upward. console.warn(`[bdk] ${err.code}: ${err.message}`); } }); // Safe in the browser: this simply never fires without a native shell. bdk.on("location", (loc) => updateMap(loc)); ``` ::: :::: --- # Detect features URL: https://docs.thebdk.com/getting-started/capabilities ::::ref-section{title="Check one feature"} Ask whether this build has a feature switched on. `has` is `true` only then — the right check before you show the UI. #code :::code-group ```ts [Before showing UI] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); if (await bdk.capabilities.has("media")) { showPhotoPicker(); } ``` ::: :::: ::::ref-section{title="Read the full map"} Read every feature this build reports — `enabled`, `available`, and `config` per id — plus `platform` and the app version. Pass `timeoutMs` to wait that many milliseconds. `get` resolves `null` when the answer isn't known: a plain browser, an older app build, or a slow start. `null` is not off. ::callout{type="warn"} Don't treat `null` as off. Wait for `bdk.ready()` and call `get` again. :: #code :::code-group ```ts [Read the map] let caps = await bdk.capabilities.get(); if (!caps) { await bdk.ready(3000); caps = await bdk.capabilities.get(); } if (!caps) { // Still unknown — not off. } else { const media = caps.features.media; console.log(caps.platform, caps.app?.versionName, media); } ``` ::: :::: ::::ref-section{title="Interpret the states"} `enabled`, `available`, and a `null` map are three different answers. - Missing from `features`, or `enabled` is not `true` — not in this build. Hide the UI. - `enabled` is `true` and `available` is `false` — in this build, but unusable on this device right now. - `get()` returned `null` — unknown. Don't treat it as off. Wait for `bdk.ready()` and try again. :::: ::::ref-section{title="When a feature is off"} A call to a switched-off feature resolves `ok: false` with `code: "common/feature_disabled"`. Branch on `!result.ok`. Each feature page names its id. In a plain browser the call resolves `ok: false` with a code such as `common/feature_disabled`. Branch on `!result.ok`. #code :::code-group ```ts [Branch on disabled] const result = await bdk.media.pickPhotos(); if (!result.ok) { if (result.code === "common/feature_disabled") { // This build doesn't have it. return; } console.error(result.code, result.message); return; } ``` ::: :::: --- # Device & lifecycle URL: https://docs.thebdk.com/browser/device-and-lifecycle ::::ref-section{title="Read device info"} Get device facts like `playerId`, `deviceOS`, `versionName`, or a permission status. `createBdkNative()` requests device info on init — read it with `getDeviceInfo()`, `await bdk.ready(timeoutMs)`, or the `deviceInfo` event. `bdk.getDeviceInfo()` returns the latest `BdkDeviceInfo`, or `null` if it hasn't arrived yet (for example, in a plain browser or before the first `deviceInfo` event). `BdkDeviceInfo` fields (each `string` or `null` unless noted): | Property | Type | Description | | --- | --- | --- | | `playerId` | `string` | OneSignal player id for push. | | `pushToken` | `string` | Device push token. | | `deviceModel` | `string` | Device model name. | | `deviceOS` | `string` | Operating system. | | `deviceOSVersion` | `string` | OS version. | | `bdkRelease` | `string \| number` | BDK runtime release. | | `deviceLanguage` | `string` | Device language. | | `deviceWidth` | `string \| number` | Screen width. | | `deviceHeight` | `string \| number` | Screen height. | | `versionName` | `string` | Host app version name. | | `versionCode` | `string \| number` | Host app version code. | | `biometricsAvailable` | `boolean` | Whether biometric login is available. | | `smartLoginAvailable` | `boolean` | Whether smart login is available. | | `cameraPermissionStatus` | `string` | Camera permission state. | | `contactsPermissionStatus` | `string` | Contacts permission state. | | `audiorecordPermissionStatus` | `string` | Audio-record permission state. | | `externalstoragePermissionStatus` | `string` | External-storage permission state. | | `locationPermissionStatus` | `string` | Location permission state. | | `idfa` | `string` | iOS advertising id. iOS only. | ::callout Wait for the value with `await bdk.ready(timeoutMs)`, or subscribe to the `deviceInfo` event — at startup and after a permission prompt. :: #code :::code-group ```ts [Read a field] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const info = bdk.getDeviceInfo(); if (info) { console.log(info.playerId, info.deviceOS, info.versionName); } ``` ```ts [Wait for it] const info = await bdk.ready(3000); // BdkDeviceInfo | null ``` ```ts [Listen] bdk.on("deviceInfo", (info) => { console.log(info.locationPermissionStatus); }); ``` ::: :::: ::::ref-section{title="Detect the native app"} Check whether your page is running inside the native app. Branch on it to enable native-only UI or fall back to web behavior. `bdk.isNative()` returns `true` once device info is available, `false` otherwise. ::callout{type="warn"} `isNative()` reads `false` until device info arrives, so a check at page load takes the web branch even inside the app. `await bdk.ready(timeoutMs)` first — it resolves as soon as the device reports in, or `null` at the timeout — then branch. :: #code :::code-group ```ts [Branch on environment] const info = await bdk.ready(3000); // resolves early once the device reports in if (info) { await bdk.device.vibrate(); } else { // web fallback } ``` ::: :::: ::::ref-section{title="Hide the loading splash"} Dismiss the loading splash once your page is ready. Use this in `"manual"` mode to control exactly when the UI appears; with the default `removeLoading: "automatic"` you don't need to call it. `bdk.app.removeLoading()`. #code :::code-group ```ts [Manual dismiss] const bdk = createBdkNative({ removeLoading: "manual" }); // ...once your UI is mounted: await bdk.app.removeLoading(); ``` ::: :::: ::::ref-section{title="Restyle the loading splash"} Change the appearance of the loading splash. `bdk.app.updateLoading(options)`. Recognized `LoadingScreenOptions` keys: `splash_layer_url`, `splash_layer_width`, `splash_layer_top`, `splash_layer_left`, and `splash_section_background`. #code :::code-group ```ts [Custom splash] await bdk.app.updateLoading({ splash_layer_url: "https://cdn.example.com/splash.png", splash_layer_width: "60%", splash_layer_top: "40%", splash_layer_left: "20%", splash_section_background: "#0B0B0B" }); ``` ::: :::: ::::ref-section{title="Go back"} Navigate back, the equivalent of the native back action. `bdk.app.goBack()`. #code :::code-group ```ts [Back] await bdk.app.goBack(); ``` ::: :::: ::::ref-section{title="Change the launch page"} Set which URL the app opens on next launch, then clear it when done. `bdk.app.setLaunchPage(options)` sets the alternate launch URL. `bdk.app.resetLaunchPage()` removes the override and restores the default. #code :::code-group ```ts [Set] await bdk.app.setLaunchPage({ url: "https://app.example.com/home" }); ``` ```ts [Reset] await bdk.app.resetLaunchPage(); ``` ::: :::: ::::ref-section{title="Vibrate the device"} Trigger device haptics. `bdk.device.vibrate(options?)`. #code :::code-group ```ts [Vibrate] await bdk.device.vibrate(); ``` ::: :::: ::::ref-section{title="Read the device contacts"} Fetch the device address book. Ask for the `contacts` permission first. For structured results and paging, use `contacts.list`. `bdk.device.getContacts()`. ::callout{type="warn"} The device address book arrives on the **`contacts`** event — an array of contacts, each with a name, phone number(s), and email(s) — not in the returned promise. Subscribe before you call. :: #code :::code-group ```ts [Get contacts] bdk.on("contacts", (addressBook) => { console.log("contacts payload", addressBook); }); await bdk.permissions.ask("contacts"); await bdk.device.getContacts(); ``` ::: :::: ::::ref-section{title="Save and read cache values"} Persist small values in native storage and read them back later — handy for flags like onboarding state. `bdk.device.saveToCache(options)` and `bdk.device.getFromCache(options)`. ::callout{type="warn"} The cached value arrives on the **`deviceVariable`** event as a `DeviceVariableResult` with the variable's `name` and its stored `data` value, not in the returned promise. Subscribe before you read. If the key was never stored, `data` is `null`. :: #code :::code-group ```ts [Save] await bdk.device.saveToCache({ key: "onboarded", value: "true" }); ``` ```ts [Read] bdk.on("deviceVariable", ({ name, data }) => { if (name === "onboarded") { console.log("cached value", data); } }); await bdk.device.getFromCache({ key: "onboarded" }); ``` ::: :::: ::::ref-section{title="Lifecycle events reference"} Subscribe with `bdk.on(event, listener)`, which returns an unsubscribe function. The events: - **`deviceInfo`** → `BdkDeviceInfo`. The shell reported device info; also feeds `getDeviceInfo()` and `ready()`. - **`contacts`**. The device address book from `device.getContacts()` — an array of contacts, each with a name, phone number(s), and email(s). - **`deviceVariable`** → `DeviceVariableResult` with the variable's `name` and its stored `data` value. A cached value from `device.getFromCache(...)`. - **`backButtonPressed`** → `undefined`. The hardware back button was pressed. ::callout{type="warn"} If a listener throws, the error surfaces once as a `BdkError` with code `BDK_LISTENER_ERROR`, delivered to your `onError` config callback and the `error` event. :: #code :::code-group ```ts [Subscribe + unsubscribe] const offDeviceInfo = bdk.on("deviceInfo", (info) => console.log(info.deviceModel)); const offContacts = bdk.on("contacts", (book) => console.log(book)); const offVariable = bdk.on("deviceVariable", ({ name, data }) => console.log(name, data)); const offBack = bdk.on("backButtonPressed", () => console.log("back pressed")); // call the returned functions to stop listening offDeviceInfo(); offContacts(); offVariable(); offBack(); ``` ::: :::: ::::ref-section{title="Pick a contact"} Open the system contact picker. It does not prompt for address-book permission. On iOS, pass `multiple: true` to pick more than one. Each item is a [`BdkContact`](/reference/objects) with structured `phones` and `emails`. This feature can be switched off in a given app build — check `await bdk.capabilities.has("contacts.picker")` before showing the picker. See [Detect features](/getting-started/capabilities). If the feature is off, the call throws. | Property | Type | Description | | --- | --- | --- | | `multiple` | `boolean` | Pick more than one contact. iOS only. | #code :::code-group ```ts [Pick one] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // The picker throws when the feature is off in this build — check first. if (await bdk.capabilities.has("contacts.picker")) { const result = await bdk.contacts.pick(); if (!result.ok) { console.error(result.code, result.message); } else { for (const contact of result.contacts) { console.log(contact.name, contact.phones, contact.emails); } } } ``` ```ts [Pick several] const result = await bdk.contacts.pick({ multiple: true }); // iOS only if (result.ok) { console.log(result.contacts.length); } ``` ::: :::: ::::ref-section{title="List contacts"} Read a page of the device address book. This needs contacts permission; the picker above does not. Native accepts `limit` 1–200 and pages with `offset`; the result has `access` (`full` or `limited`), `total`, `hasMore`, and `nextOffset`. Check `await bdk.capabilities.has("contacts.book")` before showing the list. If the feature is off, the call throws. | Property | Type | Description | | --- | --- | --- | | `limit` | `number` | Page size, 1–200. | | `offset` | `number` | Zero-based start of the page. | #code :::code-group ```ts [First page] // list() throws when the feature is off in this build — check first. if (await bdk.capabilities.has("contacts.book")) { const result = await bdk.contacts.list({ limit: 50, offset: 0 }); if (!result.ok) { console.error(result.code, result.message); } else { console.log(result.access, result.total, result.contacts); if (result.hasMore && result.nextOffset != null) { const next = await bdk.contacts.list({ limit: 50, offset: result.nextOffset }); if (next.ok) console.log(next.contacts); } } } ``` ::: :::: --- # Open links URL: https://docs.thebdk.com/browser/navigation-and-links ::::ref-section{title="Open a link"} Open a URL in an in-app web view, an external app, or a native screen. Set `view` to choose the destination. URLs are normalized by default (missing `https://` is added, `urlParams` applied); pass `useRawLink: true` to skip that. With `view: "website"`, iOS opens an in-app browser sheet and Android hands the URL to the device's default browser. | Property | Type | Description | | --- | --- | --- | | `link` | `string` · required | The URL to open. | | `view` | `string` | Where to open it: `website`, `another app`, or `another screen`. | | `transition` | `string` | The screen transition animation. | | `useRawLink` | `boolean` | Open the link exactly as given, skipping URL normalization. | | `urlParams` | `UrlParam[]` | Query parameters to append to the URL. Each is `{ key, value }`. | #code :::code-group ```ts [Basic] await bdk.navigation.openLink({ link: "https://example.com/pricing", view: "website" }); ``` ```ts [With options] await bdk.navigation.openLink({ link: "example.com/account", // protocol added automatically view: "website", transition: "slide", urlParams: [ { key: "ref", value: "app" }, { key: "plan", value: "pro" } ] }); ``` ```ts [Raw URL] await bdk.navigation.openLink({ link: "myapp://deep/link?id=42", view: "another app", useRawLink: true }); ``` ::: :::: ::::ref-section{title="Navigate within the app"} Move to another screen inside the app. | Property | Type | Description | | --- | --- | --- | | `url` | `string` · required | The in-app screen to move to. | | `baseUrl` | `string` | The app's base URL, used to resolve relative links. | #code :::code-group ```ts [Navigate] await bdk.navigation.navigate({ url: "https://example.com/account", baseUrl: "example.com" }); ``` ::: :::: ::::ref-section{title="Fall back to web navigation"} These calls resolve to a `NativeCommandResult` (see [Handling results](/getting-started/interaction-models) for its fields). Outside the app `triggered` is `false` — use that to fall back to ordinary web navigation. #code :::code-group ```ts [Web fallback] const result = await bdk.navigation.openLink({ link: "https://example.com/pricing", view: "website" }); if (!result.triggered) { window.open("https://example.com/pricing", "_blank"); } ``` ::: :::: --- # Deep links URL: https://docs.thebdk.com/browser/deep-links ::::ref-section{title="Listen for deep links"} Subscribe with `onReceived` to handle custom URI-scheme links — it fires for warm deliveries and replays one unconsumed cold-start link, deduped by `id`. Don't also call `get()` for the same launch. Branch dynamic links are a separate server feature — see [Branch deep links](/server/branch). The payload is a [`BdkDeepLinkRecord`](/reference/objects). Path-form links (`form: "path"`) are for you to route. URL-form links are already applied by native — don't navigate again. This feature can be switched off in a given app build — check `await bdk.capabilities.has("deeplink.scheme")` before showing the UI. See [Detect features](/getting-started/capabilities). When it's off, `get()` resolves `ok: false` with `code: "common/feature_disabled"`. #code :::code-group ```ts [Warm and cold] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const off = bdk.deeplink.onReceived((link) => { if (link.form === "path") { routeTo("/" + link.path, link.query); return; } // URL-form: native already opened targetUrl. Don't navigate again. console.log(link.targetUrl, link.coldStart); }); // later off(); ``` ::: :::: ::::ref-section{title="Read a pending link"} Call `get()` to consume the stored link — use this when you are not using `onReceived`. `received` is `false` when there is none. A received record includes `coldStart` (whether this link launched the app) and `consumed` (whether a previous `get()` already took it). #code :::code-group ```ts [Consume once] const result = await bdk.deeplink.get(); if (!result.ok) { console.warn(result.code, result.message); // e.g. common/feature_disabled } else if (!result.received) { console.log("No pending link"); } else { console.log(result.form, result.coldStart, result.consumed); } ``` ::: :::: ::::ref-section{title="List your schemes"} `schemes()` returns the app's registered URI schemes in preference order. It returns `[]` when the capability is absent. #code :::code-group ```ts [Registered schemes] const schemes = await bdk.deeplink.schemes(); const preferred = schemes[0]; // undefined when this build has no scheme ``` ::: :::: ::::ref-section{title="Build a link"} `buildLink` encodes a URL-form or path-form link with the first registered scheme. URL-form is `scheme://open?url=…`. Path-form is `scheme://path?query`. It returns `null` when the build has no scheme. #code :::code-group ```ts [URL form] const openPage = await bdk.deeplink.buildLink({ url: "https://app.example.com/orders/123" }); // com.example.app://open?url=https%3A%2F%2Fapp.example.com%2Forders%2F123 ``` ```ts [Path form] const route = await bdk.deeplink.buildLink({ path: "orders/123", query: { tab: "details" } }); // com.example.app://orders/123?tab=details if (!route) { // this build has no scheme } ``` ::: :::: --- # Camera & media URL: https://docs.thebdk.com/browser/media ::::ref-section{title="Take a photo"} Open the camera to take a photo. The image arrives on the `photoCaptured` event, so subscribe before you call, and ask for camera permission first. ::callout `capturePhoto` emits `photoCaptured`; `pickPhoto` emits `photoSelected`. They are not interchangeable. :: #code :::code-group ```ts [Basic] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); bdk.on("photoCaptured", (photo) => { console.log(photo.fileUrl, photo.contentType); // MediaResult }); await bdk.permissions.ask("camera"); await bdk.media.capturePhoto(); ``` ::: :::: ::::ref-section{title="Use the photo result"} The `photoCaptured` listener receives a `MediaResult`. Use `dataUri` to preview inline and `fileUrl` to upload. `bdk.on(...)` returns an unsubscribe function. | Property | Type | Description | | --- | --- | --- | | `fileUrl` | `string \| null` | Hosted URL of the file — use this to upload. | | `dataUri` | `string \| null` | Data URI — use this to preview inline. | | `contentType` | `string \| null` | The file's MIME type. | | `data` | — | The raw file data from the device. | #code :::code-group ```ts [Preview + upload] const off = bdk.on("photoCaptured", (photo) => { if (photo.dataUri) { (document.querySelector("#preview") as HTMLImageElement).src = photo.dataUri; } if (photo.fileUrl) { void uploadAvatar(photo.fileUrl); } }); await bdk.media.capturePhoto(); off(); // when done ``` ::: :::: ::::ref-section{title="Pick from the photo library"} Open the photo library instead of the camera. Results arrive on the `photoSelected` event, not `photoCaptured`. #code :::code-group ```ts [Pick] bdk.on("photoSelected", (photo) => console.log("picked", photo.fileUrl)); await bdk.media.pickPhoto(); ``` ::: :::: ::::ref-section{title="Take a screenshot"} Capture a screenshot of the current screen. The image arrives on the `screenshot` event. #code :::code-group ```ts [Screenshot] bdk.on("screenshot", (image) => console.log("captured", image)); await bdk.media.captureScreenshot(); ``` ::: :::: ::::ref-section{title="Record audio"} Open the native audio recorder. The recording arrives on the `audioRecorded` event as a `MediaResult` — use `fileUrl` to upload it. #code :::code-group ```ts [Record] bdk.on("audioRecorded", (audio) => { if (audio.fileUrl) void uploadClip(audio.fileUrl); // MediaResult }); await bdk.media.recordAudio(); ``` ::: :::: ::::ref-section{title="Scan a barcode"} Open the scanner for QR codes and barcodes. The decoded value arrives on the `barcodeScanned` event. #code :::code-group ```ts [Scan] bdk.on("barcodeScanned", (code) => console.log("scanned", code)); await bdk.media.scanBarcode(); ``` ::: :::: ::::ref-section{title="Fall back on the web"} Outside the app the call resolves with `triggered: false` and `photoCaptured` never fires. Detect this and show a standard `` instead. #code :::code-group ```ts [Detect non-native] const result = await bdk.media.capturePhoto(); if (!result.triggered) { document.querySelector("#file-input")?.removeAttribute("hidden"); } ``` ::: :::: ::::ref-section{title="Pick multiple photos"} Open the system photo picker so the user can select more than one image. Unlike the helpers above, the photos arrive on the awaited call — there is no event. | Property | Type | Description | | --- | --- | --- | | `limit` | `number` | Max photos to select. Native accepts 1–30. | | `result` | `string` | How to return each photo: `base64` or `fileUrl`. | | `maxDimensionPx` | `number` | Longest edge in pixels. | | `jpegQuality` | `number` | JPEG quality from 0.1 through 1.0. | Success is `{ ok: true, selectedCount, succeededCount, failedCount, items }` — each item is a JPEG [`BdkMediaItem`](/reference/objects). If the user cancels, the call resolves `ok: false` with `code: "common/cancelled"`; treat that as a normal outcome. ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("media")` before showing the UI — see [Detect features](/getting-started/capabilities). When it's off, the call resolves `ok: false` with `code: "common/feature_disabled"`. Builds can cap the limit and disable file URLs. :: #code :::code-group ```ts [Pick several] if (!(await bdk.capabilities.has("media"))) return; const result = await bdk.media.pickPhotos({ limit: 5, result: "base64", maxDimensionPx: 2048, jpegQuality: 0.8 }); if (!result.ok) { if (result.code === "common/cancelled") return; // user dismissed the picker console.error(result.code, result.message); return; } console.log(result.selectedCount, result.succeededCount, result.failedCount); for (const item of result.items) { console.log(item.width, item.height, item.name); // BdkMediaItem } ``` ::: :::: ::::ref-section{title="Take a photo from the camera"} Open the system camera and take one photo. The image arrives on the awaited call as a single [`BdkMediaItem`](/reference/objects) — no event. `camera` is `back` or `front`; on Android it is a best-effort hint. The same `result`, `maxDimensionPx`, and `jpegQuality` options as [Pick multiple photos](#pick-multiple-photos) apply. #code :::code-group ```ts [Take one] const result = await bdk.media.takePhoto({ camera: "front" }); if (!result.ok) { if (result.code === "common/cancelled") return; console.error(result.code, result.message); return; } console.log(result.item.width, result.item.height); // BdkMediaItem ``` ::: :::: ::::ref-section{title="Show a photo"} Turn a picked or captured item into a data URI you can put in an ``. The call is synchronous and returns `null` when the item is file-URL-only. #code :::code-group ```ts [Preview] const uri = bdk.media.toDataUri(item); if (uri) { (document.querySelector("#preview") as HTMLImageElement).src = uri; } ``` ::: :::: --- # Play audio & video URL: https://docs.thebdk.com/browser/media-playback ::::ref-section{title="Play audio"} Play a remote audio track in the native player. #code :::code-group ```ts [Play] const bdk = createBdkNative(); await bdk.media.playAudio({ url: "https://example.com/tracks/song.mp3" }); ``` ::: :::: ::::ref-section{title="Pause and stop audio"} Pause the current track or stop playback entirely. #code :::code-group ```ts [Pause / stop] await bdk.media.pauseAudio(); await bdk.media.stopAudio(); ``` ::: :::: ::::ref-section{title="Play a video"} Play a remote video in the native player. Pass `muted: true` to start playback muted. #code :::code-group ```ts [Play video] await bdk.media.playVideo({ url: "https://example.com/clips/demo.mp4" }); ``` ::: :::: ::::ref-section{title="Play a video playlist"} Play a sequence of videos back to back. Pass `muted: true` to start playback muted. ::callout{type="warn"} iOS only — wrap the call in try/catch. :: #code :::code-group ```ts [Playlist] await bdk.media.playVideoPlaylist({ videos: [ "https://example.com/clips/one.mp4", "https://example.com/clips/two.mp4" ], muted: true }); ``` ::: :::: --- # Geolocation URL: https://docs.thebdk.com/browser/location ::::ref-section{title="Get started"} Readings are delivered on the `location` event as a `"latitude,longitude"` string or an object with `latitude` and `longitude`, so subscribe before you call. Request the `location` permission first. #code :::code-group ```ts [Setup] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // Subscribe first — the position is delivered to this event, not the promise. bdk.on("location", (info) => { if (typeof info === "string") { const [latitude, longitude] = info.split(",").map(Number); console.log(latitude, longitude); } else { console.log(info.latitude, info.longitude); } }); await bdk.permissions.ask("location"); ``` ::: :::: ::::ref-section{title="Get the current position"} Read the device's latest fix. The reading arrives on the `location` event. Start tracking first — without it there's no fix to read yet. #code :::code-group ```ts [Latest fix] bdk.on("location", (info) => { console.log("current position", info); }); await bdk.location.getCurrentPosition(); ``` ::: :::: ::::ref-section{title="Follow the device while the app is open"} Start foreground tracking to keep the device's position up to date, then read it whenever you need it. Call `stopForegroundTracking()` to end it. ::callout Tracking keeps the fix fresh; it doesn't push updates to your page. Poll `getCurrentPosition()` for each reading you want. :: #code :::code-group ```ts [Follow the device] const off = bdk.on("location", (info) => { console.log("moving", info); }); await bdk.location.startForegroundTracking(); // read the latest fix whenever you need it const timer = setInterval(() => bdk.location.getCurrentPosition(), 3000); // later, to stop: clearInterval(timer); off(); ``` ::: :::: ::::ref-section{title="Stop a foreground stream"} Stop an active foreground stream. #code :::code-group ```ts [Stop tracking] await bdk.location.stopForegroundTracking(); ``` ::: :::: ::::ref-section{title="Keep tracking in the background"} Keep receiving location updates while the app is backgrounded. On a successful start, `backgroundLocationEnabled` fires with `{ enabled, alreadyRunning, reason }`. ::callout Android only — wrap the call in try/catch. :: #code :::code-group ```ts [Enable on Android] bdk.on("backgroundLocationEnabled", ({ enabled, alreadyRunning, reason }) => { console.log({ enabled, alreadyRunning, reason }); }); try { await bdk.location.enableBackground({ id: "https://example.com/location", interval: 15 }); } catch { } ``` ::: :::: ::::ref-section{title="Stop background tracking"} Stop background updates. On a successful stop, `backgroundLocationDisabled` fires with `{ enabled }`. Android only — wrap the call in try/catch. #code :::code-group ```ts [Disable on Android] bdk.on("backgroundLocationDisabled", ({ enabled }) => { console.log("background location enabled?", enabled); }); try { await bdk.location.disableBackground(); } catch { } ``` ::: :::: ::::ref-section{title="Events"} All position data arrives on events. `bdk.on(...)` returns an unsubscribe function. - `location` — every position reading (one-shot or streamed): a `"latitude,longitude"` string or an object with `latitude` and `longitude`. - `backgroundLocationEnabled` — `{ enabled, alreadyRunning, reason }`. - `backgroundLocationDisabled` — `{ enabled }`. #code :::code-group ```ts [Subscribe to everything] const offs = [ bdk.on("location", (info) => console.log("location", info)), bdk.on("backgroundLocationEnabled", (p) => console.log("bg on", p)), bdk.on("backgroundLocationDisabled", (p) => console.log("bg off", p)) ]; // Clean up all listeners at once. offs.forEach((off) => off()); ``` ::: :::: --- # Runtime permissions URL: https://docs.thebdk.com/browser/permissions ::::ref-section{title="Request a permission"} Show the native OS permission prompt for one capability. Call it right before you use the feature. `permission` is one of `"camera"`, `"contacts"`, `"record audio"`, `"write to storage"`, or `"location"`. The resolved promise tells you the request was sent, not whether the user said yes — read the current state with `permissions.status()`, or listen for the `permissions.changed` event. ::callout{type="warn"} The grant/deny outcome arrives on the `permissions.changed` event. Read the current state with `permissions.status()`. :: #code :::code-group ```ts [Ask once] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // Fires the native prompt. The resolved value is a dispatch // receipt, NOT whether the user granted access. await bdk.permissions.ask("camera"); ``` ```ts [All permission names] import type { PermissionName } from "@bdk/native/browser"; const all: PermissionName[] = [ "camera", "contacts", "record audio", "write to storage", "location" ]; for (const permission of all) { await bdk.permissions.ask(permission); } ``` ::: :::: ::::ref-section{title="Gate a feature behind a permission"} Check the current status, prompt only if it isn't granted, then wait for the `deviceInfo` event to confirm before using the feature. Get the current snapshot from `bdk.ready()` or `bdk.getDeviceInfo()`; the `deviceInfo` event delivers the updated one after the user answers. To read the real OS authorization state, use `status()`. Status field names don't match the `PermissionName` strings: - `"camera"` → `cameraPermissionStatus` - `"contacts"` → `contactsPermissionStatus` - `"record audio"` → `audiorecordPermissionStatus` - `"write to storage"` → `externalstoragePermissionStatus` - `"location"` → `locationPermissionStatus` #code :::code-group ```ts [Gate a camera feature] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); async function openCamera() { const info = await bdk.ready(); if (info?.cameraPermissionStatus === "granted") { bdk.media.capturePhoto(); return; } // Ask, then wait for the refreshed deviceInfo to confirm. const off = bdk.on("deviceInfo", (next) => { if (next.cameraPermissionStatus === "granted") { off(); bdk.media.capturePhoto(); } }); await bdk.permissions.ask("camera"); } ``` ```ts [Check a status field] const info = bdk.getDeviceInfo(); // Field name differs from the PermissionName string. if (info?.audiorecordPermissionStatus !== "granted") { await bdk.permissions.ask("record audio"); } ``` ::: :::: ::::ref-section{title="Check permission status"} Read the current OS authorization map — the full set, or a filtered list via `types`. This reads state; it does not prompt. Canonical types: `push`, `location`, `camera`, `photos`, `microphone`, `contacts`, and `tracking` (iOS). Each entry has `status` (`granted`, `denied`, `notDetermined`, `limited`, `provisional`, `restricted`, `unsupported`), `canPrompt`, and an optional `detail` with location `precision` (`precise` or `approximate`) and `scope` (`whenInUse` or `always`). Older permission names are aliased to their canonical keys on the device; the result's `aliasesApplied` map reports any renames. This feature can be switched off in a given app build — check `await bdk.capabilities.has("permissions")` before showing the UI. See [Detect features](/getting-started/capabilities). When it's off, the call resolves `ok: false` with `code: "common/feature_disabled"`. | Property | Type | Description | | --- | --- | --- | | `types` | `string[]` | Subset to report. Omit for the full map. | ::callout A granted `camera` status is OS authorization only — it does not guarantee `getUserMedia()` will succeed. :: #code :::code-group ```ts [Full map] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); if (!(await bdk.capabilities.has("permissions"))) { // Feature isn't enabled in this build } const result = await bdk.permissions.status(); if (!result.ok) { console.error(result.code, result.message); } else { console.log(result.platform, result.permissions.camera?.status); } ``` ```ts [Filtered] const result = await bdk.permissions.status({ types: ["camera", "photos", "microphone"] }); if (result.ok) { const camera = result.permissions.camera; console.log(camera?.status, camera?.canPrompt, camera?.detail); } ``` ::: :::: ::::ref-section{title="React to changes"} The `permissions.changed` event fires after a prompt and after the user returns from settings — not on the first snapshot. Subscribe before you ask or open settings. The payload has the `changed` type names and the full refreshed `permissions` map. #code :::code-group ```ts [Subscribe first] bdk.on("permissions.changed", ({ changed, permissions }) => { console.log(changed, permissions.camera?.status); }); await bdk.permissions.ask("camera"); ``` ::: :::: ::::ref-section{title="Open the settings screen"} Open the OS settings page for this app (`section: "app"`) or its notifications (`section: "notifications"`). The resolved call only means the screen opened — the outcome arrives on `permissions.changed` after the user returns. Subscribe before you call. | Property | Type | Description | | --- | --- | --- | | `section` | `string` | `app` or `notifications`. | #code :::code-group ```ts [Open notifications settings] bdk.on("permissions.changed", ({ changed, permissions }) => { console.log("after settings", changed, permissions); }); const result = await bdk.permissions.openSettings({ section: "notifications" }); if (!result.ok) { console.error(result.code, result.message); } ``` ::: :::: ::::ref-section{title="App tracking (iOS)"} Read or request Apple's tracking authorization, then read `idfa` when it is granted. iOS only — on Android the call resolves `ok: false` with `code: "common/unsupported_platform"`. Statuses: `granted`, `denied`, `restricted`, `notDetermined`. `idfa` is a string when authorized, otherwise `null`. A denial still resolves `ok: true` — it's an answer, not an error. Check `await bdk.capabilities.has("att")` before showing the UI. When the feature is off, the call resolves `ok: false` with `code: "common/feature_disabled"`. Subscribe to `att.changed` for later transitions. ::callout Present a short explainer, then call `request()` when `canPrompt` is true. `prompted: true` means a request was issued, not that the system dialog appeared. :: #code :::code-group ```ts [Status then request] if (!(await bdk.capabilities.has("att"))) { // Feature isn't enabled in this build } const current = await bdk.att.status(); if (!current.ok) { console.error(current.code, current.message); } else if (current.canPrompt) { const result = await bdk.att.request(); if (result.ok) { // denied is still ok: true console.log(result.status, result.idfa); } } ``` ```ts [Listen for changes] bdk.on("att.changed", ({ status, idfa }) => { console.log(status, idfa); }); ``` ::: :::: --- # Notifications & badges URL: https://docs.thebdk.com/browser/notifications ::::ref-section{title="Check notification status"} Read the current notification authorization plus time-sensitive and critical-alert support. Safe to call even when those optional features are off — they report `supported: false`. | Property | Type | Description | | --- | --- | --- | | `authorization` | `string` | `authorized`, `denied`, `notDetermined`, `provisional`, or `ephemeral`. | | `timeSensitive` | `object` | `{ supported, setting?, channelId?, channelImportance?, channelEnabled? }`. | | `critical` | `object` | `{ supported, authorization?, canRequest? }`. | | `platform` | `string` | `ios` or `android`. | In a plain browser the call resolves `ok: false` with a code such as `common/feature_disabled`. Branch on `!result.ok`. #code :::code-group ```ts [Status] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const result = await bdk.notifications.status(); if (!result.ok) { return; } console.log(result.authorization, result.platform); console.log(result.timeSensitive.supported, result.critical.supported); ``` ::: :::: ::::ref-section{title="Ask for push permission"} Read the current push authorization, then request it. `granted` is `true` for both `granted` and `provisional`. When `mode` is `manual`, show your own explainer first, then call `request()`. | Property | Type | Description | | --- | --- | --- | | `status` | `string` | `granted`, `denied`, `notDetermined`, or `provisional`. | | `granted` | `boolean` | True for `granted` and `provisional`. | | `canPrompt` | `boolean` | Whether `request()` can still ask the user. | | `canOpenSettings` | `boolean` | Whether the app can open notification settings. | | `mode` | `string` | `automatic` or `manual`. | ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("push.prompt")` before showing the UI. When it is off, the call resolves `ok: false` with `code: "common/feature_disabled"`. See [Detect features](/getting-started/capabilities). :: #code :::code-group ```ts [Check then request] if (!(await bdk.capabilities.has("push.prompt"))) { // Hide the prompt UI — this build does not include it. } const state = await bdk.push.status(); if (!state.ok) { return; } if (state.granted) { // Already allowed — includes provisional. } else if (state.canPrompt) { const result = await bdk.push.request(); if (!result.ok) return; console.log(result.status, result.granted); } ``` ```ts [Manual mode] const state = await bdk.push.status(); if (!state.ok) return; if (state.mode === "manual" && state.canPrompt) { // Show your own explainer, then request. const result = await bdk.push.request(); if (!result.ok) return; console.log(result.status, result.granted); } ``` ::: :::: ::::ref-section{title="Open notification settings"} Send the user to the OS notification settings after a denial, or to change time-sensitive and channel options. `notifications.openSettings()` is the general deep-link; pass `{ channel: "urgent" }` on Android to open the urgent channel. `push.openSettings()` is the same idea from the push-prompt namespace — no channel option. #code :::code-group ```ts [Notification settings] const result = await bdk.notifications.openSettings(); if (!result.ok) return; // result.opened === true — the OS settings screen was requested. ``` ```ts [Android urgent channel] const result = await bdk.notifications.openSettings({ channel: "urgent" }); if (!result.ok) return; ``` ```ts [Push settings] const result = await bdk.push.openSettings(); if (!result.ok) return; ``` ::: :::: ::::ref-section{title="Request critical alerts"} Ask for Apple's separate critical-alert permission. iOS only — it does not consume the ordinary push prompt. On Android the call resolves `ok: false` with `code: "common/unsupported_platform"`. ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("notifications.critical")` before showing the UI. When it is off, the call resolves `ok: false` with `code: "common/feature_disabled"`. See [Detect features](/getting-started/capabilities). :: #code :::code-group ```ts [Request] if (!(await bdk.capabilities.has("notifications.critical"))) { // Hide the critical-alert UI. } const result = await bdk.notifications.requestCritical(); if (!result.ok) { // common/unsupported_platform on Android; common/feature_disabled when off. return; } console.log(result.granted, result.critical, result.authorization); ``` ::: :::: ::::ref-section{title="Set the app badge"} Read, set, or clear the app-icon badge. iOS sets an exact count. Android `set` is unsupported (`ok: false`, `code: "common/unsupported_platform"`); `get` is an approximate count of active notifications (`source: "active_notifications"`, `approximate: true`). `clear` works on both — on Android it reports how many cancellable notifications were removed. Pass a finite non-negative integer to `set`. Check `canSet()` before offering a setter. ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("badge")` before showing the UI. When it is off, the call resolves `ok: false` with `code: "common/feature_disabled"`. See [Detect features](/getting-started/capabilities). :: #code :::code-group ```ts [Set on iOS] if (await bdk.badge.canSet()) { const result = await bdk.badge.set(5); if (!result.ok) return; } ``` ```ts [Get and clear] const current = await bdk.badge.get(); if (!current.ok) return; console.log(current.count, current.source, current.approximate); const cleared = await bdk.badge.clear(); if (!cleared.ok) return; console.log(cleared.clearedNotifications); ``` ::: :::: ::::ref-section{title="React to permission changes"} Refresh status after the user answers a prompt or returns from Settings. `permissions.changed` fires when a tracked permission changes — not on the first snapshot — so subscribe before you call `request` or `openSettings`. | Property | Type | Description | | --- | --- | --- | | `changed` | `string[]` | Type names that changed — includes `push` when notification authorization changes. | | `previous` | `object` | Prior status per changed type. | | `permissions` | `object` | Full current permission map. | #code :::code-group ```ts [Refresh on change] bdk.on("permissions.changed", async ({ changed }) => { if (!changed.includes("push")) return; const push = await bdk.push.status(); if (push.ok) console.log(push.status, push.granted); const notifications = await bdk.notifications.status(); if (notifications.ok) console.log(notifications.authorization); }); await bdk.push.request(); // or: await bdk.notifications.openSettings(); ``` ::: :::: --- # Background data messages URL: https://docs.thebdk.com/browser/data-messages ::::ref-section{title="Listen for data messages"} Subscribe for a data payload as it arrives. These are data messages, not user-visible notifications — the result arrives on `push.dataReceived`, so subscribe before you read the queue. Each live event is a [`BdkPushDataMessage`](/reference/objects) without `delivered`. `appState` is `foreground`, `background`, or `cold`. | Property | Type | Description | | --- | --- | --- | | `id` | `string` | Message id. | | `receivedAt` | `number` | When the device received it. | | `appState` | `string` | `foreground`, `background`, or `cold`. | | `data` | `object` | The payload your provider sent. | ::callout{type="warn"} Subscribe to `push.dataReceived` before you call `getDataMessages()`, or a live delivery can be missed. :: #code :::code-group ```ts [Subscribe then read] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const off = bdk.on("push.dataReceived", (message) => { console.log(message.id, message.appState, message.data); }); const queued = await bdk.push.getDataMessages(); if (!queued.ok) return; // off() when the page unmounts ``` ::: :::: ::::ref-section{title="Read the retained queue"} Read messages that arrived while the page wasn't listening. Newest first. The read does not remove them. The native side keeps the most recent 20. When `delivered` is present, native attempted delivery — not that a page listener confirmed receipt. ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("push.data")` before showing the UI. When it is off, the call resolves `ok: false` with `code: "common/feature_disabled"`. See [Detect features](/getting-started/capabilities). :: ::callout In a plain browser the call resolves `ok: false` with a code like `common/feature_disabled` — branch on `!result.ok`. :: #code :::code-group ```ts [Read the queue] if (!(await bdk.capabilities.has("push.data"))) { // Hide the inbox UI — this build does not include it. } const result = await bdk.push.getDataMessages(); if (!result.ok) { return; } console.log(result.count); for (const message of result.messages) { console.log(message.id, message.appState, message.data, message.delivered); } ``` ::: :::: ::::ref-section{title="Send a data message"} Send a data or silent payload from your push provider. There is no SDK call to send one from the page. :::: --- # Biometric login URL: https://docs.thebdk.com/browser/biometrics ::::ref-section{title="Prompt for biometric auth"} Gate a sensitive action behind Face ID, Touch ID, or fingerprint. Use it before revealing a recovery phrase, confirming a payment, or unlocking a screen. Pass an optional `reason` string to show in the prompt. ::callout Check `getDeviceInfo()?.biometricsAvailable` before prompting, and fall back to PIN/password when it's `false`. Wrap the call in try/catch. :: ::callout{type="warn"} Awaiting the call only confirms the prompt was requested. The actual success or failure arrives on the `biometricResult` event. :: #code :::code-group ```ts [Trigger the prompt] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); try { await bdk.auth.authenticateBiometrics(); } catch { } ``` ```ts [With a reason] try { await bdk.auth.authenticateBiometrics({ reason: "Confirm it's you to view your recovery phrase" }); } catch { } ``` ```ts [Guard on availability] const info = bdk.getDeviceInfo(); if (info?.biometricsAvailable) { try { await bdk.auth.authenticateBiometrics(); } catch { } } else { // Fall back to PIN / password. } ``` ::: :::: ::::ref-section{title="Read the result"} Subscribe to `biometricResult` to learn whether auth succeeded. The `BiometricResult` payload has `status` (`"success"` or `"failed"`), `data` (the raw native auth result), and `platform` (`"ios" | "android"`). `bdk.on(...)` returns an unsubscribe function — call it when you're done. ::callout{type="warn"} A failed or cancelled prompt comes through `biometricResult`, not the `error` event, and does not reject the promise. Branch on `status` inside your listener. :: #code :::code-group ```ts [Handle the result] const off = bdk.on("biometricResult", ({ data, status, platform }) => { if (status === "success") { console.log(`Authenticated on ${platform}`, data); // Unlock the protected action here. } else { console.warn("Biometric auth did not succeed:", status, data); } }); // Later, when you're done listening: off(); ``` ::: :::: ::::ref-section{title="Subscribe before you prompt"} Register the `biometricResult` listener before calling `authenticateBiometrics()`, or a fast result can be missed. #code :::code-group ```ts [subscribe-before-dispatch] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); function unlockWithBiometrics() { return new Promise((resolve) => { // 1. Subscribe FIRST so no result is missed. const off = bdk.on("biometricResult", ({ status }) => { off(); resolve(status === "success"); }); // 2. Then dispatch the prompt. void bdk.auth.authenticateBiometrics({ reason: "Unlock your account" }); }); } const ok = await unlockWithBiometrics(); ``` ::: :::: --- # Saved login URL: https://docs.thebdk.com/browser/smart-login ::::ref-section{title="Saved login"} Let returning users sign back in without re-typing their credentials. Methods live on `bdk.auth`, with one event for the response. - `auth.updateCredentials({ email, password })` — save credentials after login. - `auth.clearCredentials()` — wipe saved credentials, e.g. on logout. - `auth.loginViaCredentials()` — request saved credentials; they arrive on the `smartLoginCredentials` event. ::callout{type="warn"} `loginViaCredentials()` doesn't return the credentials — read them from the `smartLoginCredentials` event. :: ::callout Outside the native app the save/clear commands don't run. Branch on `!result.triggered`. To check support up front, read `bdk.getDeviceInfo()?.smartLoginAvailable`. :: #code :::code-group ```ts [Setup] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Save credentials after login"} Call this right after a user authenticates so they sign in instantly next time. ::callout{type="warn"} Only save after you've verified the login is real. :: #code :::code-group ```ts [Save after login] const result = await bdk.auth.updateCredentials({ email: "ada@example.com", password: "hunter2" }); if (!result.triggered) { // Not running in the native app — Smart Login storage unavailable. console.warn("Smart Login not available:", result.reason); } ``` ```ts [Guard on device support] const info = bdk.getDeviceInfo(); if (info?.smartLoginAvailable) { await bdk.auth.updateCredentials({ email, password }); } ``` ::: :::: ::::ref-section{title="Clear saved credentials"} Call this on logout to forget the saved login. Takes no arguments. #code :::code-group ```ts [On logout] async function logout() { await bdk.auth.clearCredentials(); // ...continue your app's sign-out flow } ``` ::: :::: ::::ref-section{title="Log in with saved credentials"} On a returning visit, request the saved credentials. They arrive on the `smartLoginCredentials` event as `{ email: string | null; password: string | null }`. If either field is `null`, nothing usable is saved — fall back to your login screen. ::callout{type="warn"} Register your `smartLoginCredentials` listener with `bdk.on(...)` **before** you call `loginViaCredentials()`, or you'll miss the response. :: #code :::code-group ```ts [Request + listen] const off = bdk.on("smartLoginCredentials", ({ email, password }) => { if (email && password) { // Auto-fill your form or call your sign-in endpoint. signIn(email, password); } else { // Nothing saved (or values failed validation) — show the login screen. showLoginForm(); } }); await bdk.auth.loginViaCredentials(); // Later, when the screen unmounts: off(); ``` ```ts [Full saved-credentials flow] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // 1. On a returning launch, ask for any saved credentials. bdk.on("smartLoginCredentials", async ({ email, password }) => { if (!email || !password) return showLoginForm(); await signIn(email, password); }); await bdk.auth.loginViaCredentials(); // 2. After a fresh manual login, save them for next time. async function onManualLoginSuccess(email: string, password: string) { await bdk.auth.updateCredentials({ email, password }); } // 3. On logout, forget them. async function onLogout() { await bdk.auth.clearCredentials(); } ``` ::: :::: --- # External sign-in URL: https://docs.thebdk.com/browser/sign-in ::::ref-section{title="Start a sign-in"} Pass `bdk.auth.signIn` an HTTPS authorization URL you already built — client id, scope, state, PKCE challenge, and normally `redirect_uri`. The SDK never builds that URL. With `handoff: "page"`, the resolved result is the validated `callbackUrl` plus decoded query `params`. This feature can be switched off in a given app build — check `await bdk.capabilities.has("auth.oauth")` before showing the UI. See [Detect features](/getting-started/capabilities). When it's off, the call resolves `ok: false` with `code: "common/feature_disabled"`. | Property | Type | Description | | --- | --- | --- | | `url` | `string` · required | Full HTTPS authorization URL. You build it. | | `redirectUri` | `string` | Callback URI when it isn't already on `url`. | | `handoff` | `string` | `page` returns the callback here. `webview` opens it in the webview. | | `preferEphemeral` | `boolean` | Request an ephemeral iOS session when supported. | | `timeoutMs` | `number` | Return-trip timeout in milliseconds. Native reads 60000–3600000. | #code :::code-group ```ts [Page handoff] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const authorizeUrl = new URL("https://idp.example.com/authorize"); authorizeUrl.searchParams.set("client_id", clientId); authorizeUrl.searchParams.set("redirect_uri", "com.example.app://oauth/return"); authorizeUrl.searchParams.set("response_type", "code"); authorizeUrl.searchParams.set("scope", "openid email"); authorizeUrl.searchParams.set("state", state); authorizeUrl.searchParams.set("code_challenge", challenge); authorizeUrl.searchParams.set("code_challenge_method", "S256"); if (!(await bdk.capabilities.has("auth.oauth"))) { window.location.href = authorizeUrl.toString(); } else { const result = await bdk.auth.signIn({ url: authorizeUrl.toString(), handoff: "page" }); if (!result.ok) { console.warn(result.code, result.message); // e.g. common/feature_disabled return; } console.log(result.callbackUrl, result.params.code); } ``` ::: :::: ::::ref-section{title="Hand off to the webview"} Use `handoff: "webview"` to load the callback in the app webview. The resolved `loaded: true` only means that page opened — subscribe to `auth.completed` and `auth.cancelled` before you call, and treat those events as the outcome. #code :::code-group ```ts [Webview handoff] bdk.on("auth.completed", ({ mode, provider, handoff }) => { console.log(mode, provider, handoff); }); bdk.on("auth.cancelled", ({ reason }) => { console.log(reason); // dismissed | provider_denied | timeout }); const result = await bdk.auth.signIn({ url: authorizeUrl, handoff: "webview" }); if (!result.ok) { console.warn(result.code, result.message); return; } // result.loaded === true means the page opened, not that sign-in finished. ``` ::: :::: ::::ref-section{title="Handle the callback"} Set `redirectUri` (or `redirect_uri` on the URL you build) to a callback within your app's registered scheme. Fragment-only OAuth responses are not supported. | Code | Meaning | | --- | --- | | `auth/redirect_unroutable` | The callback URI cannot be routed back into this app. | | `auth/in_progress` | A sign-in is already running. | | `auth/callback_mismatch` | The callback did not match this sign-in session. | | `auth/provider_denied` | The provider or user denied the request. | | `auth/session_failed` | The sign-in session failed to complete. | ::callout{type="warn"} A custom-scheme callback is not authoritative for identity — finish the session server-side. :: #code :::code-group ```ts [Pass redirectUri] const result = await bdk.auth.signIn({ url: "https://idp.example.com/authorize?client_id=…&state=…", redirectUri: "com.example.app://oauth/return", handoff: "page" }); if (!result.ok) { console.warn(result.code, result.message); } ``` ::: :::: ::::ref-section{title="Listen for the result"} `auth.completed` reports `{ mode, provider, handoff }` when the session finishes. `auth.cancelled` reports `{ mode, provider, reason }` — `mode` is `auto` or `explicit`; `reason` is `dismissed`, `provider_denied`, or `timeout`. Subscribe before you call. #code :::code-group ```ts [Completed and cancelled] const offs = [ bdk.on("auth.completed", ({ mode, provider, handoff }) => { console.log(mode, provider, handoff); }), bdk.on("auth.cancelled", ({ reason }) => { console.log(reason); }) ]; // later offs.forEach((off) => off()); ``` ::: :::: --- # In-app purchases URL: https://docs.thebdk.com/browser/in-app-purchases ::::ref-section{title="Read results from events, not the call"} Awaiting `purchaseIos` or `purchaseAndroid` only confirms the command was sent — the result arrives later on `purchaseSuccess` or `purchaseFailed`. Subscribe before you call. For the rest of the `iap` namespace, use the envelope-based helpers on [Sell products & subscriptions](/browser/purchases). Every purchase and consume takes `{ id, type }`: `id` is the App Store / Play product identifier, and `type` is `"product"` (one-time / consumable) or `"subscription"`. Use the `Ios` / `Android` method for the device you're on. ::callout{type="warn"} The payload key is `id`, not `product_id`. :: ::callout Results from the device are unverified. Treat `purchaseSuccess` as "the store reported a transaction", then verify the receipt server-side before granting entitlements. See [Verify in-app purchases](/server/iap-verification). :: :::: ::::ref-section{title="Buy a product on iOS"} Opens the App Store purchase sheet. The result lands on `purchaseSuccess`, or `purchaseFailed` on cancel or error — both carry `{ platform, data }`. #code :::code-group ```ts [iOS purchase] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); bdk.on("purchaseSuccess", ({ platform, data }) => { // data is the raw, UNVERIFIED store payload — send it to your server to verify. console.log("purchased on", platform, data); }); bdk.on("purchaseFailed", ({ data }) => { console.warn("purchase failed or cancelled", data); }); await bdk.iap.purchaseIos({ id: "pro_monthly", type: "subscription" }); ``` ::: :::: ::::ref-section{title="Buy a product on Android"} Opens the Google Play purchase sheet. Results surface on the same `purchaseSuccess` / `purchaseFailed` events; the `platform` field tells you which store they came from. #code :::code-group ```ts [Android purchase] bdk.on("purchaseSuccess", ({ platform, data }) => { if (platform === "android") void verifyOnServer(data); }); await bdk.iap.purchaseAndroid({ id: "coins_100", type: "product" }); ``` ::: :::: ::::ref-section{title="Buy from one shared codebase"} Pick the right method by platform when you ship to both stores. The call resolves with no effect off-device, so add a web fallback. #code :::code-group ```ts [Platform switch] async function buy(id: string, type: "product" | "subscription") { const os = bdk.getDeviceInfo()?.deviceOS?.toLowerCase(); if (os === "ios") return bdk.iap.purchaseIos({ id, type }); if (os === "android") return bdk.iap.purchaseAndroid({ id, type }); // Not running natively — show a web fallback / upsell. } await buy("pro_monthly", "subscription"); ``` ::: :::: ::::ref-section{title="Consume a product so it can be bought again"} Use for consumables (coins, lives, refills) — a consumable can't be repurchased until it's consumed. ::callout Consuming on the device only updates local state. The authoritative consume for Google Play happens server-side — see [Verify in-app purchases](/server/iap-verification). :: #code :::code-group ```ts [Android consume] await bdk.iap.consumeAndroidPurchase({ id: "coins_100", type: "product" }); ``` ::: :::: ::::ref-section{title="IAP events reference"} Which event answers which call. Each carries the `platform` (`"ios"` or `"android"`) and a `data` object with the store's purchase details — product id, transaction id, receipt data, and result/error codes. Treat `data` as the raw store payload and verify it server-side. `bdk.on(...)` returns an unsubscribe function. | Event | Fired by | | --- | --- | | `purchaseSuccess` | `purchaseIos` / `purchaseAndroid` succeeded | | `purchaseFailed` | purchase cancelled or errored | | `receiptReceived` | a store receipt / token was returned | #code :::code-group ```ts [Subscribe to all IAP events] const offs = [ bdk.on("purchaseSuccess", (e) => console.log("success", e.platform, e.data)), bdk.on("purchaseFailed", (e) => console.log("failed", e.data)), bdk.on("receiptReceived", (e) => console.log("receipt", e.data)) ]; // later offs.forEach((off) => off()); ``` ::: :::: ::::ref-section{title="Verify on the server"} The step that actually grants entitlements. Send the `data` from `purchaseSuccess` / `receiptReceived` to your backend and validate it. See [Verify in-app purchases](/server/iap-verification) for `verifyIosReceipt`, `verifyAndroidReceipt`, `readAndroidReceipt`, and server-side `consumeAndroidPurchase`. #code :::code-group ```ts [Server route — Node only, never bundled in the browser] import { verifyAndroidReceipt } from "@bdk/native/server/iap"; // In your API handler, with the purchaseToken forwarded from the client: const validation = await verifyAndroidReceipt({ packageName: "com.example.app", productId: "pro_monthly", purchaseToken, productType: "subscription" }); if (validation.isValid) { // Inspect validation.payload / validation.raw before granting entitlements. } ``` ::: :::: --- # Sell products & subscriptions URL: https://docs.thebdk.com/browser/purchases ::::ref-section{title="Load your product catalog"} Fetch localized products for a paywall. Unknown store ids come back in `invalidIds`. Each product is a [`BdkIapProduct`](/reference/objects) — `kind` is `consumable`, `nonConsumable`, `subscription`, or `nonRenewingSubscription`. In a plain browser the command doesn't run; the returned object has no `ok` field and `triggered: false`. ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("iap")` before showing the paywall. When it is off, the call resolves `ok: false` with `code: "common/feature_disabled"`. See [Detect features](/getting-started/capabilities). :: ::callout The previous-generation purchase flow (`purchaseIos` / `purchaseAndroid`) is documented at [In-app purchases](/browser/in-app-purchases) and remains supported. :: #code :::code-group ```ts [Catalog] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); if (!(await bdk.capabilities.has("iap"))) { // Hide the paywall — this build does not include the store. } const result = await bdk.iap.products(["coins_100", "pro_monthly"]); if (!result.ok) { // Disabled, or not running in the app (no `ok`, `triggered: false`). return; } for (const product of result.products) { console.log(product.id, product.kind, product.price.formatted); } console.log("unknown ids", result.invalidIds); ``` ::: :::: ::::ref-section{title="Start a purchase"} Open the store sheet. The call resolves `{ state: "launched" }` — that is not a sale. The outcome arrives on `iap.purchaseCompleted`, so subscribe before you call. Pass `planId` and `offerId` from the product's subscription plans when you buy a subscription. | Property | Type | Description | | --- | --- | --- | | `id` | `string` · required | The App Store / Play product identifier. | | `planId` | `string` | Subscription plan to buy. | | `offerId` | `string` | Offer on that plan. | | `replaces` | `string` | Android product id of the active subscription this purchase replaces. | | `requestId` | `string` | Optional correlation id, at most 64 characters. | ::callout{type="warn"} Subscribe to `iap.purchaseCompleted` before you call. A deferred grant re-emits later — keep a persistent listener, not a one-shot. :: #code :::code-group ```ts [Launch] bdk.on("iap.purchaseCompleted", (event) => { if (event.state === "purchased") unlock(event.transaction?.productId); if (event.state === "pending") showWaitingForApproval(); }); const result = await bdk.iap.purchase({ id: "pro_monthly", planId: "monthly", offerId: "trial" }); if (!result.ok) return; // result.state === "launched" — the store sheet opened. Wait for the event. ``` ::: :::: ::::ref-section{title="Purchase and wait"} Wait for the first outcome (`purchased`, `pending`, `cancelled`, or `failed`). `pending` is deferred approval — the grant arrives later on `iap.purchaseCompleted`, so keep a persistent listener. `purchaseAndWait` takes the same options as `purchase`, plus `timeoutMs` to bound the wait for that first outcome (the store sheet, not a later deferred grant). ::callout{type="warn"} A `pending` result is not a grant. Do not unlock. The persistent listener receives the later `purchased` event. :: #code :::code-group ```ts [Wait] bdk.on("iap.purchaseCompleted", (event) => { // Deferred grants and later settlements land here. if (event.state === "purchased") unlock(event.transaction?.productId); }); const result = await bdk.iap.purchaseAndWait({ id: "pro_monthly" }); if (!("state" in result)) { // Disabled, timed out, or not running in the app. return; } if (result.state === "purchased") unlock(result.transaction?.productId); if (result.state === "pending") showWaitingForApproval(); if (result.state === "cancelled") return; if (result.state === "failed") console.warn(result.code); ``` ::: :::: ::::ref-section{title="Check entitlements"} Read what the store currently says the user owns. Each item is a [`BdkIapEntitlement`](/reference/objects) — `state` is `active`, `gracePeriod`, `billingRetry`, `revoked`, or `expired`. #code :::code-group ```ts [Entitlements] const result = await bdk.iap.entitlements(); if (!result.ok) return; for (const entitlement of result.entitlements) { console.log(entitlement.productId, entitlement.kind, entitlement.state); } ``` ::: :::: ::::ref-section{title="Restore purchases"} Ask the store to restore previous purchases and subscriptions. Use it on a restore button. The call returns the restored entitlements. #code :::code-group ```ts [Restore] const result = await bdk.iap.restore(); if (!result.ok) return; for (const entitlement of result.restored) { console.log(entitlement.productId, entitlement.state); } ``` ::: :::: ::::ref-section{title="Get the raw receipt"} Read the material your server needs to verify a purchase. On iOS you get `receipt` plus `jws`; on Android you get `purchases` (`productId`, `purchaseToken`, `packageName`, `productType`). Treat a client `purchased` event as the store reporting a transaction. Verify on the server before granting access — see [Verify purchases](/server/iap-verification). #code :::code-group ```ts [Receipt] const result = await bdk.iap.receipt(); if (!result.ok) return; if (result.platform === "ios") { await sendToServer({ receipt: result.receipt, jws: result.jws }); } else { await sendToServer({ purchases: result.purchases }); } ``` ::: :::: ::::ref-section{title="Consume a consumable"} Mark a consumable as used so the user can buy it again. Pass the product id. On iOS, `remaining` is the locally tracked count after the consume. #code :::code-group ```ts [Consume] const result = await bdk.iap.consume("coins_100"); if (!result.ok) return; console.log(result.consumed, result.remaining); ``` ::: :::: ::::ref-section{title="React to renewals and revocations"} Listen for subscription renewals, revocations, and transactions that started outside your UI. Subscribe once at startup, then refresh entitlements. #code :::code-group ```ts [Updates] bdk.on("iap.transactionUpdated", (event) => { // event.reason is "renewal", "revoked", or "external" void refreshEntitlements(); }); ``` ::: :::: --- # Native UI URL: https://docs.thebdk.com/browser/ui-and-feedback ::::ref-section{title="Set up"} Use `bdk.ui` to drive native surfaces from your web code. Calls that return a value (menu taps, popup buttons, picks) deliver it on an event — subscribe with `bdk.on(...)` before you call. ::callout{type="warn"} Outside the app these calls don't run (`triggered: false`) and no native surface appears. Provide a web fallback for anything the user must respond to. :: #code :::code-group ```ts [Setup] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Show a banner"} Show a non-blocking in-app banner for status messages like "You're offline." #code :::code-group ```ts [Banner] await bdk.ui.showBanner({ title: "You're offline", description: "Changes will sync when you reconnect." }); ``` ::: :::: ::::ref-section{title="Show an alert"} Show a blocking system alert the user must acknowledge before continuing. #code :::code-group ```ts [Alert] await bdk.ui.showAlert({ title: "Upload failed", description: "Please try again." }); ``` ::: :::: ::::ref-section{title="Ask the user to confirm"} Show a popup with action buttons. Pass `ok_label` and `cancel_label` to set the button labels. The pressed button arrives on the `popupClosed` event — subscribe first. ::callout The `popupClosed` payload tells you which button dismissed the popup — the OK or Cancel button. :: #code :::code-group ```ts [Popup + popupClosed] const off = bdk.on("popupClosed", (button) => { console.log("popup dismissed via", button); }); await bdk.ui.showPopup({ title: "Delete this item?", description: "This cannot be undone.", ok_label: "Delete", cancel_label: "Keep" }); // later, when you no longer need it off(); ``` ::: :::: ::::ref-section{title="Show a menu of choices"} Show a list or action sheet. The tapped item arrives on the `menuClicked` event — subscribe first. #code :::code-group ```ts [Menu + menuClicked] bdk.on("menuClicked", (item) => { console.log("menu item tapped:", item); }); await bdk.ui.showMenu({ menudata: { sections: [ { items: [ { title: "Share", returned_data: "share" }, { title: "Edit", returned_data: "edit" }, { title: "Delete", returned_data: "delete" } ] } ] } }); ``` ::: :::: ::::ref-section{title="Prompt for an app-store rating"} Show the OS rating prompt at a natural moment. #code :::code-group ```ts [Request rating] await bdk.ui.requestRating(); ``` ::: :::: ::::ref-section{title="Pick a date or time"} Open a native date/time picker. Subscribe to `datePicked` before you call — it still fires on every build. On current app builds the awaited call also resolves the result directly (`{ ok: true, value, epochMs }`) and `datePicked` delivers the epoch milliseconds. On older builds the result arrives only on `datePicked`. | Property | Type | Description | | --- | --- | --- | | `title` | `string` | Picker title. | | `description` | `string` | Supporting text under the title. | | `mode` | `string` | `date`, `time`, or `datetime`. Prefer this over the older `type` alias. | | `min` | `string` | Earliest value, in the selected mode's ISO shape. | | `max` | `string` | Latest value, in the selected mode's ISO shape. | | `initial` | `string` | Initially selected value, in the selected mode's ISO shape. | | `locale` | `string` | BCP-47 locale hint. iOS only; Android follows the device locale. | #code :::code-group ```ts [Date picker] bdk.on("datePicked", (value) => { console.log("date chosen:", value); // epoch ms on current builds }); const result = await bdk.ui.pickDateTime({ title: "Select a date", mode: "date" }); if (result.ok) { console.log(result.value, result.epochMs); } ``` ```ts [With options] bdk.on("datePicked", (value) => { console.log("date chosen:", value); }); await bdk.ui.pickDateTime({ title: "Select a date", description: "Choose when to start", mode: "datetime", min: "2026-08-01", max: "2026-08-31", initial: "2026-08-07", locale: "en-US" }); ``` ::: :::: ::::ref-section{title="Style the status bar"} Set the status-bar color. Android paints the bar with the color you pass; iOS keeps the system background and picks the readable content style for it. #code :::code-group ```ts [Status bar] await bdk.ui.updateStatusBar({ color: "#111111" }); ``` ::: :::: ::::ref-section{title="Control screen orientation"} Set or lock the screen orientation. `lockOrientation()` locks the current orientation; pass `{ locked: false }` to unlock. ::callout{type="warn"} Android only — wrap the call in try/catch. :: #code :::code-group ```ts [Orientation] await bdk.ui.setOrientation({ orientation: "landscape" }); // Android only — locks the current orientation await bdk.ui.lockOrientation(); // Unlock await bdk.ui.lockOrientation({ locked: false }); ``` ::: :::: ::::ref-section{title="Disable the iOS back-swipe"} Suppress the iOS left-edge back-swipe when a screen owns that gesture itself. `disableLeftSwipe()` disables it; pass `{ enabled: true }` to re-enable. ::callout{type="warn"} iOS only — wrap the call in try/catch. :: #code :::code-group ```ts [Disable left swipe] await bdk.ui.disableLeftSwipe(); // Re-enable await bdk.ui.disableLeftSwipe({ enabled: true }); ``` ::: :::: ::::ref-section{title="Event reference"} Register handlers with `bdk.on(event, listener)`, which returns an unsubscribe function. - **`menuClicked`** — the menu item the user tapped (its title and data). - **`popupClosed`** — which button dismissed the popup. - **`datePicked`** — the date/time the user selected. ::callout A throwing listener surfaces once as a `BdkError` with code `BDK_LISTENER_ERROR`, via both the `onError` config callback and the `error` event. :: #code :::code-group ```ts [Subscribe to all UI events] const offs = [ bdk.on("menuClicked", (item) => console.log("menu", item)), bdk.on("popupClosed", (button) => console.log("popup", button)), bdk.on("datePicked", (value) => console.log("date", value)) ]; // Surface listener errors centrally const bdkWithErrors = createBdkNative({ onError: (err) => console.error(err.code, err.message) }); // Clean up when the screen unmounts offs.forEach((off) => off()); ``` ::: :::: --- # Native sharing URL: https://docs.thebdk.com/browser/sharing ::::ref-section{title="Open the share sheet"} Hand content to the device's native share sheet so the user can send it to another app like Messages, Mail, or Instagram. There is no result event — the call resolves once the sheet is requested, not when the user completes or cancels the share. ::callout When not running in the app, the call doesn't run — `triggered` is `false` (`pending` in a browser tab, `skipped` with no `window`). :: #code :::code-group ```ts [Every share call] const bdk = createBdkNative(); // Each method dispatches and forgets — await confirms the sheet was requested. await bdk.share.text({ text_content: "Check out this app!" }); ``` ::: :::: ::::ref-section{title="Share text"} Share a string of text. The only field is `text_content` — to share a link, include the URL in the text. Use it for "Share this link" and "Tell a friend" flows. #code :::code-group ```ts [Share text] const bdk = createBdkNative(); await bdk.share.text({ text_content: "Check out this app! https://example.com" }); ``` ::: :::: ::::ref-section{title="Share an image"} Share a hosted image by URL. Pass a bare string, not an object. ::callout A protocol-relative URL beginning with `//` is upgraded to `https://`. :: #code :::code-group ```ts [Share image] const bdk = createBdkNative(); await bdk.share.image("https://example.com/photos/sunset.jpg"); ``` ```ts [Protocol-relative URL] // "//cdn.example.com/img.png" becomes "https://cdn.example.com/img.png" await bdk.share.image("//cdn.example.com/img.png"); ``` ::: :::: ::::ref-section{title="Share a video"} Share a video by URL. Pass a bare string. #code :::code-group ```ts [Share video] const bdk = createBdkNative(); await bdk.share.video("https://example.com/clips/demo.mp4"); ``` ::: :::: ::::ref-section{title="Share a file"} Share any file by URL — a PDF, a document, a download. Pass a bare string. #code :::code-group ```ts [Share file] const bdk = createBdkNative(); await bdk.share.file("https://example.com/docs/invoice.pdf"); ``` ::: :::: ::::ref-section{title="Share to an Instagram Story"} Open content directly in the Instagram Stories composer for a one-tap "Share to your Story" button. #code :::code-group ```ts [Instagram Story] const bdk = createBdkNative(); await bdk.share.instagramStory({ app_id: "your-instagram-app-id", background_image_url: "https://example.com/story-bg.jpg", sticker_url: "https://example.com/sticker.png" }); ``` ::: :::: ::::ref-section{title="Check the result"} Each method resolves a `NativeCommandResult`. Inspect `triggered` to confirm the share sheet opened — there is no event reporting whether the user finished the share. #code :::code-group ```ts [Inspect the dispatch receipt] const bdk = createBdkNative(); const result = await bdk.share.image("https://example.com/photo.jpg"); console.log(result.command); // "shareImage" console.log(result.triggered); // true if it reached the app if (!result.triggered) { console.log("Share unavailable:", result.reason); } ``` ::: :::: ::::ref-section{title="Share files and links"} Open the share sheet with a mixed list of text, links, and files — up to 10 items. On iOS `outcome` is `completed`; on Android it is `presented`. To receive content other apps share into yours, see [Receive shared content](/browser/share-inbound). | Property | Type | Description | | --- | --- | --- | | `items` | `BdkShareItem[]` | Mixed `{ type: "text", text }`, `{ type: "url", url }`, and `{ type: "file", url, filename? }` items (`image` / `video` / `audio` are the same file family). Combined with `urls`, at most 10. See [`BdkShareItem`](/reference/objects). | | `urls` | `string[]` | File URLs appended after `items`. Combined with `items`, at most 10. Pass at least one of `items` or `urls`. | | `title` | `string` | Android chooser title. Ignored on iOS. | ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("share.files")` before showing the UI — see [Detect features](/getting-started/capabilities). When it's off, the call resolves `ok: false` with `code: "common/feature_disabled"`. :: ::callout Remote file URLs are fetched with the webview's cookie session — files behind a login the webview holds work; files needing an Authorization header don't. :: #code :::code-group ```ts [Mixed items] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const result = await bdk.share.send({ items: [ { type: "text", text: "Quarterly report attached" }, { type: "url", url: "https://app.example.com/reports/123" }, { type: "file", url: "https://app.example.com/api/report.pdf", filename: "Q3-report.pdf" } ], title: "Share report" }); if (!result.ok) { // "share/fetch_failed" | "share/too_large" | "common/feature_disabled" // In a plain browser the result has ok: false with a feature-disabled code. console.log(result.code ?? "Share unavailable"); return; } console.log(result.outcome); // "completed" on iOS, "presented" on Android console.log(result.items.requested, result.items.shared); ``` ::: :::: ::::ref-section{title="Share a list of files"} Share one or more file URLs. `share.files` is sugar for `share.send` with only `urls`. #code :::code-group ```ts [Files only] const result = await bdk.share.files( ["https://app.example.com/api/report.pdf"], { title: "Share report" } ); if (!result.ok) { console.log(result.code, result.message); return; } console.log(result.outcome, result.items.shared); ``` ::: :::: --- # Receive shared content URL: https://docs.thebdk.com/browser/share-inbound ::::ref-section{title="Listen for incoming shares"} Subscribe at startup so you don't miss a share that arrived before the page loaded. `onReceived` registers a callback and returns an unsubscribe function. It listens for new shares and replays any that are already pending; a share can settle later with the same `shareId` and a new `status`, and the listener fires again. ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("share.inbound")` before showing the UI — see [Detect features](/getting-started/capabilities). :: #code :::code-group ```ts [Subscribe] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const off = bdk.share.onReceived((share) => { console.log(share.shareId, share.status); // Same shareId fires again when status changes (pending → complete / partial / failed). }); off(); // when the page unmounts ``` ::: :::: ::::ref-section{title="Read pending shares"} Pull pending shares yourself when you are not using `onReceived`. A successful read acknowledges the shares it returns — newest first. ::callout{type="warn"} Use `onReceived` or `getPending`, not both. `onReceived` already replays pending shares once. :: ::callout This feature can be switched off in a given app build. Check `await bdk.capabilities.has("share.inbound")` before showing the UI — see [Detect features](/getting-started/capabilities). When it's off — or in a plain browser — the call resolves `ok: false` with `code: "common/feature_disabled"`. Branch on `!result.ok`. :: #code :::code-group ```ts [Pull once] const result = await bdk.share.getPending(); if (!result.ok) { console.log(result.code ?? "Share unavailable"); return; } for (const share of result.shares) { console.log(share.shareId, share.status); } ``` ::: :::: ::::ref-section{title="Track upload progress"} Watch each file item as it uploads. The `share.uploadProgress` event reports `bytesSent`, `totalBytes`, and `progress` for one item at a time — subscribe at startup so you don't miss a share that is already uploading. | Property | Type | Description | | --- | --- | --- | | `shareId` | `string` | The share this item belongs to. | | `itemIndex` | `number` | Index of the item in `share.items`. | | `bytesSent` | `number` | Bytes uploaded so far. | | `totalBytes` | `number` | Total bytes for this item. | | `progress` | `number` | Upload progress for this item. | #code :::code-group ```ts [Progress] bdk.on("share.uploadProgress", ({ shareId, itemIndex, bytesSent, totalBytes, progress }) => { console.log(shareId, itemIndex, bytesSent, totalBytes, progress); }); ``` ::: :::: ::::ref-section{title="Retry a failed upload"} Retry every failed upload on a share. Call it when an item's `upload.status` is `failed`. ::callout When the feature is off, the call resolves `ok: false` with `code: "common/feature_disabled"`. :: #code :::code-group ```ts [Retry] bdk.share.onReceived((share) => { for (const item of share.items) { if ("upload" in item && item.upload.status === "failed") { void retry(share.shareId); } } }); async function retry(shareId: string) { const result = await bdk.share.retryUpload({ shareId }); if (!result.ok) { console.log(result.code, result.message); return; } console.log("retrying", result.retrying); } ``` ::: :::: ::::ref-section{title="What a share looks like"} A [`BdkInboundShare`](/reference/objects) has a `source` (`ios-extension` or `android-intent`), a `status` (`pending`, `complete`, `partial`, or `failed`), and `items` — text, a url, or a file (`image` / `video` / `file`) with an `upload` state (`pending`, `uploading`, `uploaded`, `failed`, `rejected`, `skipped`). The SDK exposes upload outcomes, never raw file bytes or device paths. #code :::code-group ```ts [Inspect a share] bdk.share.onReceived((share) => { console.log(share.source, share.status); for (const item of share.items) { if (item.kind === "text") console.log(item.text); if (item.kind === "url") console.log(item.url); if ("upload" in item) console.log(item.name, item.upload.status); } }); ``` ::: :::: --- # Read health data URL: https://docs.thebdk.com/browser/health ::::ref-section{title="Check availability"} Call `status()` to see whether Health is available on this device and the per-type authorization state before you read anything. Health can be switched off in a given app build — check [`bdk.capabilities.has("health.read")`](/getting-started/capabilities) before showing any health UI. When it isn't available, `status()` returns a `reason`: | Reason | Meaning | | --- | --- | | `device_unsupported` | This device has no health data provider. | | `provider_missing` | The OS health app / provider isn't installed. | | `provider_update_required` | The provider is installed but needs an update. | ::callout Outside the app the command doesn't run: the returned object has no `ok` field and `triggered: false`. `status()`'s result is a union — check `"ok" in result` before reading envelope-only fields. :: #code :::code-group ```ts [Check status] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); if (await bdk.capabilities.has("health.read")) { const result = await bdk.health.status(); if ("ok" in result && result.ok) { console.log(result.available, result.reason, result.types); // types: { steps: { status: "granted", requested: true }, ... } } } ``` ::: :::: ::::ref-section{title="Request access"} Ask for authorization to read health data. Pass `types` to request a subset, or omit it to request every type your app build enables. ::callout{type="warn"} On iOS, read authorization always reports `unknown` — HealthKit never reveals whether a read was granted or denied. A denied read looks exactly like an empty result, so treat an empty read as possibly-denied, not as "no data recorded". :: #code :::code-group ```ts [Request all enabled types] const result = await bdk.health.request({}); if ("ok" in result && result.ok) { console.log(result.types); } else if ("ok" in result) { console.warn(result.code, result.message); } else if (!result.triggered) { return; } ``` ```ts [Request a subset] await bdk.health.request({ types: ["steps", "heartRate"] }); ``` ::: :::: ::::ref-section{title="Read totals"} Use `aggregate()` for steps, distance, and active calories instead of summing raw samples yourself — it buckets and sums on the native side so you don't double-count across sources. `interval` controls the bucket size: `total`, `hour`, `day`, or `month`. The result carries `unit` (the type's unit), `stat` (`sum` for quantity totals, `avg` for types like heart rate), and the `buckets` themselves. #code :::code-group ```ts [Daily step totals] const result = await bdk.health.aggregate({ type: "steps", interval: "day" }); if ("ok" in result && result.ok) { console.log(result.unit, result.stat); // "count" "sum" for (const bucket of result.buckets) { console.log(bucket); } } else if ("ok" in result) { console.warn(result.code, result.message); } else if (!result.triggered) { return; } ``` ::: :::: ::::ref-section{title="Read raw samples"} Read individual samples for a type over a date range when a total isn't enough — for example, plotting each heart-rate reading. `limit` caps how many samples come back; check `truncated` to see if there are more than `limit` in range, and use `startDate`/`endDate`/`ascending` to page through them. #code :::code-group ```ts [Raw heart-rate samples] const result = await bdk.health.read({ type: "heartRate", startDate: "2026-08-01T00:00:00Z", endDate: "2026-08-08T00:00:00Z", limit: 200, ascending: true }); if ("ok" in result && result.ok) { console.log(result.kind, result.unit, result.count, result.truncated); console.log(result.samples); } else if ("ok" in result) { console.warn(result.code, result.message); } else if (!result.triggered) { return; } ``` ::: :::: ::::ref-section{title="List enabled types"} `enabledTypes()` returns the `HealthTypeId`s this app build enables — an empty array when Health is off. Use it to build a picker instead of hardcoding the full list. | `HealthTypeId` | Kind | Unit / aggregate | | --- | --- | --- | | `steps` | quantity | count / sum | | `distance` | quantity | m / sum | | `activeCalories` | quantity | kcal / sum | | `heartRate` | quantity | bpm / discrete | | `restingHeartRate` | quantity | bpm / discrete | | `weight` | quantity | kg / discrete | | `height` | quantity | cm / discrete | | `bloodOxygen` | quantity | % / discrete | | `sleep` | session | stages | | `workouts` | session | activities | ::callout `aggregate()` doesn't support `sleep` or `workouts` — they're session types, not summable quantities. Use `read()` to pull their raw samples instead. :: #code :::code-group ```ts [List enabled types] const types = await bdk.health.enabledTypes(); console.log(types); // e.g. ["steps", "distance", "heartRate"] ``` ::: :::: --- # NFC tags URL: https://docs.thebdk.com/browser/nfc ::::ref-section{title="Check availability"} Call `isAvailable()` before showing any NFC UI — it's `true` only when NFC is both enabled in this app build and usable on this device (hardware present, radio on). You can also check [`bdk.capabilities.has("nfc")`](/getting-started/capabilities) earlier, before device capabilities have loaded. #code :::code-group ```ts [Check availability] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); if (await bdk.nfc.isAvailable()) { // show the "Scan tag" button } ``` ::: :::: ::::ref-section{title="Read a tag"} Start a one-shot NDEF scan and wait for a physical tag. Reading requires an actual NFC tag near the device — there's no simulated result. If you call `read()` again while a scan is already in progress, it resolves with `nfc/busy`. #code :::code-group ```ts [Scan for a tag] const result = await bdk.nfc.read({ timeoutMs: 15000 }); if (!result.ok) { console.warn(result.code, result.message); // e.g. nfc/tag_lost, nfc/busy } else { console.log(result.tag.id, result.tag.ndef.records); } ``` ::: :::: ::::ref-section{title="Write a tag"} Write NDEF records to a tag during a scan session. `write()` replaces the tag's existing NDEF message — it doesn't append. Each record carries a `type`: `{ type: "text", text, language }`, `{ type: "uri", uri }`, or `{ type: "external", externalType, payloadBase64 }`, up to the number of records this app build allows. #code :::code-group ```ts [Write a URI record] const result = await bdk.nfc.write({ records: [{ type: "uri", uri: "https://example.com" }] }); if (result.ok) { console.log(result.tag.id, result.bytesWritten); } ``` ::: :::: ::::ref-section{title="Cancel a scan"} Stop an in-progress read or write session. If there's no active session, `cancel()` still resolves `ok: true` with `cancelled: false` — it isn't an error. Narrow on `result.ok` before reading `cancelled`. #code :::code-group ```ts [Cancel] const result = await bdk.nfc.cancel(); if (result.ok && result.cancelled) { // an in-progress session was cancelled } ``` ::: :::: ::::ref-section{title="Launch the app from a tag"} `getLaunchTag()` tells you whether this launch of the app was triggered by scanning a tag. It's consumed once — call it a single time at startup. For a tag scanned while the app is already open, subscribe to the `nfc.launchTag` event instead. ::callout On Android 16, a URL-record tag opens the browser instead of launching the app, because of how the OS routes URL NDEF tags. Use a custom-scheme record or an `external` record if you need launch-by-tag to work reliably. :: #code :::code-group ```ts [Cold launch + live scans] bdk.on("nfc.launchTag", ({ url, tag }) => { console.log("scanned while running", url, tag); }); const launch = await bdk.nfc.getLaunchTag(); if (launch.ok && launch.launched) { console.log("launched from a tag", launch.url, launch.tag); } ``` ::: :::: --- # Listen for events URL: https://docs.thebdk.com/browser/events ::::ref-section{title="Subscribe to an event"} Register a listener for a native event. The payload is typed from the event name (see `BdkNativeEvents`). Dot-named events such as `iap.purchaseCompleted` and `deeplink.received` use the same `bdk.on` call. Subscribe right after `createBdkNative()` so you don't miss a result. `bdk.on` returns an unsubscribe function — call it on unmount. #code :::code-group ```ts [Subscribe + cleanup] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const off = bdk.on("deviceInfo", (info) => console.log(info.deviceOS, info.bdkRelease, info.playerId) ); off(); // stop listening ``` ::: :::: ::::ref-section{title="Get the result of a call"} Subscribe before you call the method. Awaiting the call confirms it was dispatched, not that it succeeded — the result arrives on the event. Some events fire quickly (photos, barcode, location, pickers, popups, contacts, screenshots); others may resolve much later or never (purchases, receipts, biometrics, Smart Login). ::callout `iap.purchaseIos` / `purchaseAndroid` take `{ id, type }`, where `type` is `"product"` or `"subscription"`. Match these key names exactly — they are the native contract. :: #code :::code-group ```ts [Quick callback] bdk.on("barcodeScanned", (code) => console.log("scanned", code)); bdk.on("location", (loc) => console.log("location", loc)); await bdk.media.scanBarcode(); await bdk.location.getCurrentPosition(); ``` ```ts [Webhook style] bdk.on("purchaseSuccess", ({ platform, data }) => console.log("purchased on", platform, data)); bdk.on("purchaseFailed", ({ platform, data }) => console.warn("failed", platform, data)); bdk.on("receiptReceived", ({ platform, data }) => { // send data to your backend to verify }); // The await below only confirms dispatch — NOT that the purchase succeeded. await bdk.iap.purchaseAndroid({ id: "pro_monthly", type: "subscription" }); ``` ::: :::: ::::ref-section{title="All events and payloads"} Every event and the payload its listener receives. The field-level shape of each object is in [Objects](/reference/objects). | Event | Listener receives | |---|---| | `deviceInfo` | `BdkDeviceInfo` | | `photoSelected` / `photoCaptured` | `MediaResult` | | `audioRecorded` | `MediaResult` | | `screenshot` | the captured screenshot image (a data URI you can preview or upload) | | `barcodeScanned` | the scanned code: its `type` and decoded `content` | | `contacts` | the device address book: each contact's name, phone numbers, and emails | | `location` | the device position as a `"latitude,longitude"` string or an object with `latitude` and `longitude` | | `backgroundLocationEnabled` | `{ enabled, alreadyRunning, reason }` | | `backgroundLocationDisabled` | `{ enabled }` | | `deviceVariable` | `{ name, data }` | | `menuClicked` | the menu item the user tapped | | `popupClosed` | which button dismissed the popup | | `datePicked` | the date/time the user selected | | `backButtonPressed` | `undefined` | | `biometricResult` | `{ data, status, platform }` | | `smartLoginCredentials` | `{ email, password }` | | `purchaseSuccess` / `purchaseFailed` | `{ platform, data }` | | `receiptReceived` | `{ platform, data }` | | `capabilities` | `BdkCapabilities` | | `permissions.changed` | `{ changed, previous, permissions }` | | `att.changed` | `{ status, idfa }` | | `deeplink.received` | `BdkDeepLinkRecord` | | `auth.completed` | `{ mode, provider, handoff }` | | `auth.cancelled` | `{ reason }` | | `push.dataReceived` | `{ id, receivedAt, appState, data }` | | `share.received` | `{ share }` (`BdkInboundShare`) | | `share.uploadProgress` | `{ shareId, itemIndex, bytesSent, totalBytes, progress }` | | `nfc.launchTag` | `{ url, tag }` | | `iap.purchaseCompleted` | `{ platform, requestId, ok, state, code, transaction }` | | `iap.transactionUpdated` | `{ reason, transaction }` | | `error` | `BdkError` | #code :::code-group ```ts [Typed payloads] // The payload type is inferred from the event name. bdk.on("deviceInfo", (info) => info.playerId); // BdkDeviceInfo bdk.on("photoCaptured", (photo) => photo.fileUrl); // MediaResult bdk.on("iap.purchaseCompleted", (e) => e.state); // { platform, requestId, ok, state, ... } bdk.on("biometricResult", ({ status, platform }) => {}); // { data, status, platform } bdk.on("error", (err) => err.code); // BdkError ``` ::: :::: ::::ref-section{title="Handle errors"} A throwing listener doesn't break the others. Catch errors centrally via the `error` event or the `onError` config callback — both receive a `BdkError`. #code :::code-group ```ts [Centralized errors] bdk.on("error", (err) => console.error(err.code, err.message, err.details)); // or at init: const bdk = createBdkNative({ onError: (err) => reportToSentry(err) }); ``` ::: :::: --- # Server helpers URL: https://docs.thebdk.com/server/overview ::::ref-section{title="Call provider APIs from your server"} Server helpers call third-party providers — OneSignal, App Store / Google Play receipts, Branch, ChottuLink, and Firebase Dynamic Links — from your Node backend. Each helper returns a typed result you can `await` directly. ::callout{type="warn"} Never import `@bdk/native/server` or any `@bdk/native/server/*` subpath into a browser bundle — these modules read `process.env` and call provider APIs with secret keys. The browser-safe entry points are `@bdk/native` (root) and `@bdk/native/browser`. :: #code :::code-group ```ts [Server-only helper] // Node 18+ / server runtime ONLY — never in a browser bundle. import { sendPushNotification } from "@bdk/native/server/onesignal"; const result = await sendPushNotification({ title: "Hello", message: "Your order shipped", includeSegments: ["Subscribed Users"] }); // `result` is a real, typed PushNotificationResult — not an event. console.log(result.notificationId, result.numberOfRecipients, result.sentSuccessfully); ``` ::: :::: ::::ref-section{title="Import a provider"} Import the helper for the provider you need from its subpath. Requires Node 18+. ::callout `@bdk/native/server` is a barrel that re-exports `onesignal`, `iap`, `branch`, `chottulink`, and `firebase-dynamic-links`. Prefer a specific subpath when you only need one provider. :: #code :::code-group ```ts [Per-provider subpaths] import { sendPushNotification } from "@bdk/native/server/onesignal"; import { verifyIosReceipt, verifyAndroidReceipt } from "@bdk/native/server/iap"; import { createBranchLink } from "@bdk/native/server/branch"; import { createChottuLink } from "@bdk/native/server/chottulink"; import { createFirebaseDynamicLink } from "@bdk/native/server/firebase-dynamic-links"; ``` ```ts [Barrel entry] // Re-exports every provider from one module. import { sendPushNotification, createChottuLink } from "@bdk/native/server"; ``` ::: :::: ::::ref-section{title="Configure credentials"} Pass credentials as function options, or set the matching environment variables and let each helper read them. Each row lists the option and the variables checked, in order. | Provider | Option | Environment variables (in lookup order) | | --- | --- | --- | | OneSignal | `oneSignalAppId` | `BDK_ONESIGNAL_APP_ID`, `ONESIGNAL_APP_ID` | | OneSignal | `oneSignalApiKey` | `BDK_ONESIGNAL_API_KEY`, `ONESIGNAL_API_KEY` | | IAP (Apple) | `sharedSecret` | `BDK_IOS_INAPP_SHARED_SECRET`, `IOS_INAPP_SHARED_SECRET`, `APP_STORE_SHARED_SECRET` | | IAP (Google) | `accessToken` | `BDK_GOOGLE_PLAY_ACCESS_TOKEN`, `GOOGLE_PLAY_ACCESS_TOKEN` | | IAP (Google) | `serviceAccount` | `BDK_GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`, `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`, `GOOGLE_APPLICATION_CREDENTIALS_JSON` | | Branch | `branchKey` | `BDK_BRANCH_KEY`, `BRANCH_KEY`, `BRANCH_LIVE_KEY` | | Branch | `branchLinkDomain` | `BDK_BRANCH_LINK_DOMAIN`, `BRANCH_LINK_DOMAIN` | | ChottuLink | `apiKey` | `BDK_CHOTTULINK_API_KEY`, `CHOTTULINK_API_KEY` | | ChottuLink | `domain` | `BDK_CHOTTULINK_DOMAIN`, `CHOTTULINK_DOMAIN`, `CHOTTULINK_LINK_DOMAIN` | | Firebase | `firebaseWebApiKey` | `BDK_FIREBASE_WEB_API_KEY`, `FIREBASE_WEB_API_KEY`, `FIREBASE_DYNAMIC_LINKS_WEB_API_KEY` | | Firebase | `domainUriPrefix` | `BDK_FIREBASE_DYNAMIC_LINK_DOMAIN_URI_PREFIX`, `FIREBASE_DYNAMIC_LINK_DOMAIN_URI_PREFIX` | | Firebase | `iosBundleId` | `BDK_IOS_BUNDLE_ID`, `IOS_BUNDLE_ID` | | Firebase | `iosAppStoreId` | `BDK_IOS_APPSTORE_ID`, `IOS_APPSTORE_ID`, `IOS_APP_STORE_ID` | | Firebase | `androidPackageName` | `BDK_ANDROID_BUNDLE_ID`, `ANDROID_BUNDLE_ID`, `ANDROID_PACKAGE_NAME` | ::callout An explicit option always wins over the environment. A missing required value throws a `ValidationError` — except a missing Google Play credential (`serviceAccount`/`accessToken`), which surfaces as a `ProviderError` (`BDK_PROVIDER_ERROR`). :: #code :::code-group ```ts [Resolve from the environment] // Reads BDK_CHOTTULINK_API_KEY (or CHOTTULINK_API_KEY) and // BDK_CHOTTULINK_DOMAIN (or CHOTTULINK_DOMAIN / CHOTTULINK_LINK_DOMAIN). import { createChottuLink } from "@bdk/native/server/chottulink"; const link = await createChottuLink({ destinationUrl: "https://example.com/welcome" }); ``` ```ts [Pass options explicitly] import { createChottuLink } from "@bdk/native/server/chottulink"; const link = await createChottuLink({ apiKey: process.env.MY_CHOTTU_KEY, domain: "links.example.com", destinationUrl: "https://example.com/welcome" }); ``` ::: :::: ::::ref-section{title="Use a custom fetch"} Pass a `fetch` option to use your own HTTP client — for proxies, retries, or mocking in tests. It defaults to the global `fetch` on Node 18+, so you usually skip it. The type is `FetchLike` (`typeof fetch`). ::callout{type="warn"} If no fetch implementation can be resolved, the call throws a `ProviderError`. Any standard-fetch-compatible function (e.g. `undici`, `node-fetch`, or a test spy) is accepted. :: #code :::code-group ```ts [Inject a custom fetch] import { createChottuLink } from "@bdk/native/server/chottulink"; // FetchLike is `typeof fetch` — any standard-fetch-compatible function works. const tracingFetch: typeof fetch = (input, init) => { console.log("provider request →", input); return globalThis.fetch(input, init); }; const link = await createChottuLink({ destinationUrl: "https://example.com/welcome", fetch: tracingFetch }); ``` ```ts [Mock fetch in a test] import { sendPushNotification } from "@bdk/native/server/onesignal"; const fakeFetch: typeof fetch = async () => new Response(JSON.stringify({ id: "fake-id", recipients: 1 }), { status: 200 }); const result = await sendPushNotification({ oneSignalAppId: "test-app", oneSignalApiKey: "test-key", message: "hi", fetch: fakeFetch }); ``` ::: :::: ::::ref-section{title="Handle errors"} A provider non-OK HTTP status throws with code `BDK_PROVIDER_ERROR`. A missing required option or env var throws with code `BDK_VALIDATION_ERROR`. ::callout{type="warn"} Inspect `error.code` rather than parsing message strings. On `BDK_PROVIDER_ERROR`, `error.details?.status` and `error.details?.response` give you the upstream provider's status and body. :: #code :::code-group ```ts [Catch a provider failure] import { sendPushNotification } from "@bdk/native/server/onesignal"; try { const result = await sendPushNotification({ message: "hi" }); return result; } catch (error) { if ((error as { code?: string }).code === "BDK_PROVIDER_ERROR") { console.error("provider rejected", error.details?.status, error.details?.response); } throw error; } ``` ```ts [Discriminate by code] import { createBranchLink } from "@bdk/native/server/branch"; try { // Throws BDK_VALIDATION_ERROR if branchKey is absent from options and env. await createBranchLink({ deepLinkUrl: "https://example.com/welcome" }); } catch (error) { if (error.code === "BDK_VALIDATION_ERROR") { // A required option/env var was missing. } else if (error.code === "BDK_PROVIDER_ERROR") { // Branch returned a non-OK status. } } ``` ::: :::: --- # Verify purchases URL: https://docs.thebdk.com/server/iap-verification ::::ref-section{title="Verify a Google Play purchase"} Confirm a purchase token with Google Play. Authenticate with a service account or access token, passed in the call or via `BDK_GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` / `BDK_GOOGLE_PLAY_ACCESS_TOKEN`. Returns `{ isValid, payload, errorMessage, raw }`. #code :::code-group ```ts [Env service account] import { verifyAndroidReceipt } from "@bdk/native/server/iap"; const validation = await verifyAndroidReceipt({ packageName: "com.example.app", productId: "pro_monthly", purchaseToken, productType: "subscription" }); if (validation.isValid) { // Read state before granting — see the next section. } ``` ```ts [Explicit auth] await verifyAndroidReceipt({ serviceAccount: JSON.parse(process.env.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON!), packageName: "com.example.app", productId: "coins_100", purchaseToken, productType: "product" }); ``` ::: :::: ::::ref-section{title="Check entitlement before granting"} `isValid: true` only means the token was accepted — not that the user owns the product. Use `readAndroidReceipt(input)` to read purchase state, acknowledgement state, and expiry before granting anything. ::callout{type="warn"} Never grant an entitlement off `isValid` alone. An expired or unacknowledged purchase can still report `isValid: true`. :: #code :::code-group ```ts [Read meaningful fields] import { verifyAndroidReceipt, readAndroidReceipt } from "@bdk/native/server/iap"; const validation = await verifyAndroidReceipt({ packageName: "com.example.app", productId: "pro_monthly", purchaseToken, productType: "subscription" }); if (validation.isValid) { // Google's purchase response has no productId, so tag the entry with the // one you verified — readAndroidReceipt filters receiptData by productId. const product = readAndroidReceipt({ productId: "pro_monthly", receiptData: [{ productId: "pro_monthly", ...(validation.raw as Record) }] }); if (Number(product.expiresDateMs) > Date.now() && product.acknowledgementState === 1) { grantEntitlement(product.id); } } ``` ::: :::: ::::ref-section{title="Consume a one-time product"} Mark a consumable product as consumed so it can be purchased again. #code :::code-group ```ts [Consume] import { consumeAndroidPurchase } from "@bdk/native/server/iap"; await consumeAndroidPurchase({ packageName: "com.example.app", productId: "coins_100", purchaseToken }); ``` ::: :::: ::::ref-section{title="Verify an App Store receipt"} Validate an iOS receipt with Apple. Pass the base64 `receipt`, and your `sharedSecret` (or set `BDK_IOS_INAPP_SHARED_SECRET`). A production receipt that Apple flags as sandbox is retried against the sandbox endpoint automatically; set `useSandboxFallback: false` to opt out. Returns `{ isValid, resultData, errorData, raw }` — `resultData` holds Apple's full response on success, `errorData` the failure reason. #code :::code-group ```ts [Verify] import { verifyIosReceipt } from "@bdk/native/server/iap"; const validation = await verifyIosReceipt({ receipt, sharedSecret: process.env.APP_STORE_SHARED_SECRET }); if (validation.isValid) { // resultData is Apple's full verifyReceipt payload — read it next. } else { console.error(validation.errorData); } ``` ::: :::: ::::ref-section{title="Read an App Store entitlement"} `isValid: true` means Apple accepted the receipt, not that the entitlement is live. Pass the verification's `resultData` to `readIosReceipt(input)` to pull the latest purchase for a product — its purchase, original-purchase, and expiry timestamps — before granting. #code :::code-group ```ts [Read meaningful fields] import { verifyIosReceipt, readIosReceipt } from "@bdk/native/server/iap"; const validation = await verifyIosReceipt({ receipt, sharedSecret: process.env.APP_STORE_SHARED_SECRET }); if (validation.isValid) { const product = readIosReceipt({ productId: "pro_monthly", receiptData: validation.resultData as Record }); if (Number(product.expiresDateMs) > Date.now()) { grantEntitlement(product.id); } } ``` ::: :::: ::::ref-section{title="Verify inside an Express route"} These are plain async functions, so they drop into any handler — Express, a Next API route, a Firebase Function, or any Node service. #code :::code-group ```ts [Express] import express from "express"; import { verifyAndroidReceipt } from "@bdk/native/server/iap"; const app = express(); app.use(express.json()); app.post("/iap/android/verify", async (req, res) => { const validation = await verifyAndroidReceipt({ packageName: req.body.packageName, productId: req.body.productId, purchaseToken: req.body.purchaseToken, productType: req.body.productType }); res.json(validation); }); app.listen(3000); ``` ::: :::: --- # Push notifications URL: https://docs.thebdk.com/server/onesignal-push ::::ref-section{title="Send a push notification"} Send a push through OneSignal from your backend. `await` resolves with the delivery result — notification id, recipient count, and whether OneSignal accepted it. Target recipients with `playerIds`, `subscriptionIds`, or `includeSegments` / `excludeSegments`. Set content with `message`, `title`, `subtitle`, `imageUrl`, `data`, or a saved `templateId`. ::callout{type="warn"} Server-only. Never import `@bdk/native/server/onesignal` (or any `@bdk/native/server/*` entry) into a browser bundle — it carries your OneSignal REST API key. :: #code :::code-group ```ts [Basic] import { sendPushNotification } from "@bdk/native/server/onesignal"; const result = await sendPushNotification({ oneSignalAppId: "00000000-0000-0000-0000-000000000000", oneSignalApiKey: "os_v2_app_...", message: "Your order has shipped!", title: "Order update", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"] }); if (result.sentSuccessfully) { console.log("Delivered", result.notificationId, "to", result.numberOfRecipients); } else { console.error("OneSignal rejected the push:", result.errorMessage); } ``` ```ts [Target a segment] const result = await sendPushNotification({ message: "Flash sale ends tonight", title: "Last chance", includeSegments: ["Subscribed Users"], excludeSegments: ["Engaged Last 24h"] }); ``` ```ts [Rich content + deep link data] const result = await sendPushNotification({ message: "New reply on your post", title: "Inbox", subtitle: "From @jess", imageUrl: "https://cdn.example.com/preview.png", onLoadUrl: "https://app.example.com/posts/42", urlParams: [{ key: "ref", value: "push" }], data: { postId: 42, kind: "reply" }, subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"] }); ``` ::: :::: ::::ref-section{title="Input fields"} The object you pass to `sendPushNotification`. Every field is optional, but each call needs credentials (inline or via env) and at least one targeting field. - `message`, `title`, `subtitle` — body, heading, and iOS subtitle. - `playerIds` / `subscriptionIds` — device targets. Prefer `subscriptionIds`; `playerIds` is the legacy key. - `includeSegments` / `excludeSegments` — target or exclude named OneSignal segments. - `templateId` — send a saved OneSignal template instead of inline content. - `imageUrl` — a media attachment. - `onLoadUrl` + `urlParams` — a launch URL. `urlParams` entries are appended as query parameters and passed through in `data`. - `data` — an arbitrary `Record` payload. - `iosBadgeType`, `iosBadgeCount` — iOS badge controls. - `oneSignalAppId`, `oneSignalApiKey` — inline credentials. - `targetChannel`, `endpoint`, `authorizationScheme`, `fetch` — advanced overrides. ::callout The `OneSignalUrlParam` type used by `urlParams` is `{ key: string; value: string | number | boolean }`. :: #code :::code-group ```ts [Type] import type { SendPushNotificationInput, OneSignalUrlParam } from "@bdk/native/server/onesignal"; const input: SendPushNotificationInput = { message: "Welcome aboard", title: "Hello", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"], data: { onboarding: true } }; ``` ```ts [Send a saved template] const result = await sendPushNotification({ templateId: "5a5a1018-1656-4e8e-8a52-8a52a5b5a5b5", includeSegments: ["Total Subscriptions"] }); ``` ::: :::: ::::ref-section{title="Read the result"} `sendPushNotification` resolves with a `PushNotificationResult`. Check it to confirm delivery and surface failures. - `notificationId: string | null` — the OneSignal notification id. - `numberOfRecipients: number | null` — how many recipients OneSignal reported. - `sentSuccessfully: boolean` — `true` only when OneSignal returned no `errors`. - `errorMessage: string | null` — the errors OneSignal returned, or `null` on success. - `raw: unknown` — the full parsed OneSignal response. ::callout{type="warn"} A `200 OK` can still carry errors (e.g. "All included players are not subscribed"), so always check `sentSuccessfully` — not just the absence of a thrown error. A non-2xx status throws a `BdkError` with code `BDK_PROVIDER_ERROR`. :: #code :::code-group ```ts [Handle the result] import { sendPushNotification } from "@bdk/native/server/onesignal"; try { const result = await sendPushNotification({ message: "Ping", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"] }); if (!result.sentSuccessfully) { // 2xx response but OneSignal reported errors in the body. console.warn("Not delivered:", result.errorMessage, result.raw); } } catch (err) { if (err.code === "BDK_PROVIDER_ERROR") { console.error("OneSignal HTTP error:", err.details); } else { throw err; } } ``` ::: :::: ::::ref-section{title="Set credentials"} Pass your OneSignal app id and REST API key inline as `oneSignalAppId` / `oneSignalApiKey`, or set them in the environment and omit them from the call. For the app id it reads `BDK_ONESIGNAL_APP_ID`, then `ONESIGNAL_APP_ID`; for the key, `BDK_ONESIGNAL_API_KEY`, then `ONESIGNAL_API_KEY`. Inline values win. Missing either throws a `BdkError` with code `BDK_VALIDATION_ERROR`. The `Authorization` scheme is picked from your key automatically — override it with `authorizationScheme` (`"Basic" | "Key" | "Bearer"`) if needed. ::callout Prefer env vars — keep secrets out of your code and rotate keys without redeploying. :: #code :::code-group ```ts [From env] // BDK_ONESIGNAL_APP_ID and BDK_ONESIGNAL_API_KEY (or ONESIGNAL_APP_ID / // ONESIGNAL_API_KEY) are read from the environment — no credentials in code. import { sendPushNotification } from "@bdk/native/server/onesignal"; const result = await sendPushNotification({ message: "Server-driven push", includeSegments: ["Subscribed Users"] }); ``` ```ts [Explicit credentials + scheme override] const result = await sendPushNotification({ oneSignalAppId: process.env.MY_APP_ID, oneSignalApiKey: process.env.MY_REST_KEY, authorizationScheme: "Bearer", message: "Custom auth scheme", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"] }); ``` ::: :::: ::::ref-section{title="Call from a backend"} Drop the call into any backend route, queue worker, or cron job. Node 18+ uses the global `fetch`; on older runtimes pass your own via `fetch`. Override `endpoint` to point at a proxy or test server (defaults to `https://onesignal.com/api/v1/notifications`). ::callout{type="warn"} If no `fetch` is available and you don't pass one, the request throws a `BdkError` with code `BDK_PROVIDER_ERROR`. :: #code :::code-group ```ts [Express route] import express from "express"; import { sendPushNotification } from "@bdk/native/server/onesignal"; const app = express(); app.use(express.json()); app.post("/notify", async (req, res) => { const result = await sendPushNotification({ message: req.body.message, title: req.body.title, subscriptionIds: req.body.subscriptionIds // credentials read from env }); res.status(result.sentSuccessfully ? 200 : 502).json(result); }); ``` ```ts [Custom fetch / endpoint] import { sendPushNotification } from "@bdk/native/server/onesignal"; import fetch from "node-fetch"; const result = await sendPushNotification({ message: "Via a custom client", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"], fetch: fetch as unknown as typeof globalThis.fetch, endpoint: "https://onesignal.com/api/v1/notifications" }); ``` ::: :::: --- # Branch deep links URL: https://docs.thebdk.com/server/branch ::::ref-section{title="Create a deep link"} Generate a Branch deep link from your backend — a share link, invite URL, or anything you return from an API route. Server-only: import from `@bdk/native/server/branch` and never include it in a browser bundle. Only `deepLinkUrl` is required. If it has no protocol, `https://` is added; any `urlParams` are appended as query parameters. ::callout{type="warn"} On success, `dynamicLink` holds the link and `errorMessage` is `null`. If Branch returns no URL, the call resolves with `dynamicLink: null` and an `errorMessage` — check it. Transport or non-`2xx` errors throw a `BdkError` with code `BDK_PROVIDER_ERROR`, so wrap the call in `try/catch`. :: #code :::code-group ```ts [Basic] import { createBranchLink } from "@bdk/native/server/branch"; const result = await createBranchLink({ deepLinkUrl: "myapp://product/42" }); if (result.dynamicLink) { console.log("Share this link:", result.dynamicLink); } else { console.error(result.errorMessage); } ``` ```ts [With analytics and social] import { createBranchLink } from "@bdk/native/server/branch"; const result = await createBranchLink({ deepLinkUrl: "myapp://product/42", alias: "summer-sale", iosUrl: "https://apps.apple.com/app/id000000000", androidUrl: "https://play.google.com/store/apps/details?id=com.example", desktopUrl: "https://example.com/product/42", analytics: { channel: "email", feature: "promo", campaign: "summer-2026", stage: "launch", tags: ["sale", "newsletter"] }, social: { title: "50% off today", description: "Tap to open the deal in the app.", imageUrl: "https://example.com/og/sale.png" } }); ``` ::: :::: ::::ref-section{title="Input and result types"} Import these types for autocomplete on the options you pass and the result you get back. `deepLinkUrl` is the only required input. `CreateBranchLinkInput`: - `deepLinkUrl: string` — the destination the link opens (required). - `branchKey?: string` — Branch key; falls back to env if omitted. - `branchLinkDomain?: string` — your Branch link domain; falls back to env if omitted. - `desktopUrl?`, `iosUrl?`, `androidUrl?: string` — platform fallback URLs. `desktopUrl` defaults to `deepLinkUrl` when not set. - `alias?: string` — a custom path for the generated link. - `analytics?: BranchAnalytics` — `{ channel?, feature?, campaign?, stage?, tags? }`, where `tags` is a `string[]`. - `social?: BranchSocial` — `{ title?, description?, imageUrl? }`, mapped to Open Graph fields. - `urlParams?: Record` — query parameters appended to `deepLinkUrl`. - `baseUrl?: string` — override the Branch API base (defaults to `https://api2.branch.io/v1`). - `fetch?: FetchLike` — provide a custom `fetch` implementation (defaults to the global `fetch`). `BranchLinkResult`: - `dynamicLink: string | null` — the generated Branch URL, or `null` on failure. - `errorMessage: string | null` — a message when no URL was returned, otherwise `null`. - `raw: unknown` — the raw Branch response payload, for debugging or reading extra fields. #code :::code-group ```ts [Types] import type { CreateBranchLinkInput, BranchLinkResult, BranchAnalytics, BranchSocial } from "@bdk/native/server/branch"; const input: CreateBranchLinkInput = { deepLinkUrl: "myapp://invite", urlParams: { ref: "user_123", utm_source: "share" } }; ``` ```ts [Inspecting the raw response] const result: BranchLinkResult = await createBranchLink({ deepLinkUrl: "myapp://invite" }); console.log(result.raw); // full Branch payload ``` ::: :::: ::::ref-section{title="Set your Branch credentials"} Branch needs a key and a link domain. Set them once as environment variables, or pass them per call to target a different Branch app. A value on `input` always wins; otherwise these env vars are read in order: - `branchKey` — `BDK_BRANCH_KEY`, then `BRANCH_KEY`, then `BRANCH_LIVE_KEY`. - `branchLinkDomain` — `BDK_BRANCH_LINK_DOMAIN`, then `BRANCH_LINK_DOMAIN`. If neither input nor env supplies a value, the call throws a `BdkError` with code `BDK_VALIDATION_ERROR` naming the missing field (e.g. `branchKey is required.`). ::callout{type="warn"} Only call `createBranchLink` from server-side code. Importing `@bdk/native/server/branch` into a browser bundle would leak your Branch key. :: #code :::code-group ```ts [Via environment] // Set in your server environment: // BDK_BRANCH_KEY=key_live_xxx // BDK_BRANCH_LINK_DOMAIN=example.app.link import { createBranchLink } from "@bdk/native/server/branch"; const result = await createBranchLink({ deepLinkUrl: "myapp://home" }); ``` ```ts [Explicit key] import { createBranchLink } from "@bdk/native/server/branch"; const result = await createBranchLink({ branchKey: "key_live_xxx", branchLinkDomain: "example.app.link", deepLinkUrl: "myapp://home" }); ``` ```ts [Handling missing credentials] import { createBranchLink } from "@bdk/native/server/branch"; try { await createBranchLink({ deepLinkUrl: "myapp://home" }); } catch (err) { if (err.code === "BDK_VALIDATION_ERROR") { console.error("Missing Branch config:", err.message); } } ``` ::: :::: --- # ChottuLink deep links URL: https://docs.thebdk.com/server/chottulink ::::ref-section{title="Create a deep link"} Call `createChottuLink(input)` to generate a dynamic deep link. It resolves to the finished link. Only `destinationUrl` is required. A `destinationUrl` without a scheme gets `https://` added for you. ::callout{type="warn"} Server-only. This uses your secret API key, so never import `@bdk/native/server/chottulink` into a browser bundle. :: #code :::code-group ```ts [Basic] import { createChottuLink } from "@bdk/native/server/chottulink"; const result = await createChottuLink({ apiKey: process.env.CHOTTULINK_API_KEY, domain: "links.example.com", destinationUrl: "https://example.com/products/42" }); console.log(result.dynamicLink); // the generated short link, or null ``` ```ts [Protocol optional] // destinationUrl without a scheme is upgraded to https:// for you. const result = await createChottuLink({ apiKey: process.env.CHOTTULINK_API_KEY, domain: "links.example.com", destinationUrl: "example.com/welcome" }); ``` ::: :::: ::::ref-section{title="Set credentials"} Pass `apiKey` and `domain` directly, or set them as environment variables. Use env vars in production to keep secrets out of code. The key is read from `BDK_CHOTTULINK_API_KEY`, then `CHOTTULINK_API_KEY`. The domain is read from `BDK_CHOTTULINK_DOMAIN`, then `CHOTTULINK_DOMAIN`, then `CHOTTULINK_LINK_DOMAIN`. An explicit value always wins over the environment. ::callout{type="warn"} The API key is a secret. Never ship it to the browser or commit it to source control. :: #code :::code-group ```ts [From environment] // Reads BDK_CHOTTULINK_API_KEY / CHOTTULINK_API_KEY for the key, // and BDK_CHOTTULINK_DOMAIN / CHOTTULINK_DOMAIN / CHOTTULINK_LINK_DOMAIN for the domain. import { createChottuLink } from "@bdk/native/server/chottulink"; const result = await createChottuLink({ destinationUrl: "https://example.com/invite/abc" }); ``` ```ts [Explicit key] const result = await createChottuLink({ apiKey: "ck_live_xxx", domain: "https://links.example.com/", // protocol + trailing slash are stripped destinationUrl: "https://example.com/invite/abc" }); ``` ::: :::: ::::ref-section{title="Input options"} Every field you can pass to `createChottuLink`. Only `destinationUrl` is required; `apiKey` and `domain` are required either here or via the environment. - `destinationUrl: string` — where the link resolves to. Gets `https://` added if no scheme is present. - `apiKey?: string` — ChottuLink API key. Falls back to env vars. - `domain?: string` — the link domain. Falls back to env vars; protocol and trailing slashes are stripped. - `linkName?: string` — a human-readable name. Defaults to `selectedPath`, then to `"BDK Native Deeplink"`. - `selectedPath?: string` — custom path segment; a leading `/` is removed and the value trimmed. - `iosBehavior?: 1 | 2` — iOS open behavior. Defaults to `2`. - `androidBehavior?: 1 | 2` — Android open behavior. Defaults to `2`. - `utm?: ChottuLinkUtm` — UTM attribution (`source`, `medium`, `campaign`, `term`, `content`), all optional. - `social?: ChottuLinkSocial` — social preview (`title`, `description`, `imageUrl`), all optional. - `urlParams?: Record` — query params appended onto `destinationUrl` (empty/null/undefined values are skipped). - `extraParams?: Record` — raw fields merged into the request body for options not modeled here. - `baseUrl?: string` — override the API base. Defaults to `https://api2.chottulink.com/chotuCore/pa/v1`. - `fetch?: FetchLike` — a custom `fetch` (for testing or non-Node runtimes). #code :::code-group ```ts [Full input] import { createChottuLink } from "@bdk/native/server/chottulink"; const result = await createChottuLink({ apiKey: process.env.CHOTTULINK_API_KEY, domain: "links.example.com", destinationUrl: "https://example.com/products/42", linkName: "summer-promo", selectedPath: "/promo/summer", iosBehavior: 2, androidBehavior: 1, urlParams: { ref: "newsletter", discount: 20 }, utm: { source: "newsletter", medium: "email", campaign: "summer_sale" }, social: { title: "Summer Sale", description: "Up to 50% off", imageUrl: "https://cdn.example.com/sale.png" } }); ``` ::: :::: ::::ref-section{title="Add UTM, social preview, and custom params"} Use `utm` and `social` to attach attribution and rich link previews. Empty values are dropped, so you only send what you set. Use `urlParams` to append query parameters onto the destination URL itself. For any request-body field not modeled here, use `extraParams`. #code :::code-group ```ts [urlParams + extraParams] const result = await createChottuLink({ destinationUrl: "https://example.com/landing", // appended to the destination URL: ...?ref=app&v=3 urlParams: { ref: "app", v: 3 }, // merged into the raw request body for unmodeled options extraParams: { custom_field: "value" } }); ``` ::: :::: ::::ref-section{title="Read the result"} The resolved value is a `ChottuLinkResult` with three fields: - `dynamicLink: string | null` — the generated link (the API's `short_url`, falling back to `long_url`), or `null`. - `errorMessage: string | null` — an error message from the API response, or `null`. - `raw: unknown` — the full, unmodified parsed response. ::callout{type="warn"} Check `dynamicLink` for `null` and inspect `errorMessage` before using the link. :: #code :::code-group ```ts [Handling the result] const { dynamicLink, errorMessage, raw } = await createChottuLink({ destinationUrl: "https://example.com/share" }); if (errorMessage || !dynamicLink) { console.error("ChottuLink failed", errorMessage, raw); } else { console.log("Deep link:", dynamicLink); } ``` ::: :::: ::::ref-section{title="Handle errors"} `createChottuLink` throws when a required field is missing (`BDK_VALIDATION_ERROR`) or the request fails (`BDK_PROVIDER_ERROR`). For failed requests, `details` carries the HTTP `status` and response body. ::callout A transport or validation failure throws; an application error inside a successful response surfaces as `result.errorMessage` instead. :: #code :::code-group ```ts [Catching errors] import { createChottuLink } from "@bdk/native/server/chottulink"; try { const result = await createChottuLink({ destinationUrl: "https://example.com/share" }); // ...use result.dynamicLink } catch (err) { if (err.code === "BDK_VALIDATION_ERROR" || err.code === "BDK_PROVIDER_ERROR") { console.error(err.code, err.message, err.details); } throw err; } ``` ::: :::: --- # Firebase Dynamic Links URL: https://docs.thebdk.com/server/firebase-dynamic-links ::::ref-section{title="Create a short link"} Turn a deep link into a short Firebase Dynamic Link from your server. Resolves to a `FirebaseDynamicLinkResult` with `dynamicLink`, `previewLink`, and the `raw` response. Import from `@bdk/native/server/firebase-dynamic-links`; server-only. ::callout{type="warn"} Deprecated — Firebase Dynamic Links has been sunset by Google, so this helper no longer produces working links. For new work, use `@bdk/native/server/branch` (`createBranchLink`) or `@bdk/native/server/chottulink`. :: #code :::code-group ```ts [Basic] import { createFirebaseDynamicLink } from "@bdk/native/server/firebase-dynamic-links"; const result = await createFirebaseDynamicLink({ firebaseWebApiKey: process.env.FIREBASE_WEB_API_KEY, domainUriPrefix: "https://example.page.link", deepLinkUrl: "https://example.com/items/42", iosBundleId: "com.example.app", iosAppStoreId: "1234567890", androidPackageName: "com.example.app" }); if (result.dynamicLink) { console.log("Short link:", result.dynamicLink); } else { console.error("Failed:", result.errorMessage); } ``` ::: :::: ::::ref-section{title="Set inputs from the environment"} Each required value can be passed inline or read from an environment variable. Only `deepLinkUrl` has no env fallback — always pass it. - `firebaseWebApiKey` → `BDK_FIREBASE_WEB_API_KEY`, `FIREBASE_WEB_API_KEY`, `FIREBASE_DYNAMIC_LINKS_WEB_API_KEY` - `domainUriPrefix` → `BDK_FIREBASE_DYNAMIC_LINK_DOMAIN_URI_PREFIX`, `FIREBASE_DYNAMIC_LINK_DOMAIN_URI_PREFIX` - `iosBundleId` → `BDK_IOS_BUNDLE_ID`, `IOS_BUNDLE_ID` - `iosAppStoreId` → `BDK_IOS_APPSTORE_ID`, `IOS_APPSTORE_ID`, `IOS_APP_STORE_ID` - `androidPackageName` → `BDK_ANDROID_BUNDLE_ID`, `ANDROID_BUNDLE_ID`, `ANDROID_PACKAGE_NAME` A `domainUriPrefix` with no scheme gets `https://` prepended. ::callout{type="warn"} A missing required value (in both input and environment) throws a `BdkError` with code `BDK_VALIDATION_ERROR` before any network request. :: #code :::code-group ```ts [From environment] // With BDK_FIREBASE_WEB_API_KEY, *_DOMAIN_URI_PREFIX, IOS_BUNDLE_ID, // IOS_APPSTORE_ID, and ANDROID_PACKAGE_NAME set in the environment: import { createFirebaseDynamicLink } from "@bdk/native/server/firebase-dynamic-links"; const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/promo" }); ``` ```ts [Inline values] const result = await createFirebaseDynamicLink({ firebaseWebApiKey: "AIza...", domainUriPrefix: "example.page.link", // normalized to https://example.page.link deepLinkUrl: "https://example.com/promo", iosBundleId: "com.example.app", iosAppStoreId: "1234567890", androidPackageName: "com.example.app" }); ``` ::: :::: ::::ref-section{title="Read the result"} The promise resolves to a `FirebaseDynamicLinkResult`. Check `dynamicLink` or `errorMessage` before using the link. - `dynamicLink: string | null` — the short link, or `null` on failure. - `previewLink: string | null` — the preview link, or `null` if absent. - `errorMessage: string | null` — `null` on success, otherwise the API error. - `raw: unknown` — the full JSON response. ::callout{type="warn"} A non-2xx or transport error throws a `ProviderError` (`BDK_PROVIDER_ERROR`), so wrap the call in try/catch. A `2xx` response that lacks a short link resolves with `dynamicLink: null` and `errorMessage` set — branch on that too. :: #code :::code-group ```ts [Handling the result] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/items/42" }); if (result.errorMessage) { // Inspect result.raw for the full Firebase response payload. throw new Error(`Dynamic link failed: ${result.errorMessage}`); } return { link: result.dynamicLink, preview: result.previewLink }; ``` ::: :::: ::::ref-section{title="Add query params and redirect options"} Shape the URL and redirect behavior of the generated link. - `urlParams` appends query parameters to `deepLinkUrl`; `null`/`undefined` values are dropped. - `suffixOption` sets the path length: `"SHORT"` (case-insensitive) for a short suffix, otherwise `"UNGUESSABLE"`. - `enableForcedRedirect: true` skips the app preview page. #code :::code-group ```ts [Params + suffix] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/checkout", urlParams: { ref: "newsletter", discount: 20 }, suffixOption: "SHORT", enableForcedRedirect: true }); ``` ::: :::: ::::ref-section{title="Set platform fallbacks"} Control where users without the app installed are sent with `iosFallback` and `androidFallback` (both `FirebaseDynamicLinkFallback`). The `type` field (case- and spacing-insensitive) picks the behavior: - `"app-store-page"` / `"play-store-page"` — sends to the store; the iOS / Android default. No `url` needed. - `"same-url"` — redirects to `deepLinkUrl`. - `"fallback-url"` — redirects to a custom `url` (required), with optional `urlParams`. Use `iosIpadFallbackUrl` for a dedicated iPad fallback. #code :::code-group ```ts [Custom fallbacks] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/items/42", iosFallback: { type: "fallback-url", url: "https://example.com/get-the-app", urlParams: { platform: "ios" } }, androidFallback: { type: "same-url" }, iosIpadFallbackUrl: "https://example.com/ipad" }); ``` ::: :::: ::::ref-section{title="Add a social preview and analytics tags"} Add a rich preview card or campaign tracking. Empty sections are omitted from the request. - `social` (`FirebaseDynamicLinkSocial`) — the preview card: `title`, `description`, `imageUrl`. - `googlePlayAnalytics` (`FirebaseGooglePlayAnalytics`) — UTM params (`utmSource`, `utmMedium`, `utmCampaign`, `utmTerm`, `utmContent`) plus `gclid`. - `itunesConnectAnalytics` (`FirebaseItunesConnectAnalytics`) — Apple campaign fields `at`, `ct`, `mt`, `pt`. #code :::code-group ```ts [With metadata] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/promo", social: { title: "Summer Sale", description: "Up to 50% off — open in the app.", imageUrl: "https://example.com/og.png" }, googlePlayAnalytics: { utmSource: "newsletter", utmMedium: "email", utmCampaign: "summer_2026" }, itunesConnectAnalytics: { at: "affiliate-token", ct: "summer_2026" } }); ``` ::: :::: ::::ref-section{title="Use a custom endpoint or fetch"} Point at a different endpoint or inject a `fetch` for testing or proxying. Set `baseUrl` (default `https://firebasedynamiclinks.googleapis.com/v1`) and pass a `fetch` (`FetchLike`) matching the global `fetch` signature. #code :::code-group ```ts [Injected fetch] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/items/42", baseUrl: "https://my-proxy.internal/firebase/v1", fetch: myCustomFetch }); ``` ::: :::: --- # Objects URL: https://docs.thebdk.com/reference/objects ::::ref-section{title="NativeCommandResult"} What legacy dispatch-style browser commands resolve to. It tells you whether the call reached the app — not the outcome of the native action (that arrives on an event). The common case is `triggered: true`. Outside the app a command comes back with `triggered: false` — `pending` in a browser tab, `skipped` in a non-DOM environment. | Property | Type | Description | | --- | --- | --- | | `command` | `string` | The native command that was dispatched. | | `queued` | `boolean` | Whether the command was scheduled. | | `triggered` | `boolean` | Whether it was handed to the app. Branch on `!triggered` for a web fallback. | | `skipped` | `boolean` | Whether it was dropped without triggering. | | `pending` | `boolean` | Queued, waiting for the app. A browser tab outside the app resolves here. | | `reason` | `string` | Why it was skipped or pending — e.g. `waiting_for_agent`, `not_native`. | :::: ::::ref-section{title="Action results"} What namespaced helpers such as `bdk.media.pickPhotos` and `bdk.iap.products` resolve to. On success `ok` is `true` and extra helper-specific fields may be present; on failure it is `false` with `code` and `message` — branch on `!result.ok` or `isBdkError(result)`. Outside the app, Action-result helpers resolve `ok: false` with a code like `common/feature_disabled` or `common/timeout` (branch on `!result.ok`); legacy commands resolve `triggered: false` (branch on `!result.triggered`). | Property | Type | Description | | --- | --- | --- | | `ok` | `boolean` | `true` on success, `false` on failure. | | `action` | `string` | The helper that ran. | | `code` | `string` | Error code when `ok` is `false` — e.g. `common/feature_disabled`. | | `message` | `string` | Human-readable error when `ok` is `false`. | | `recoverable` | `boolean` | Whether the failure is recoverable. Present when `ok` is `false`. | | `details` | `object` | Extra error context, when available. | | `requestId` | `string` | Correlation id, when the call supplied or generated one. | :::: ::::ref-section{title="BdkDeviceInfo"} The device snapshot returned by `bdk.ready()` and `bdk.getDeviceInfo()`, and delivered on the `deviceInfo` event. Most fields are `null` until the app reports them; `biometricsAvailable` and `smartLoginAvailable` are required booleans, and `idfa`, `appTrackingPermissionStatus`, and `viewType` are optional (absent until reported, not `null`). | Property | Type | Description | | --- | --- | --- | | `playerId` | `string \| null` | OneSignal player id for push. | | `pushToken` | `string \| null` | Device push token. | | `deviceModel` | `string \| null` | Device model name. | | `deviceOS` | `string \| null` | Operating system. | | `deviceOSVersion` | `string \| null` | OS version. | | `bdkRelease` | `string \| number \| null` | BDK runtime release. | | `deviceLanguage` | `string \| null` | Device language. | | `deviceWidth` | `string \| number \| null` | Screen width. | | `deviceHeight` | `string \| number \| null` | Screen height. | | `versionName` | `string \| null` | Host app version name. | | `versionCode` | `string \| number \| null` | Host app version code. | | `biometricsAvailable` | `boolean` | Whether biometric login is available. | | `smartLoginAvailable` | `boolean` | Whether saved login is available. | | `cameraPermissionStatus` | `string \| null` | Camera permission state. | | `contactsPermissionStatus` | `string \| null` | Contacts permission state. | | `audiorecordPermissionStatus` | `string \| null` | Audio-record permission state. | | `externalstoragePermissionStatus` | `string \| null` | Storage permission state. | | `locationPermissionStatus` | `string \| null` | Location permission state. | | `idfa` | `string` (optional) | iOS advertising id. iOS only. | | `appTrackingPermissionStatus` | `string` (optional) | iOS App Tracking Transparency state. iOS only. | | `viewType` | `string` (optional) | Host view type, when reported. | :::: ::::ref-section{title="BdkCapabilities"} Returned by `bdk.capabilities.get()`, and delivered on the `capabilities` event. `null` from `get()` means the snapshot is unknown — not that every feature is off. | Property | Type | Description | | --- | --- | --- | | `api` | `number` | API version. | | `platform` | `string` | `"ios"` or `"android"`. | | `app` | `object` | Host app `versionName` and `versionCode`, when reported. | | `features` | `object` | Map of feature id → entry. Ids include `permissions`, `push.prompt`, `push.data`, `att`, `contacts.picker`, `contacts.book`, `badge`, `deeplink.scheme`, `media`, `share.files`, `share.inbound`, `auth.oauth`, `notifications.timesensitive`, `notifications.critical`, `health.read`, `nfc`, `iap`, `ui.datepicker`. | | `features[id].enabled` | `boolean` | Whether this build has the feature on. | | `features[id].available` | `boolean` | Whether the device can use it. Present when reported. | | `features[id].config` | `object` | Feature-specific config, when reported. | | `features[id].details` | `object` | Extra feature details, when reported. | | `gating` | `object` | `{ schema }` when the app reports a gating schema. | :::: ::::ref-section{title="MediaResult"} Delivered by `photoCaptured`, `photoSelected`, and `audioRecorded`. Use `dataUri` to preview inline and `fileUrl` to upload. | Property | Type | Description | | --- | --- | --- | | `fileUrl` | `string \| null` | Hosted URL of the file — use to upload. | | `dataUri` | `string \| null` | Data URI — use to preview inline. | | `contentType` | `string \| null` | The file's MIME type. | | `data` | — | The raw file data from the device. | :::: ::::ref-section{title="BdkMediaItem"} A JPEG from `bdk.media.pickPhotos()` or `bdk.media.takePhoto()`. Exactly one of `base64` or `fileUrl` is set; the other is `null`. | Property | Type | Description | | --- | --- | --- | | `kind` | `string` | `"image"`. | | `mimeType` | `string` | `"image/jpeg"`. | | `base64` | `string \| null` | JPEG bytes as base64. | | `fileUrl` | `string \| null` | Hosted file URL. | | `width` | `number` | Pixel width. | | `height` | `number` | Pixel height. | | `sizeBytes` | `number` | File size in bytes. | | `name` | `string` | File name. | :::: ::::ref-section{title="BiometricResult"} Delivered on the `biometricResult` event after `bdk.auth.authenticateBiometrics()`. | Property | Type | Description | | --- | --- | --- | | `status` | `string` | `"success"` or `"failed"`. | | `platform` | `string` | `"ios"` or `"android"`. | | `data` | — | The raw native auth result. | :::: ::::ref-section{title="SmartLoginCredentials"} Delivered on the `smartLoginCredentials` event. If either field is `null`, nothing usable is saved — fall back to your login screen. | Property | Type | Description | | --- | --- | --- | | `email` | `string \| null` | The saved email. | | `password` | `string \| null` | The saved password. | :::: ::::ref-section{title="DeviceVariableResult"} Delivered on the `deviceVariable` event after reading a cached value. | Property | Type | Description | | --- | --- | --- | | `name` | `string` | The variable name. | | `data` | — | The stored value, or `null` if the key was never stored. | :::: ::::ref-section{title="UrlParam"} A query parameter you pass to `bdk.navigation.openLink({ urlParams })`. | Property | Type | Description | | --- | --- | --- | | `key` | `string` | The parameter name. | | `value` | `string \| number \| boolean` | The parameter value. | :::: ::::ref-section{title="BdkError"} Thrown by a rejected command and passed to the `onError` config callback and the `error` event. | Property | Type | Description | | --- | --- | --- | | `code` | `string` | One of `BDK_NOT_NATIVE`, `BDK_NATIVE_UNAVAILABLE`, `BDK_UNSUPPORTED_VERSION`, `BDK_UNSUPPORTED_PLATFORM`, `BDK_LISTENER_ERROR`, `BDK_PROVIDER_ERROR`, `BDK_VALIDATION_ERROR`. | | `message` | `string` | A human-readable description. | | `details` | `object` | Extra context about the error, when available. | :::: ::::ref-section{title="BdkContact"} A contact from `bdk.contacts.pick()` or `bdk.contacts.list()`. | Property | Type | Description | | --- | --- | --- | | `name` | `string` | Display name. | | `givenName` | `string` | Given name. | | `familyName` | `string` | Family name. | | `phones` | `array` | `{ type, label, number }`. | | `emails` | `array` | `{ type, label, address }`. | :::: ::::ref-section{title="Permission entry"} One value in the `permissions` map from `bdk.permissions.status()` and the `permissions.changed` event. | Property | Type | Description | | --- | --- | --- | | `status` | `string` | `"granted"`, `"denied"`, `"notDetermined"`, `"limited"`, `"provisional"`, `"restricted"`, or `"unsupported"`. | | `canPrompt` | `boolean` | Whether the OS will still show a prompt. | | `detail` | `object` | Optional `{ precision, scope, degraded }`. | :::: ::::ref-section{title="BdkDeepLinkRecord"} Delivered on `deeplink.received`. URL form (`form: "url"`) already navigated; path form (`form: "path"`) is for in-page routing. | Property | Type | Description | | --- | --- | --- | | `form` | `string` | `"url"` or `"path"`. | | `rawUrl` | `string` | The raw incoming URI. | | `source` | `string` | `"scheme"`. | | `coldStart` | `boolean` | Whether the app was launched by this link. | | `id` | `string` | Record id — events and replay are deduped by this. | | `ts` | — | When the link was received. | | `targetUrl` | `string` | URL-form destination. | | `path` | `string` | Path-form path. | | `query` | `object` | Path-form query. | :::: ::::ref-section{title="BdkIapProduct"} A store product from `bdk.iap.products()`. | Property | Type | Description | | --- | --- | --- | | `id` | `string` | Product id. | | `kind` | `string` | `"consumable"`, `"nonConsumable"`, `"subscription"`, or `"nonRenewingSubscription"`. | | `title` | `string` | Store title. | | `description` | `string` | Store description. | | `price` | `object` | `{ amount, currency, formatted }`. | | `subscription` | `object` | Present on subscriptions: `{ groupId, period, plans }`. | | `subscription.plans` | `array` | Each plan has `planId`, `offerId`, and `phases`. | | `subscription.plans[].phases` | `array` | `{ type, price, period, cycles }`. `type` is `"trial"`, `"intro"`, or `"base"`. | | `raw` | `Record` (optional) | Raw store payload. | :::: ::::ref-section{title="BdkIapEntitlement"} A current entitlement from `bdk.iap.entitlements()` or `bdk.iap.restore()`. | Property | Type | Description | | --- | --- | --- | | `productId` | `string` | Product id. | | `kind` | `string` | Product kind. | | `state` | `string` | `"active"`, `"gracePeriod"`, `"billingRetry"`, `"revoked"`, or `"expired"`. | | `expiresAt` | `string \| null` | Expiry, when the store reports one. | | `willRenew` | `boolean` | Whether the subscription will renew. | | `planId` | `string \| null` | Current plan id, when present. | | `latestTransactionId` | `string` | Latest transaction id, when present. | | `purchaseToken` | `string \| null` | Store purchase token, when present. | | `acknowledged` | `boolean` | Whether the purchase is acknowledged. | :::: ::::ref-section{title="IAP purchase completed"} Delivered on `iap.purchaseCompleted`. `pending` is not a final grant — keep listening. | Property | Type | Description | | --- | --- | --- | | `platform` | `string` | `"ios"` or `"android"`. | | `requestId` | `string \| null` | Correlation id. | | `ok` | `boolean` | Whether the purchase succeeded. | | `state` | `string` | `"purchased"`, `"pending"`, `"cancelled"`, or `"failed"`. | | `code` | `string \| null` | Error code when the purchase failed. | | `transaction` | `object` | Store transaction, when present. | :::: ::::ref-section{title="BdkInboundShare"} Inbound content from the OS share sheet, on `share.received` and from `bdk.share.getPending()`. File bytes and local paths are never included. | Property | Type | Description | | --- | --- | --- | | `shareId` | `string` | Share id. | | `receivedAt` | — | When the share arrived. | | `source` | `string` | `"ios-extension"` or `"android-intent"`. | | `status` | `string` | `"pending"`, `"complete"`, `"partial"`, or `"failed"`. | | `items` | `array` | Text, URL, or file items. File items have `kind` (`image`, `video`, or `file`), `name`, `mime`, `sizeBytes`, and `upload`. | | `items[].upload.status` | `string` | `"pending"`, `"uploading"`, `"uploaded"`, `"failed"`, `"rejected"`, or `"skipped"`. | | `items[].upload.httpStatus` | — | HTTP status, when present. | | `items[].upload.response` | — | Upload response, when present. | | `items[].upload.error` | — | Upload error, when present. | :::: ::::ref-section{title="BdkShareItem"} One item passed to `bdk.share.send()`. Mix types freely in a single call. | Property | Type | Description | | --- | --- | --- | | `type` | `string` | `"text"`, `"url"`, `"file"`, `"image"`, `"video"`, or `"audio"`. | | `text` | `string` | The text to share. Text items only. | | `url` | `string` | The link, or the remote file to fetch and share. | | `filename` | `string` | Optional name for the shared file. | :::: ::::ref-section{title="BdkPushDataMessage"} One retained background data message from `bdk.push.getDataMessages()`; live arrivals carry the same fields on `push.dataReceived`. | Property | Type | Description | | --- | --- | --- | | `id` | `string` | Message id. | | `receivedAt` | `number` | When the device received it. | | `appState` | `string` | `"foreground"`, `"background"`, or `"cold"`. | | `data` | `object` | The message's data payload. | | `delivered` | `boolean` (optional) | Whether delivery to a page was attempted. | :::: ::::ref-section{title="Health results"} Returned by `bdk.health.status()` and `bdk.health.aggregate()`. Type ids are `steps`, `distance`, `activeCalories`, `heartRate`, `restingHeartRate`, `weight`, `height`, `bloodOxygen`, `sleep`, and `workouts`. Status from `bdk.health.status()`: | Property | Type | Description | | --- | --- | --- | | `available` | `boolean` | Whether health data is available on this device. | | `platform` | `string` | `"ios"` or `"android"`. | | `types` | `object` | Map of type id → `{ status, requested }`. | | `types[id].status` | `string` | `"granted"`, `"notGranted"`, or `"unknown"`. | | `types[id].requested` | — | Whether this type has been requested. | | `reason` | `string` | Why health is unavailable, when present: `device_unsupported`, `provider_missing`, or `provider_update_required`. | Aggregate from `bdk.health.aggregate()`: | Property | Type | Description | | --- | --- | --- | | `type` | `string` | The health data type id the buckets belong to. | | `unit` | `string` | Aggregate unit. | | `stat` | `string` | `"sum"` or `"avg"`. | | `interval` | `string` | `"total"`, `"hour"`, `"day"`, or `"month"`. | | `buckets` | `array` | Aggregated buckets. Use these instead of summing raw samples. | :::: ::::ref-section{title="NFC tag"} Returned by `bdk.nfc.read()` and `bdk.nfc.getLaunchTag()`, and as `tag` on `nfc.launchTag`. `id` is `null` on iOS and a lowercase hex string on Android. | Property | Type | Description | | --- | --- | --- | | `id` | `string \| null` | Tag id. | | `ndef` | `object` | NDEF message: `writable`, `capacityBytes`, `records`. | | `ndef.records` | `array` | NDEF records. | :::: ::::ref-section{title="Event payloads"} A few events deliver raw native data rather than a fixed object. Here's what each one contains. | Event | Delivers | | --- | --- | | `contacts` | The device address book — an array of contacts, each with a name, phone number(s), and email(s). | | `location` | A `"latitude,longitude"` string, or an object with optional `latitude`, `longitude`, and `data`. | | `barcodeScanned` | The scanned code — its `type` (e.g. `qr`) and decoded `content`. | | `screenshot` | The captured image, as a data URI (`string`). | | `menuClicked` | The menu item the user tapped. | | `popupClosed` | Which button dismissed the popup. | | `datePicked` | The date/time the user selected. | | `purchaseSuccess` / `purchaseFailed` / `receiptReceived` | `{ platform, data }` — the store's raw purchase details; verify server-side. | | `permissions.changed` | `{ changed, previous, permissions }` — `permissions` is a map of permission entries. Fires when a tracked permission changes, not on the first snapshot. | | `att.changed` | `{ status, idfa }` — `status` is `granted`, `denied`, `restricted`, or `notDetermined`. | | `auth.completed` | `{ mode, provider, handoff }`. | | `auth.cancelled` | `{ mode, provider, reason }` — `mode` is `auto` or `explicit`; `reason` is `dismissed`, `provider_denied`, or `timeout`. | | `push.dataReceived` | `{ id, receivedAt, appState, data }` — `appState` is `foreground`, `background`, or `cold`. | | `share.uploadProgress` | `{ shareId, itemIndex, bytesSent, totalBytes, progress }`. | | `nfc.launchTag` | `{ url, tag }` — `tag` is an NFC tag. | | `iap.transactionUpdated` | `{ reason, transaction }` — `reason` is `renewal`, `revoked`, or `external`. | :::: --- # Use with AI agents URL: https://docs.thebdk.com/reference/ai-agents ::::ref-section{title="Built for AI agents"} Point your agent at the machine-readable docs: [llms.txt](https://docs.thebdk.com/llms.txt) is the page index, and [llms-full.txt](https://docs.thebdk.com/llms-full.txt) is the full-text dump. Load those first, then use the starter prompt below. :::: ::::ref-section{title="Prime your agent"} Drop this into your agent's context before you ask it to build a native feature. #code :::code-group ```text [Starter prompt] You're building with @bdk/native (docs: https://docs.thebdk.com). Install: npm install @bdk/native Browser: import { createBdkNative } from "@bdk/native/browser" const bdk = createBdkNative(); Rules: - Use the typed namespaced helpers: bdk.media.*, bdk.ui.*, bdk.navigation.*, bdk.iap.*, bdk.auth.*, bdk.location.*, bdk.device.*, bdk.share.*, bdk.permissions.*. - Results arrive on an event — subscribe with bdk.on(event, cb) BEFORE you call. - To detect the native app: const info = await bdk.ready(3000). Never branch on bdk.isNative() at page load — it reads false until device info arrives. - bdk.environment() is safe synchronously: "native" is reliable immediately on current app builds; on "unknown" fall back to await bdk.ready(3000). - Outside the app a call resolves with triggered: false; branch on !result.triggered to fall back to the web. - Server features: import from @bdk/native/server/* in backend code only, and read credentials from env — never hardcode keys. Full machine-readable reference: https://docs.thebdk.com/llms-full.txt ``` ::: :::: ::::ref-section{title="The patterns to follow"} A handful of rules keep generated code correct. - **Subscribe before you call.** Interactive features (camera, pickers, biometrics, purchases) deliver their result on an event, not the returned promise. - **`await bdk.ready(...)` before branching on `bdk.isNative()`.** Device info arrives shortly after load, so a page-load `isNative()` check takes the web path even inside the app. - **`bdk.environment()` for an instant answer.** `"native"` is trustworthy at first tick on current app builds; treat `"unknown"` as "wait" and use `ready()`. UX only — never auth. - **Branch on `!result.triggered`.** Outside the native app a call doesn't run — handle that path so your web build keeps working. - **Keep server code on the server.** `@bdk/native/server/*` runs on your backend with your credentials; never import it into a browser bundle. #code :::code-group ```ts [Correct usage] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // Subscribe first… bdk.on("photoCaptured", (photo) => console.log(photo.fileUrl)); // …then call. Outside the app, fall back to the web. const result = await bdk.media.capturePhoto(); if (!result.triggered) openWebFilePicker(); ``` ::: :::: ::::ref-section{title="Keys stay in the environment"} Agents copy your examples closely, so every example here reads credentials from the environment. Follow the same rule: pass keys from env (or a secrets manager), never in client code or source. ::callout{type="warn"} Server helpers read their credentials from arguments or environment variables. Don't hardcode API keys, and never ship a server import to the browser. :: #code :::code-group ```ts [Server, keys from env] import { sendPushNotification } from "@bdk/native/server/onesignal"; await sendPushNotification({ message: "Your order shipped", playerIds: [playerId], oneSignalAppId: process.env.ONESIGNAL_APP_ID, oneSignalApiKey: process.env.ONESIGNAL_API_KEY }); ``` ::: ::::