Skip to main content
Reference for the Despia Extension SDK. An Extension exposes native iOS and Android functionality to the web app through the standard despia() call, the same call exposed by the despia-native NPM package.
Extensions are currently reserved for Despia employees and Despia Whitelabel resellers. Public availability is not yet open. If you are a partner and need access, contact support@despia.com.

How despia() works

The web app calls native code through a single global function published by despia-native:
Under the hood, the call assigns the URL string to window.despia. The native runtime intercepts the assignment, dispatches to the matching handler, and pushes results back to the WebView. There are two return channels: window.<varName> for awaited responses (read by despia-native via the variable observer with a 30-second timeout), and direct calls to window.<callback>(payload) for fire-and-listen and streaming patterns. This is the contract every Extension implements. You expose a URL scheme, you write to a variable or call a window function, and the web app reads the response. You do not implement any JavaScript-side bridge.

Three response patterns

There are exactly three ways an Extension can communicate back to the web app. The choice is determined by whether the operation has a bounded duration, not by how complex the response is.
Anything that waits for user input must use fire-and-listen, never variable. Alert prompts, picker dialogs, native photo pickers, file pickers, biometric confirmation sheets, and OAuth login flows all wait on the user, who has no SLA. Treating these as awaited variables guarantees a 30-second hang and a undefined resolve when the user reads the dialog slowly.

Variable

For deterministic native work where you can guarantee a result inside 30 seconds. Reading device sensors (battery, geolocation), reading SDK state (HealthKit step count, active RevenueCat entitlements, contacts), running a cryptographic operation, hitting your own backend with a timeout. The web app awaits one line and gets a value or null.
If you cannot honestly say “this finishes in under 30 seconds, every time, on every device”, do not use a variable. Use fire-and-listen.

Fire and listen

For any operation with no enforceable upper time bound. The web app fires the action and registers a window.<callback> listener. The native side calls the listener when work finishes, however long that takes. This is the right pattern for:
  • User input: alert prompts, picker dialogs, photo / file pickers, biometric prompts, OAuth login windows, share sheets
  • Long-running native work: PDF export, video transcoding, large file uploads, ML inference
  • Anything that calls into another app: deep-linking out to settings or the camera, returning later
The naming convention is to prefix the callback with on_ (e.g. on_prompt_done, on_purchase_complete, on_export_finished). The despia.event call is a no-op if the listener isn’t defined, which is why listeners must be registered before firing.

Stream / event

Same mechanism as fire-and-listen, but the native side calls the listener repeatedly. Use for live BLE scan results, push notification deliveries, customer-info change observers, and similar genuine streams.
Variable and event channels combine freely. A single action can stream live updates via event while still resolving an awaited final-result variable, as long as the final result is bounded.

Picking the right pattern

Use this decision flow for every action you build.
When in doubt, prefer fire-and-listen. The cost is one extra window.<callback> definition. The cost of mis-using a variable for an unbounded operation is a 30-second hang for every user every time.

Folder layout

Folder name must be despia-extension-<scheme> matching the scheme field in the spec.

Delivery

Two delivery paths. Both produce identical builds.

API reference: despia global

The Extension SDK exposes a single global called despia in both Swift and Kotlin source files. It is implicit, no import needed. The full surface:

despia.action(name, closure)

Registers a handler for despia('<scheme>://<name>?...'). Closures must register at file scope.

despia.hydration(closure)

Runs once per WebView page load. Use for SDK initialisation. Make it idempotent.

despia.params.<key>

Reads a typed URL param. The type is determined by the spec’s declaration for that param.

despia.env.<key>

Reads a value configured in the dashboard. Always returns String. Empty string if no value is configured. Defaults declared in the spec apply automatically.

despia.files["<key>"].data and .array

Reads binary data uploaded via window.native.set_file() from the web app. The web app sends a @file/<uuid> token in the URL; the runtime resolves it to native bytes before your closure runs.
UUIDs are single-use. Reading once consumes them.

despia.variable(name, value)

Sets window.<name> = value. This is the primary response channel. The web app reads it with await despia('scheme://...', ['name']). Failure semantics: write null to signal failure. despia-native resolves the awaited promise with null for that key, which is the standard “I tried and it didn’t work” signal across all extensions.
Values must be JSON-serialisable. Strings, numbers, booleans, arrays, and string-keyed maps. No Date (use ISO strings), no URL (use String).

despia.event(callbackName, payload)

Calls window.<callbackName>(payload) if defined. Use this for the fire-and-listen pattern (single async response that may exceed 30 seconds) and for streams (multiple pushes over time). No-op if the web app hasn’t defined the callback.

Variable extension: battery level

The smallest possible extension. One action, one variable. Reads battery level. Resolves immediately.

despia-extension.json

Sources/ios/battery.swift

Sources/android/battery.kt

Web app

That is the entire pattern. Three files, twenty lines each, one line of caller code.

Fire-and-listen extension: alert prompt

The canonical user-input case. The web app shows a native text-input prompt and waits for the user to type. Because the user has no SLA, this must be fire-and-listen, not variable. A user reading the dialog for 31 seconds would otherwise hang the call.

despia-extension.json

returns is null because the action does not write a variable. The completion is delivered through the on_prompt_done event when (or if) the user dismisses the dialog.

Sources/ios/alert.swift

Sources/android/alert.kt

Notice both implementations always emit on_prompt_done exactly once, on every exit path: OK, Cancel, and (Android) tap-outside-to-dismiss. Failing to emit on any path leaves the listener hanging forever.

Web app

The reason this can never be a variable is simple: the user might take 5 seconds, 5 minutes, or 5 hours. There is no upper bound. The 30-second variable timeout is for deterministic native operations only.

Stream extension: BLE scanner

Combines the variable channel (final scan result, bounded by the duration param) with the event channel (live discoveries during the scan). Also demonstrates typed params, dashboard config, and capabilities.

despia-extension.json

Sources/ios/ble.swift

Sources/android/ble.kt

Web app


despia-extension.json reference

Complete schema. Every field, every type.

Top-level

dependencies

iOS entry: package (SPM git URL), version (semver), products (SPM product names). Android entry: artifact (Maven group:name:version). System frameworks need no entry.

vars

hosts

Required-param enforcement: if a required param is missing, the closure never runs. The runtime writes { error: "Missing required param: <name>" } to returns.varName if defined.

events

autoInject

capabilities

iOS entry: Android entry:

Param decoding

The runtime decodes URL params into typed values before invoking the closure. Pre-decoding normalisation: trims whitespace and zero-width unicode (U+200B, U+FEFF, U+00A0); decodes + as space unless the value looks like JSON; applies percent-decoding once, then again if still encoded.

File bridge

For binary data or JSON over 8KB. The web app stores data in native memory, sends a UUID through the URL.
Native side reads the resolved bytes:
UUIDs are single-use. The runtime auto-evicts them after the first read.

Reserved schemes

Owned by the runtime. Build fails if your scheme matches:

Behaviour rules

These describe how the runtime and the despia-native SDK behave. Build extensions to match. Variables are for bounded operations only. A variable response is only correct when you can guarantee completion in under 30 seconds on every device. Anything that waits for the user (alert prompts, picker dialogs, biometric confirmation sheets, OAuth windows, file pickers, share sheets) must use fire-and-listen via despia.event. Anything that waits on a network call without a strict timeout, or any potentially long native work (PDF export, video transcode, large uploads, ML inference), must also use fire-and-listen. The 30-second timeout is for deterministic operations like reading sensors, reading SDK state, or running a local cryptographic operation. The 30-second window. despia-native waits 30 seconds for awaited variables. If your action takes longer, the web side times out and resolves with undefined. For longer or unbounded work, use fire-and-listen instead. null means failure. When despia-native sees window.<varName> === null, it resolves immediately with null for that key. Use this as the universal failure signal across all variable returns. For event callbacks, include an error field in the payload instead. Pre-clearing. Before each await despia('scheme://', ['var']) call, despia-native deletes window[var] to avoid stale resolves. Your action must always set the variable, even on failure (set it to null). Otherwise the call hangs until timeout. Multi-variable awaits. await despia('scheme://', ['a', 'b', 'c']) waits for all three. Each must be defined and non-null and non-"n/a". If any one stays unset, the call resolves to {} after 5 minutes. Set every promised variable in your action. Listeners must be defined before firing. For fire-and-listen, register window.<callback> before calling despia(...). The native side may complete before the JS event loop runs again, and despia.event is a no-op if the listener isn’t defined. Closures register at file scope. despia.action and despia.hydration calls go at the top level of the file, not inside functions or classes. The runtime auto-discovers them on file load. State lives in singletons. Use static let shared (Swift) or private object (Kotlin) for state across calls. Files stay loaded for the WebView lifetime. Hydration is idempotent. It runs on every page load including SPA route reloads. Check if SDK.isConfigured before re-initialising. Payloads are JSON-only. Strings, numbers, booleans, arrays, string-keyed maps. No Date (use ISO strings), no URL (use String), no custom classes. The web app never imports anything besides despia-native. No script tags, no extension-specific packages. The single import gives access to every extension.

What the build system handles


Resources

despia-native on NPM

Web-side SDK

Partner support