Capacitor Plugin API Reference

Every method, event, type, and error code of the DeployYourApp Capacitor plugin.


Complete API reference for @deploy-your-app/capacitor-update-manager. For the concepts behind these calls, see Update lifecycle.

import { DeployYourApp, DYA_EVENTS } from '@deploy-your-app/capacitor-update-manager';

Apps that cannot resolve that bare import from their web sources — the plugin lives in src-capacitor/node_modules, which the web bundler does not see — use Capacitor's own seam instead, and get the identical API:

import { registerPlugin } from '@capacitor/core';
const DeployYourApp = registerPlugin('DeployYourApp');

Migrating to 0.5.0

0.5.0 closes two contract defects that only affected registerPlugin() consumers. Both were in the native layer, so a store release is required — an OTA update cannot fix an app already affected. An affected app is stuck in a rollback loop, and the very bundle that would carry the fix is rolled back before it can run.

  1. notifyAppReady() no longer requires a token you had no documented way to send. Since 0.4.2 native refused any call without a readiness token, and only the package's JavaScript wrapper knew to inject one — so registerPlugin() consumers had every OTA bundle roll back at appReadyTimeout, forever, while the promise resolved successfully and nothing was logged app-side. Native now derives the token from the URL of the page it served. No code change on your side, beyond upgrading and shipping a store build. If your router strips the launch query, see notifyAppReady.

  2. getNextBundle() now resolves { bundle } instead of the bundle itself. This is the one breaking change. Native used to resolve {} for "nothing staged", which is truthy and indistinguishable from a real bundle; only the wrapper normalized it to null.

    - const next = await DeployYourApp.getNextBundle();
    - if (next) showUpdateReady(next.version);
    + const { bundle } = await DeployYourApp.getNextBundle();
    + if (bundle) showUpdateReady(bundle.version);
    

    This applies to importers of the package too: the wrapper no longer rewrites the result, because whatever it had to repair was exactly what a registerPlugin() consumer was left holding.

  3. A refused notifyAppReady() is now observable. It rejects with READY_TOKEN_MISMATCH and emits appReadyRejected, where before it called resolve() and left the rollback unexplained. If you call notifyAppReady() without a catch, add one.

Update methods

checkForUpdate(options?)

Asks the update server whether a newer bundle exists for this device. Emits updateAvailable or noUpdateAvailable. Before the network call, any pending bundle whose delayUntil has passed is staged for the next launch.

const result = await DeployYourApp.checkForUpdate();
const beta = await DeployYourApp.checkForUpdate({ channel: 'beta' });
Parameter Type Default Description
options.channel string configured channel Channel to check for this call only. Does not change the stored channel.

Returns Promise<UpdateCheckResult>.

download(options)

Downloads a bundle ZIP, verifies its SHA-256 checksum and its RSA signature, then extracts it to device storage under the bundle asset policy. Emits downloadProgress while running, then downloadComplete or downloadFailed.

Signature verification is mandatory. There is no unsigned install path: with no publicKey configured, or no signature supplied, the call rejects with SIGNATURE_REQUIRED before anything is stored.

const bundle = await DeployYourApp.download({
  url: result.url,
  version: result.version,
  checksum: result.checksum,
  signature: result.signature,
});
Parameter Type Default Description
options.url string required Bundle download URL.
options.version string required Version string. Must match ^[0-9A-Za-z.\-+]{1,64}$ — it becomes the bundle's directory name.
options.checksum string required Expected SHA-256 hex digest of the downloaded bytes.
options.signature string required in practice Base64 RSA-SHA256 signature over the downloaded bytes. Omitting it rejects with SIGNATURE_REQUIRED.

Returns Promise<DownloadResult>.

Rejects with DOWNLOAD_FAILED when another download is already running, or when version equals the currently served bundle. Rejects with EXTRACTION_FAILED when the archive contains no index.html at its root or in a single top-level folder, and with ASSET_TYPE_REJECTED, MANIFEST_MISSING, or MANIFEST_MISMATCH when the archive violates the asset policy.

Bundle asset policy

The updater is structurally incapable of installing anything but web assets. Enforcement lives in the native extractor on both platforms and applies to every bundle, however it was produced. This is the primary answer to App Store Guideline 2.5.2 — see App Store review.

Extension allowlist. Every file entry must end in one of:

html htm css js mjs cjs json map webmanifest
svg png jpg jpeg gif webp avif ico bmp
woff woff2 ttf otf eot
mp3 mp4 webm ogg wav m4a
txt xml csv md
wasm

Matching is case-insensitive and only the final extension counts, so bundle.js.map passes and evil.js.dylib does not. A file with no extension is rejected. Directory entries are exempt — they have no extension, and requiring one would reject every nested folder.

Symlink entries differ by platform. On iOS they are skipped and never written to disk. On Android they cannot be identified at all: a ZIP records symlink-ness in the central directory's external attributes, and java.util.zip.ZipEntry exposes no accessor for it (isDirectory() merely tests for a trailing /), so reading it would require a third-party ZIP implementation. Such an entry is extracted as an ordinary file whose content is the link target path — never as a link, so it cannot redirect a later write out of the bundle directory. It must then pass the extension allowlist and the manifest hash check like any other file, and a manifest generated from a real build tree does not list it, so it is rejected with MANIFEST_MISMATCH rather than installed.

Executable magic-byte rejection. The first four bytes of every extracted file are matched against this table regardless of the file's name, so a native binary renamed to app.js is still refused:

Leading bytes Format
FE ED FA CE Mach-O
FE ED FA CF Mach-O
CE FA ED FE Mach-O
CF FA ED FE Mach-O
CA FE BA BE Mach-O fat binary or Java class
7F 45 4C 46 ELF
64 65 78 0A Android DEX
4D 5A PE executable

Per-file manifest. Every bundle carries a .dya-manifest.json at its archive root, written by dya deploy:

{
  "manifestVersion": 1,
  "bundleVersion": "1.4.0",
  "createdAt": "2026-08-04T00:00:00.000Z",
  "files": {
    "index.html": "e3b0c442...",
    "assets/app.js": "9f86d081..."
  }
}

The comparison is bidirectional: every manifest entry must exist on disk with the declared SHA-256, and every extracted file must appear in the manifest. A one-way check would let an attacker add a file (forward check only) or remove one (reverse check only). .dya-manifest.json itself is the single entry exempt from appearing in files — it cannot list its own hash.

manifestVersion is validated: anything other than 1 is rejected with MANIFEST_MISSING, naming the version found and the version expected. A manifest this app cannot interpret is treated as a manifest it does not have, which is why it shares that code rather than getting one of its own. The check runs before files is read, since a future schema version is free to change that key's shape.

A bundle produced by an older CLI has no manifest and is rejected with MANIFEST_MISSING. There is no fallback path.

apply(options)

Activates a downloaded bundle immediately: the webview is pointed at the new bundle and reloaded. Starts the app-ready timer, so the new bundle must call notifyAppReady() within appReadyTimeout or the plugin rolls back. Emits updateApplied or updateFailed.

await DeployYourApp.apply({ id: bundle.id });
Parameter Type Default Description
options.id string required Bundle identifier from download() or listBundles().

Returns Promise<void>. Rejects with BUNDLE_NOT_FOUND when the bundle is not on disk.

notifyAppReady(options?)

Confirms the current bundle booted. Cancels the rollback timer, marks the bundle active, clears the launch-attempt counter, and deletes the previous bundle when autoDeletePrevious is enabled. Emits appReady.

await DeployYourApp.notifyAppReady();

Returns Promise<void>. Safe to call on every launch.

Only the document actually running the bundle on trial may confirm it — otherwise a page still running an older bundle could confirm a newly activated one, marking a broken bundle active and leaving it with no route back. The plugin proves this itself: it puts a readiness token in the URL of the page it serves and reads that token back natively when you call. You pass nothing, whether you import this package or reach the plugin through registerPlugin('DeployYourApp').

Option Type Description
token string Readiness token, for callers whose router rewrote the URL before this could be called

Rejects with READY_TOKEN_MISMATCH — and emits appReadyRejected — when that proof is missing. The only way to hit it in otherwise-correct code is a history-mode SPA whose router strips the launch query before your boot code runs. Capture the token first thing and hand it back:

// module scope, before the router mounts
const token = new URL(location.href).searchParams.get('__dya_ready');

// after the app has painted
await DeployYourApp.notifyAppReady(token ? { token } : undefined);

Importing DeployYourApp from the package does that capture for you; a registerPlugin() consumer in a history-mode app must do it itself.

Before 0.5.0 this rejection path called resolve(). The promise succeeded, the app's error handling never fired, and the bundle rolled back ~10s later with no app-side signal. See the migration note.

reset()

Deletes every downloaded bundle and its metadata, then serves the built-in bundle shipped in the app binary.

await DeployYourApp.reset();

Returns Promise<void>.

reload()

Reloads the webview without changing which bundle is served.

await DeployYourApp.reload();

Returns Promise<void>.

Bundle methods

getCurrentBundle()

Metadata for the bundle being served. Returns { id: 'builtin', version: '0.0.0', status: 'builtin' } when the built-in bundle is active.

const bundle = await DeployYourApp.getCurrentBundle();

Returns Promise<BundleInfo>.

getNextBundle()

The bundle staged to activate on the next launch.

const { bundle } = await DeployYourApp.getNextBundle();
if (bundle) showUpdateReady(bundle.version);

Returns Promise<{ bundle: BundleInfo | null }> — an envelope, not the bundle itself, because a Capacitor bridge cannot resolve a bare null.

Changed in 0.5.0. This used to be declared Promise<BundleInfo | null>, and native resolved {} when nothing was staged. Only the package's JavaScript wrapper turned that {} into null, so a registerPlugin() consumer got a truthy empty object: the app believed an update was permanently staged, showed it with an undefined version, and the install button no-opped. See the migration note.

listBundles()

Every downloaded bundle still on the device. The built-in bundle is not included.

const { bundles } = await DeployYourApp.listBundles();

Returns Promise<{ bundles: BundleInfo[] }>.

deleteBundle(options)

Removes a bundle's files and metadata. Deleting the bundle currently being served reverts the webview to the built-in bundle.

await DeployYourApp.deleteBundle({ id: '1.2.3' });
Parameter Type Default Description
options.id string required Bundle identifier to delete.

Returns Promise<void>.

Channel and device methods

getChannel()

const { channel } = await DeployYourApp.getChannel();

Returns Promise<{ channel: string }>.

setChannel(options)

Switches the update channel and persists it to device storage, so it survives restarts and overrides the channel value in the Capacitor config. Used by the next update check.

await DeployYourApp.setChannel({ channel: 'beta' });
Parameter Type Default Description
options.channel string required Channel name. Not validated on-device; an unknown name falls back to the app's default channel server-side.

Returns Promise<void>.

getDeviceId()

The device UUID the plugin generated on first use and stored locally. It is not derived from any hardware or advertising identifier.

const { deviceId } = await DeployYourApp.getDeviceId();

Returns Promise<{ deviceId: string }>.

setCustomId(options)

Stores a custom identifier for this device. It is included in the update-check request body.

await DeployYourApp.setCustomId({ customId: 'employee-4521' });
Parameter Type Default Description
options.customId string required Your identifier for this device.

Returns Promise<void>. The current DeployYourApp server validates the update-check body against a schema that does not include customId, so the value is discarded rather than stored.

Version methods

getNativeVersion()

The native app version — CFBundleShortVersionString on iOS, versionName on Android.

const { version } = await DeployYourApp.getNativeVersion();

Returns Promise<{ version: string }>.

getPluginVersion()

const { version } = await DeployYourApp.getPluginVersion();

Returns Promise<{ version: string }>.

setVersionOverride(options)

Reports a different bundle version to the update server on subsequent checks, without touching the bundle on disk. Persisted across restarts.

await DeployYourApp.setVersionOverride({ version: '0.0.1' });
await DeployYourApp.setVersionOverride({ version: '' }); // clear
Parameter Type Default Description
options.version string required Version to report. An empty string clears the override.

Returns Promise<void>.

getVersionOverride()

const { version } = await DeployYourApp.getVersionOverride();

Returns Promise<{ version: string }> — an empty string when no override is set.

Analytics methods

Events are buffered on device and posted to {statsUrl}/api/stats. A flush happens when analyticsBatchSize events are buffered or every analyticsFlushInterval seconds, whichever comes first. The buffer holds at most 500 events; the oldest are dropped first. A failed batch is retried three times with 1s / 2s / 4s backoff, then pushed back into the buffer for the next cycle. A 4xx response is not retried.

Nothing is sent when analyticsEnabled is false.

trackEvent(options)

await DeployYourApp.trackEvent({
  name: 'checkout_completed',
  properties: { plan: 'scale', total: 29 },
});
Parameter Type Default Description
options.name string required Event name, sent as eventType. The server rejects names longer than 100 characters.
options.properties object {} Arbitrary JSON, sent as eventData and stored verbatim.

Returns Promise<void>.

trackPageView(options)

Records an event with eventType: 'page_view'.

await DeployYourApp.trackPageView({ path: '/settings', title: 'Settings' });
Parameter Type Default Description
options.path string required Page path.
options.title string unset Page title.

Returns Promise<void>.

trackError(options)

Records an event with eventType: 'error'.

await DeployYourApp.trackError({ message: err.message, stack: err.stack, fatal: false });
Parameter Type Default Description
options.message string required Error message.
options.stack string unset Stack trace.
options.fatal boolean false Whether the error was fatal.

Returns Promise<void>.

flushAnalytics()

Sends the buffered events now. Resolves even when the send fails — failed events stay buffered for the next cycle.

await DeployYourApp.flushAnalytics();

Returns Promise<void>.

Events the plugin records itself

With analytics enabled, the plugin writes its own update lifecycle events into the same buffer. They are stored as structured device update events on the server.

eventType eventData
update_check
update_available version, bundleId
update_download_start version
update_download_complete version, duration_ms, bytes
update_download_fail version, reason
update_verify_pass version
update_verify_fail version, reason (checksum_mismatch or signature_invalid)
update_apply version, and applyMode when staged from a pending bundle
update_app_ready duration_ms since the apply, when known
update_rollback from, to, reason

Events

Listen with the standard Capacitor listener API. Name constants are exported as DYA_EVENTS.

const handle = await DeployYourApp.addListener(DYA_EVENTS.DOWNLOAD_PROGRESS, (progress) => {
  console.log(`Download: ${progress.percent}%`);
});

await handle.remove();
Event DYA_EVENTS key Payload Fired when
downloadProgress DOWNLOAD_PROGRESS { percent, bytesDownloaded, totalBytes } During a download. Throttled to whole-percent changes plus a final event. totalBytes is 0 when the server sends no content length, and percent stays 0 in that case.
updateAvailable UPDATE_AVAILABLE { version, message? } A check found a new version.
noUpdateAvailable NO_UPDATE_AVAILABLE none A check found nothing new.
downloadComplete DOWNLOAD_COMPLETE { id, version } A download finished and the bundle was stored.
downloadFailed DOWNLOAD_FAILED { message } A download failed, or an automatic update cycle threw.
updateApplied UPDATE_APPLIED { id, version } A bundle was activated and the webview repointed.
updateFailed UPDATE_FAILED { message } apply() failed.
rollback ROLLBACK { from, to, reason, attempts? } An automatic rollback ran. reason is appReadyTimeout or crashLoop; attempts is present only for crashLoop. to is builtin when there was no previous bundle.
appReady APP_READY none notifyAppReady() succeeded.
appReadyRejected APP_READY_REJECTED { bundleId, servedBundleId, message } notifyAppReady() was refused because the calling document could not be shown to be running the bundle on trial. The call also rejects with READY_TOKEN_MISMATCH.
appVersionChange APP_VERSION_CHANGE { previousVersion, currentVersion } The native app version changed and resetWhenUpdate reverted to the built-in bundle.

Several of these fire during launch, before your web code can register a listener. Those are retained by Capacitor and delivered to the first listener that attaches: updateAvailable, downloadComplete, downloadFailed, updateApplied, updateFailed, rollback, appReadyRejected, and appVersionChange. The remaining events — downloadProgress, noUpdateAvailable, and appReady — are not retained.

removeAllListeners() removes every listener registered through the plugin.

Types

BundleInfo

Property Type Description
id string Bundle identifier. Equal to the version string for downloaded bundles.
version string Bundle version.
checksum string? SHA-256 checksum recorded at download time.
status string builtin, downloaded, pending, active, or failed.
downloadedAt number? Unix timestamp in milliseconds.
size number? Size in bytes of the downloaded archive.

pending means served or staged but not yet confirmed by notifyAppReady(). active means confirmed. failed means rolled back.

UpdateCheckResult

Property Type Description
available boolean Whether a new bundle is available.
version string? Version of the available bundle.
url string? Download URL. Time-limited when the server issues a presigned URL.
checksum string? Expected SHA-256 checksum.
signature string? Base64 RSA signature. Absent only for bundles stored as unsigned, which the plugin then refuses with SIGNATURE_REQUIRED.
message string? Release notes.
applyMode string? Server-dictated apply mode for this update.
mandatory boolean true forces immediate activation.
delayUntil string? ISO 8601 timestamp before which the update must not be activated.

Every property except available and mandatory is absent when no update is available.

DownloadResult

Property Type Description
id string Identifier of the stored bundle — pass to apply().
version string Version of the stored bundle.

DownloadProgressEvent

Property Type Description
percent number Integer 0-100, or 0 when the total size is unknown.
bytesDownloaded number Bytes received so far.
totalBytes number Total expected bytes, 0 when unknown.

Error codes

Rejected calls carry a stable code property alongside message.

try {
  await DeployYourApp.download(update);
} catch (err) {
  if (err.code === 'CHECKSUM_MISMATCH') {
    // corrupted download — retry
  }
}
Code Meaning
MISSING_PARAMS A required parameter was absent.
NETWORK_ERROR The update server was unreachable or answered with an unexpected status.
PARSE_ERROR The response could not be parsed, or a version string was rejected by ^[0-9A-Za-z.\-+]{1,64}$.
DOWNLOAD_FAILED The download returned a non-2xx status, another download was already running, or the requested version is the active bundle.
CHECKSUM_MISMATCH The SHA-256 digest of the downloaded bytes did not match.
SIGNATURE_REQUIRED publicKey is not configured, or the server sent no signature. Verification never fails open.
SIGNATURE_INVALID RSA signature verification failed.
BUNDLE_NOT_FOUND The referenced bundle is not on the device.
INVALID_URL The download URL could not be parsed.
EXTRACTION_FAILED The archive could not be extracted, contained an unsafe entry, exceeded a size cap, or had no index.html.
ASSET_TYPE_REJECTED A bundle entry is not a web asset: its extension is outside the allowlist, or its leading bytes match a native executable format.
MANIFEST_MISSING The bundle carries no readable .dya-manifest.json, or one whose manifestVersion is not 1. Re-deploy with a current dya CLI.
MANIFEST_MISMATCH The extracted files and the manifest disagree — a wrong hash, a file with no manifest entry, a manifest entry with no file, or a files map that is absent or malformed in a manifest that otherwise parsed.
READY_TOKEN_MISMATCH notifyAppReady() could not be attributed to the bundle on trial. See notifyAppReady.
STORAGE_ERROR A file-system operation failed.
UNKNOWN Any error the native layer did not classify.

Two codes are not raised identically on both platforms. INVALID_URL is raised on iOS only; on Android a malformed URL surfaces as UNKNOWN. PARSE_ERROR covers malformed update-check JSON on iOS; on Android that case also surfaces as UNKNOWN. Branch on UNKNOWN as a catch-all rather than assuming a code is platform-symmetric.

Server contract

The plugin talks to exactly two endpoints. Everything else in the DeployYourApp API is dashboard-facing and requires authentication.

POST {updateUrl}/api/update

Rate limited to 60 requests per minute. Request body:

{
  "appId": "com.yourcompany.yourapp",
  "platform": "ios",
  "channel": "production",
  "deviceId": "0f4b1c1e-8a2f-4c3a-9b1e-2f5d6a7b8c90",
  "customId": "",
  "nativeVersion": "1.4.0",
  "nativeBuild": "142",
  "currentVersion": "1.3.2",
  "currentBundleId": "1.3.2",
  "pluginVersion": "0.3.0",
  "osVersion": "17.5.1",
  "device": "Apple iPhone15,2"
}

platform is ios or android. osVersion and device are sent by both platforms — Android reports "MANUFACTURER MODEL" (e.g. Google Pixel 7), iOS the hardware identifier (e.g. Apple iPhone15,2). The server's schema covers appId, deviceId, platform, currentVersion, nativeVersion, pluginVersion, and channel; the remaining fields are accepted but discarded.

A 204 No Content response means no update, and the plugin treats it as { available: false }. The server answers 204 for an unknown app, a missing or empty channel, a device not subscribed to a private channel, a bundle for another platform, a version the device already runs, a blocked major/minor step or downgrade, a native version outside the bundle's compatibility range, a scheduled deployment that has not started, and a device outside the rollout percentage.

A 200 response carries the JSON that becomes UpdateCheckResult. Any other status rejects with NETWORK_ERROR.

POST {statsUrl}/api/stats

Rate limited to 100 requests per minute. Batches carry at most 100 events.

{
  "appId": "com.yourcompany.yourapp",
  "deviceId": "0f4b1c1e-8a2f-4c3a-9b1e-2f5d6a7b8c90",
  "events": [
    {
      "eventType": "page_view",
      "eventData": { "path": "/settings", "title": "Settings" },
      "bundleVersion": "1.3.2",
      "timestamp": "2026-07-31T12:00:00.000Z"
    }
  ]
}

bundleVersion is filled in by the plugin with the active bundle's version, or builtin.

Web platform

The plugin registers a web implementation so imports resolve in a browser or PWA build, but a browser cannot swap the served assets. Behaviour differs:

Method Web behaviour
checkForUpdate() Always resolves { available: false }. No network call.
download(), apply(), deleteBundle() Reject as unavailable.
notifyAppReady(), reset() Resolve without doing anything.
reload() Calls window.location.reload().
getCurrentBundle() Returns the built-in placeholder.
getNextBundle(), listBundles() Return { bundle: null } and { bundles: [] }.
Channel, device ID, custom ID, version override Work, backed by localStorage keys prefixed dya_.
getNativeVersion() Returns 0.0.0.
Analytics methods Work, posting to {statsUrl}/api/stats in the same batched format.

No plugin events are emitted on web.


Previous
Configuration
Next
Update Lifecycle