Plugin API
Import from @otakit/capacitor-updater. The default flow usually only needs notifyAppReady(). The other public methods exist for advanced manual update flows where your app decides when to check, download, and apply an update.
import { OtaKit } from "@otakit/capacitor-updater";Configuration
Hosted OtaKit keeps the config small. The default automatic behavior is runtime catch-up on cold start, staged activation on later cold starts, and background checks on resume.
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
// Optional:
// channel: "production",
// runtimeVersion: "2026.04",
// launchPolicy: "apply-staged",
// resumePolicy: "shadow",
// runtimePolicy: "immediate",
// checkInterval: 600000,
}
}Hosted OtaKit points at the managed CDN and ingest service automatically. Do not set cdnUrl, ingestUrl, or manifestKeys unless you intentionally want custom hosting or verification behavior.
Policies
Policies are the only automatic behaviors. The same policy logic runs no matter which event triggered it. launchPolicy, resumePolicy, and runtimePolicy only decide when to invoke that policy.
The hosted defaults are usually the right production baseline: runtimePolicy: "immediate", launchPolicy: "apply-staged", and resumePolicy: "shadow".
Compatibility lanes
channel is for rollout audience. runtimeVersion is for native compatibility.
If you ship a new store build and do not want it to keep consuming older OTA bundles, bump runtimeVersion in the plugin config before uploading the next OTA bundle.
The CLI reads that same value automatically during upload, so releases stay inside the correct runtime lane without extra flags.
When runtimeVersion changes, or when the app resolves a runtime lane for the first time, OtaKit runs runtimePolicy on cold start. By default that policy is immediate, so fresh installs and new native shells catch up before that lane is marked resolved.
Automatic Flow (Default)
The hosted default is equivalent to this configuration:
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
runtimePolicy: "immediate",
launchPolicy: "apply-staged",
resumePolicy: "shadow",
appReadyTimeout: 10000,
}
}In this mode, your app code usually only needs to call notifyAppReady().
Resume throttling is intentionally narrow: checkInterval only affects resume background checks. Cold-start runtime handling, cold-start launch handling, and all manual APIs always act immediately.
Set checkInterval to 0 or a negative value if you want resume checks to run every time instead of being throttled.
Confirm the current bundle is working. Call this once when your app has fully loaded. If it is not called within appReadyTimeout, the plugin rolls back.
import { OtaKit } from "@otakit/capacitor-updater";
await OtaKit.notifyAppReady();Manual Flow (Advanced)
If you want no automatic checks at all, turn every automatic policy off and drive the update lifecycle yourself.
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
launchPolicy: "off",
resumePolicy: "off",
runtimePolicy: "off",
}
}Inspect the current updater state: current bundle, fallback bundle, staged bundle, and builtin version.
Check the configured channel for a newer bundle without downloading it. Returns no_update, already_staged, or update_available.
Ensure the latest bundle is staged for later activation. Returns no_update or staged.
Convenience helper for manual flows. It runs the same native immediate-flow operation used by automatic immediate policies. Terminal operation: if an update is applied, it does not resolve back into the old JS context.
Activate the currently staged bundle and reload the WebView. Terminal operation: on success it does not resolve back into the old JS context.
Still required after the updated bundle launches. Call this once when your app has fully loaded so the plugin can mark the new bundle healthy.
Returns information about the most recent failed update (rollback). Useful for diagnostics and crash reporting. Returns null if no failure has occurred.
There is no listener API in the current plugin surface. If you want custom update UI, use check(), getState(), download(), and apply() directly.
After a successful apply() or an update() that installs a new bundle, the app reloads immediately. Call notifyAppReady() from the reloaded app startup, not in the same JS flow that triggered the activation.
The simplest manual pattern is: check for updates, show your own prompt, then call update() if the user accepts.
const latest = await OtaKit.check();
if (latest.kind === "no_update") {
return;
}
const accepted = window.confirm("Update available. Install now?");
if (accepted) {
await OtaKit.update();
}If you want a split flow, download first and apply later:
const state = await OtaKit.getState();
if (state.staged) {
await OtaKit.apply();
return;
}
const latest = await OtaKit.check();
if (latest.kind === "no_update") {
return;
}
const accepted = window.confirm("Update available. Download now?");
if (!accepted) {
return;
}
const result = await OtaKit.download();
if (result.kind === "no_update") {
return;
}
// Later, after another user action:
await OtaKit.apply();Advanced Overrides
Use these only when you run a custom server or need custom verification behavior.
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
// Optional advanced overrides
// cdnUrl: "https://cdn.your-domain.com",
// ingestUrl: "https://ingest.your-domain.com/v1",
// serverUrl: "https://your-domain.com/api/v1",
// allowInsecureUrls: false,
// manifestKeys: [
// { kid: "key-2026-01", key: "MFkwEwYH..." }
// ]
}
}Types
type OtaKitPolicy = "off" | "shadow" | "apply-staged" | "immediate";
interface BundleInfo {
id: string;
version: string;
runtimeVersion?: string;
status: "builtin" | "pending" | "trial" | "success" | "error";
downloadedAt?: string;
sha256?: string;
channel?: string;
releaseId?: string;
}
interface OtaKitState {
current: BundleInfo;
fallback: BundleInfo;
staged: BundleInfo | null;
builtinVersion: string;
}
interface LatestVersion {
version: string;
runtimeVersion?: string;
url: string;
sha256: string;
size: number;
releaseId: string;
}
type CheckResult =
| { kind: "no_update" }
| { kind: "already_staged"; latest: LatestVersion }
| { kind: "update_available"; latest: LatestVersion };
type DownloadResult =
| { kind: "no_update" }
| { kind: "staged"; bundle: BundleInfo };