# 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
});
```
:::
::::