Use this file to discover all available pages before exploring further.
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.
The web app calls native code through a single global function published by despia-native:
import despia from 'despia-native'// Fire and forget. No response expected.despia('haptic://?style=light')// Await a single response variable. Resolves within 30 seconds or with null on failure.const data = await despia('battery://', ['battery_level'])const level = data.battery_level// Fire and listen. Define a window-scoped listener for an async push.window.on_export_done = function(data) { saveTo(data.url) }despia('export://?format=pdf')
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.
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.
Pattern
Use when
Web app call
Native call
Variable
Bounded native work. You can guarantee completion in under 30 seconds.
const data = await despia('scheme://', ['var'])
despia.variable("var", value)
Fire and listen
Single response with no upper time bound. Anything waiting on the user, the network without timeout, or any unbounded operation.
despia('scheme://') then define window.<callback> = function(data) {...}
despia.event("callback_name", payload)
Stream / event
Multiple pushes over time. Observers, push notifications, in-progress scan results.
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.
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.
const data = await despia('battery://', ['battery_level'])data.battery_level // 87 | null on failure
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:
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
// Always define the listener BEFORE firing the action.window.on_prompt_done = function(data) { if (data.cancelled) return console.log('User entered:', data.value)}despia('alert://prompt?message=What is your name?&placeholder=Jane')
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.
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.
// Called every time a new device appears.window.on_device_discovered = function(device) { appendDevice(device.name, device.rssi)}// The final result still goes through the variable channel.const data = await despia('ble://?duration=5', ['ble_result'])
// Inside the BLE delegate, called every discovery.despia.event("on_device_discovered", ["name": name, "rssi": rssi, "uuid": uuid])
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.
Use this decision flow for every action you build.
Does the action wait on the user (prompt, picker, biometric, OAuth, share sheet)? YES → fire and listen NO → next questionCan the action take longer than 30 seconds (large upload, video processing, ML)? YES → fire and listen NO → next questionDoes the action push multiple values over time (observer, scan stream)? YES → stream (event), or event + variable for the final result NO → variable
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.
Reads a value configured in the dashboard. Always returns String. Empty string if no value is configured. Defaults declared in the spec apply automatically.
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.
let blob: Data? = despia.files["upload"].datalet blobs: [Data] = despia.files["uploads"].array
val blob: ByteArray? = despia.files["upload"].dataval blobs: Array<ByteArray> = despia.files["uploads"].array
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.
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.
// Fire and listen: one push when work completesdespia.event("on_export_done", ["url": fileURL])// Stream: many pushes during a scandespia.event("on_device_discovered", ["uuid": uuid, "rssi": rssi])
despia.event("on_export_done", mapOf("url" to fileURL))despia.event("on_device_discovered", mapOf("uuid" to uuid, "rssi" to rssi))
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.
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.
import android.app.AlertDialogimport android.text.InputTypeimport android.widget.EditTextimport android.os.Handlerimport android.os.Looperdespia.action("prompt") { val message = despia.params["message"] val placeholder = despia.params["placeholder"] val initial = despia.params["default"] Handler(Looper.getMainLooper()).post { val input = EditText(activity).apply { inputType = InputType.TYPE_CLASS_TEXT hint = placeholder setText(initial) } AlertDialog.Builder(activity) .setMessage(message) .setView(input) .setPositiveButton("OK") { _, _ -> despia.event("on_prompt_done", mapOf( "cancelled" to false, "value" to input.text.toString() )) } .setNegativeButton("Cancel") { _, _ -> despia.event("on_prompt_done", mapOf( "cancelled" to true, "value" to "" )) } .setOnCancelListener { despia.event("on_prompt_done", mapOf( "cancelled" to true, "value" to "" )) } .show() }}
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.
import despia from 'despia-native'// Define the listener BEFORE firing. The native side may complete before// the JS event loop runs again, and despia.event is a no-op without a listener.window.on_prompt_done = function(result) { if (result.cancelled) { console.log('User cancelled') return } console.log('User entered:', result.value)}function askName() { despia('alert://prompt' + '?message=' + encodeURIComponent('What is your name?') + '&placeholder=' + encodeURIComponent('Jane'))}
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.
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.
import despia from 'despia-native'// Live stream of devices as they appear during the scan.window.on_device_discovered = function(device) { appendDevice(device.name, device.rssi)}// Wait for the final result. Resolves within 30 seconds (or sooner).const data = await despia('ble://?duration=5', ['ble_result'])if (data.ble_result === null) { console.log('BLE unavailable on this device')} else { console.log(`Found ${data.ble_result.count} devices`)}
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.
The runtime decodes URL params into typed values before invoking the closure.
Spec type
Swift
Kotlin
Accepts
String
despia.params.<key> returns String
despia.params["<key>"]
Any value. Empty string if missing.
Int
despia.params.<key> as Int?
despia.params.int("<key>")
"5", "5.0" (truncated), negatives.
Double
despia.params.<key> as Double?
despia.params.double("<key>")
"3.14", "3,14" (locale-tolerant).
Bool
despia.params.<key> as Bool?
despia.params.bool("<key>")
true/1/yes/on, false/0/no/off. Case-insensitive.
JSON
despia.params.<key> as? [String: Any]
despia.params.json("<key>")
URL-encoded JSON, double-encoded JSON, bare object literals.
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.
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.