# Running in the background - JavaScript bridge API

> A foreground service the page starts and stops, location that keeps arriving with the app in the background, geofences that fire with the app closed, and your own code run periodically after a restart. Each needs its switch in the build and a Play Console declaration.

- **Applies to:** AppMint
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebtoapk.com/docs/api/background

### `AppMint.background`

```js
AppMint.background.register(name, options) / unregister / list / runNow / done / isBackgroundRun / currentRun / onRun
```

**Example**

Runs your page's OWN code periodically with the app closed - refresh data, check a price, post a notification. Each run loads your page invisibly (same origin, same localStorage/IndexedDB, no screen, no ads) with `?appmint_background=1&appmint_task=<name>&appmint_run=<runId>`, fires `appmint:background { name, runId }` once it has loaded, and waits for `done(runId, { ok })`. A run is cut off after about 9 minutes.

**Returns:** every method returns a Promise. `register` → `{ ok: true, name, intervalMinutes }` or `{ ok: false, reason }` (`not_enabled`, `invalid_name`, `interval_below_15_minutes`, `invalid_interval`, `schedule_failed`). `unregister` → `true` / `false`. `list` → `[{ name, intervalMinutes, requiresNetwork, requiresCharging, registeredAt, lastRunAt, lastResult }]` (`lastResult`: `ok`, `retry`, `failed`, `timeout`, `unsupported_mode`, `page_load_failed`, …). `runNow` → `{ ok }` (one run as soon as the conditions allow - for testing). `done` → `true` when the run was ended. `isBackgroundRun()` and `currentRun()` are synchronous. **Needs:** tick **Run page code while closed** (Access step → Background). Website and offline/ZIP apps; document and media apps have no page code to run.

**One page, two roles:**

```js
function startApp() {
  const bg = window.AppMint && AppMint.background;
  if (bg && bg.isBackgroundRun()) return;          // a background run: no UI work
  renderUi();
  if (bg) bg.register('prices', { intervalMinutes: 60, requiresNetwork: true }).then(function (r) {
    if (!r.ok) console.warn('background task not scheduled:', r.reason);
  });
}

// Runs only in the background copy of the page.
window.addEventListener('appmint:background', async function (e) {
  const run = e.detail;                              // { name: 'prices', runId: '…' }
  try {
    const price = await fetch('https://api.example.com/price').then(function (r) { return r.json(); });
    const last = Number(localStorage.getItem('lastPrice') || 0);
    if (price.value < last) {
      WebToApk.notify(JSON.stringify({ title: 'Price dropped', body: 'Now ' + price.value, tag: 'price' }));
    }
    localStorage.setItem('lastPrice', String(price.value));
    AppMint.background.done(run.runId, { ok: true });
  } catch (err) {
    AppMint.background.done(run.runId, { ok: false, retry: true });   // retried with backoff
  }
});
startApp();
```

**Late-mounting pages** (React, a lazy route) use `onRun`, which also fires if the run already started, or the page hook:

```js
if (window.AppMint && AppMint.background) {
  AppMint.background.onRun(function (run) { syncInbox().then(function () { AppMint.background.done(run.runId, { ok: true }); }); });
}
window.onAppMintBackground = function (run) { console.log('background run', run.name, run.runId, AppMint.background.currentRun()); };
```

**Manage:**

```js
AppMint.background.list().then(function (tasks) { console.table(tasks); });
AppMint.background.runNow('prices');
AppMint.background.unregister('prices');
```

**Notes:** Android decides the exact moment; 15 minutes is the shortest interval and battery saving can delay runs. With **Start after phone restart** every task also runs once after a restart. A run's `WebToApk` has only `notify`, `showNotification`, the location buffer, geofences, `workEnqueueOnline`, `isBackgroundServiceRunning` and these task methods; anything else is absent (feature-detect).

### `addGeofence`

```js
window.WebToApk.addGeofence(json: String): String
```

Adds a geofence {id, lat, lng, radiusM, events, dwellMs?, notify?} → JSON {ok, status:'adding'} or {ok:false, reason}; the result is appmint:geofence-added.

**Example**

A circle that fires `enter`, `exit` and/or `dwell` with the app closed, optionally posting a notification. Geofences survive a phone restart (they are added back automatically).

**Returns:** `addGeofence` → a JSON string `{ ok: true, status: 'adding', id }` or `{ ok: false, reason }` - `not_enabled`, `invalid_id`, `invalid_position`, `invalid_radius` (1 m … 100 km), `invalid_events`, `location_permission_required`, `background_permission_required`, `foreign_origin`. Play services' answer arrives as `appmint:geofence-added` (`window.onAppMintGeofenceAdded`) = `{ id, ok, reason? }` (`location_off`, `too_many_geofences` - 100 per app, `play_services_unavailable`). A transition arrives as `appmint:geofence` (`window.onAppMintGeofence`) = `{ id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? }` - `queued: true` when it happened while the app was closed and is handed over at the next start. `removeGeofence` → `true` if it existed; `getGeofences` → a JSON array of what you added. **Needs:** tick **Geofences** in the build, and "Allow all the time" (`requestBackgroundLocation()`).

```js
function watchOffice() {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) return;
  const r = JSON.parse(b.addGeofence(JSON.stringify({
    id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
    events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
    notify: {
      enter: { title: 'At the office', body: 'Tap to check in' },
      exit: { title: 'Left the office', body: 'Tap to check out' }
    }
  })));
  if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}

window.addEventListener('appmint:geofence-added', function (e) {
  if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');
```

### `backgroundDone`

```js
window.WebToApk.backgroundDone(runId: String, resultJson: String): Boolean
```

AppMint.background.done(runId, result): ends a background run; always false in the app itself.

**Example**

Documented together with `WebToApk.backgroundRegister` - the example there shows this one too.

The raw methods behind `AppMint.background` - use `AppMint.background.register(...)` (it returns promises and handles the run event). Listed here so every bridge method has an example.

**Returns:** `backgroundRegister` → JSON `{ ok: true, name, intervalMinutes }` or `{ ok: false, reason }` (`not_enabled`, `invalid_name` - letters, digits, `_`, `-`, up to 64 - `interval_below_15_minutes`, `invalid_interval`, `schedule_failed`). `backgroundUnregister` → `true` if it existed. `backgroundList` → JSON array. `backgroundRunNow` → JSON `{ ok, name }` or `{ ok: false, reason: 'not_registered' }`. `backgroundDone(runId, resultJson)` → `true` in a background run, always `false` in the app. `isBackgroundRun()` → `false` in the app, `true` in a background run. **Needs:** tick **Run page code while closed**.

```js
const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}
```

### `backgroundList`

```js
window.WebToApk.backgroundList(): String
```

AppMint.background.list(): JSON array {name, intervalMinutes, requiresNetwork, requiresCharging, registeredAt, lastRunAt, lastResult}.

**Example**

Documented together with `WebToApk.backgroundRegister` - the example there shows this one too.

The raw methods behind `AppMint.background` - use `AppMint.background.register(...)` (it returns promises and handles the run event). Listed here so every bridge method has an example.

**Returns:** `backgroundRegister` → JSON `{ ok: true, name, intervalMinutes }` or `{ ok: false, reason }` (`not_enabled`, `invalid_name` - letters, digits, `_`, `-`, up to 64 - `interval_below_15_minutes`, `invalid_interval`, `schedule_failed`). `backgroundUnregister` → `true` if it existed. `backgroundList` → JSON array. `backgroundRunNow` → JSON `{ ok, name }` or `{ ok: false, reason: 'not_registered' }`. `backgroundDone(runId, resultJson)` → `true` in a background run, always `false` in the app. `isBackgroundRun()` → `false` in the app, `true` in a background run. **Needs:** tick **Run page code while closed**.

```js
const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}
```

### `backgroundRegister`

```js
window.WebToApk.backgroundRegister(name: String, optionsJson: String): String
```

AppMint.background.register(name, {intervalMinutes >= 15, requiresNetwork, requiresCharging}) → JSON {ok, name, intervalMinutes} or {ok:false, reason}.

**Example**

The raw methods behind `AppMint.background` - use `AppMint.background.register(...)` (it returns promises and handles the run event). Listed here so every bridge method has an example.

**Returns:** `backgroundRegister` → JSON `{ ok: true, name, intervalMinutes }` or `{ ok: false, reason }` (`not_enabled`, `invalid_name` - letters, digits, `_`, `-`, up to 64 - `interval_below_15_minutes`, `invalid_interval`, `schedule_failed`). `backgroundUnregister` → `true` if it existed. `backgroundList` → JSON array. `backgroundRunNow` → JSON `{ ok, name }` or `{ ok: false, reason: 'not_registered' }`. `backgroundDone(runId, resultJson)` → `true` in a background run, always `false` in the app. `isBackgroundRun()` → `false` in the app, `true` in a background run. **Needs:** tick **Run page code while closed**.

```js
const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}
```

### `backgroundRunNow`

```js
window.WebToApk.backgroundRunNow(name: String): String
```

AppMint.background.runNow(name): one background run as soon as its conditions allow.

**Example**

Documented together with `WebToApk.backgroundRegister` - the example there shows this one too.

The raw methods behind `AppMint.background` - use `AppMint.background.register(...)` (it returns promises and handles the run event). Listed here so every bridge method has an example.

**Returns:** `backgroundRegister` → JSON `{ ok: true, name, intervalMinutes }` or `{ ok: false, reason }` (`not_enabled`, `invalid_name` - letters, digits, `_`, `-`, up to 64 - `interval_below_15_minutes`, `invalid_interval`, `schedule_failed`). `backgroundUnregister` → `true` if it existed. `backgroundList` → JSON array. `backgroundRunNow` → JSON `{ ok, name }` or `{ ok: false, reason: 'not_registered' }`. `backgroundDone(runId, resultJson)` → `true` in a background run, always `false` in the app. `isBackgroundRun()` → `false` in the app, `true` in a background run. **Needs:** tick **Run page code while closed**.

```js
const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}
```

### `backgroundUnregister`

```js
window.WebToApk.backgroundUnregister(name: String): Boolean
```

AppMint.background.unregister(name); false when no task had that name.

**Example**

Documented together with `WebToApk.backgroundRegister` - the example there shows this one too.

The raw methods behind `AppMint.background` - use `AppMint.background.register(...)` (it returns promises and handles the run event). Listed here so every bridge method has an example.

**Returns:** `backgroundRegister` → JSON `{ ok: true, name, intervalMinutes }` or `{ ok: false, reason }` (`not_enabled`, `invalid_name` - letters, digits, `_`, `-`, up to 64 - `interval_below_15_minutes`, `invalid_interval`, `schedule_failed`). `backgroundUnregister` → `true` if it existed. `backgroundList` → JSON array. `backgroundRunNow` → JSON `{ ok, name }` or `{ ok: false, reason: 'not_registered' }`. `backgroundDone(runId, resultJson)` → `true` in a background run, always `false` in the app. `isBackgroundRun()` → `false` in the app, `true` in a background run. **Needs:** tick **Run page code while closed**.

```js
const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}
```

### `clearBufferedLocations`

```js
window.WebToApk.clearBufferedLocations(): Boolean
```

Empties the buffer getBufferedLocations reads (call it after you saved the points).

**Example**

Documented together with `WebToApk.getBufferedLocations` - the example there shows this one too.

The fixes recorded while your page was away (app in the background or closed), oldest first, up to 10,000.

**Returns:** `getBufferedLocations()` → a JSON string array of `{ type: 'fix', lat, lng, accuracy, time, …, background: true }`; `'[]'` when Background location is off. `clearBufferedLocations()` → `true` when emptied. **Needs:** **Background location** in the build.

```js
function syncTrack() {
  if (!(window.WebToApk && WebToApk.getBufferedLocations)) return;
  const points = JSON.parse(WebToApk.getBufferedLocations());
  if (!points.length) return;
  points.forEach(function (p) { drawPoint(p.lat, p.lng); });
  saveTrack(points).then(function () { WebToApk.clearBufferedLocations(); });
}
document.addEventListener('visibilitychange', function () { if (!document.hidden) syncTrack(); });
```

### `clearGeofenceEvents`

```js
window.WebToApk.clearGeofenceEvents(): Boolean
```

Empties the transition history getGeofenceEvents reads.

**Example**

Documented together with `WebToApk.getGeofenceEvents` - the example there shows this one too.

The last 100 geofence transitions, oldest first - including the ones that happened while the app was closed. Read it on start if your page mounts late and might miss the queued `appmint:geofence` events.

**Returns:** `getGeofenceEvents()` → a JSON string array of `{ id, event, time, lat, lng }`; `clearGeofenceEvents()` → `true`. **Needs:** **Geofences** in the build.

```js
if (window.WebToApk && WebToApk.getGeofenceEvents) {
  JSON.parse(WebToApk.getGeofenceEvents()).forEach(function (ev) {
    logVisit(ev.id, ev.event, ev.time);
  });
  WebToApk.clearGeofenceEvents();
}
```

### `getBufferedLocations`

```js
window.WebToApk.getBufferedLocations(): String
```

JSON array of the fixes recorded while the page was away, oldest first (up to 10,000).

**Example**

The fixes recorded while your page was away (app in the background or closed), oldest first, up to 10,000.

**Returns:** `getBufferedLocations()` → a JSON string array of `{ type: 'fix', lat, lng, accuracy, time, …, background: true }`; `'[]'` when Background location is off. `clearBufferedLocations()` → `true` when emptied. **Needs:** **Background location** in the build.

```js
function syncTrack() {
  if (!(window.WebToApk && WebToApk.getBufferedLocations)) return;
  const points = JSON.parse(WebToApk.getBufferedLocations());
  if (!points.length) return;
  points.forEach(function (p) { drawPoint(p.lat, p.lng); });
  saveTrack(points).then(function () { WebToApk.clearBufferedLocations(); });
}
document.addEventListener('visibilitychange', function () { if (!document.hidden) syncTrack(); });
```

### `getGeofenceEvents`

```js
window.WebToApk.getGeofenceEvents(): String
```

JSON array of the last 100 geofence transitions {id, event, time, lat, lng}, oldest first.

**Example**

The last 100 geofence transitions, oldest first - including the ones that happened while the app was closed. Read it on start if your page mounts late and might miss the queued `appmint:geofence` events.

**Returns:** `getGeofenceEvents()` → a JSON string array of `{ id, event, time, lat, lng }`; `clearGeofenceEvents()` → `true`. **Needs:** **Geofences** in the build.

```js
if (window.WebToApk && WebToApk.getGeofenceEvents) {
  JSON.parse(WebToApk.getGeofenceEvents()).forEach(function (ev) {
    logVisit(ev.id, ev.event, ev.time);
  });
  WebToApk.clearGeofenceEvents();
}
```

### `getGeofences`

```js
window.WebToApk.getGeofences(): String
```

JSON array of the geofences this app has added (they survive restarts).

**Example**

Documented together with `WebToApk.addGeofence` - the example there shows this one too.

A circle that fires `enter`, `exit` and/or `dwell` with the app closed, optionally posting a notification. Geofences survive a phone restart (they are added back automatically).

**Returns:** `addGeofence` → a JSON string `{ ok: true, status: 'adding', id }` or `{ ok: false, reason }` - `not_enabled`, `invalid_id`, `invalid_position`, `invalid_radius` (1 m … 100 km), `invalid_events`, `location_permission_required`, `background_permission_required`, `foreign_origin`. Play services' answer arrives as `appmint:geofence-added` (`window.onAppMintGeofenceAdded`) = `{ id, ok, reason? }` (`location_off`, `too_many_geofences` - 100 per app, `play_services_unavailable`). A transition arrives as `appmint:geofence` (`window.onAppMintGeofence`) = `{ id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? }` - `queued: true` when it happened while the app was closed and is handed over at the next start. `removeGeofence` → `true` if it existed; `getGeofences` → a JSON array of what you added. **Needs:** tick **Geofences** in the build, and "Allow all the time" (`requestBackgroundLocation()`).

```js
function watchOffice() {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) return;
  const r = JSON.parse(b.addGeofence(JSON.stringify({
    id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
    events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
    notify: {
      enter: { title: 'At the office', body: 'Tap to check in' },
      exit: { title: 'Left the office', body: 'Tap to check out' }
    }
  })));
  if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}

window.addEventListener('appmint:geofence-added', function (e) {
  if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');
```

### `getLocationPermissionState`

```js
window.WebToApk.getLocationPermissionState(): String
```

JSON {foreground:'granted'|'denied', background:'granted'|'denied', precise: boolean}.

**Example**

Documented together with `WebToApk.requestBackgroundLocation` - the example there shows this one too.

Asks for location, then for "Allow all the time" (Android 10+ asks for them separately; on Android 11+ the second step opens the app's location settings page). Geofences need it; so does restarting location tracking after a phone restart.

**Returns:** nothing directly; the answer arrives as `appmint:location-permission` (and `window.onAppMintLocationPermission(detail)`) = `{ foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }`. `getLocationPermissionState()` → the same object as a JSON string, without asking. **Needs:** **Background location** or **Geofences** in the build.

```js
function askAlways() {
  const b = window.WebToApk;
  if (!(b && b.requestBackgroundLocation)) return;
  const now = JSON.parse(b.getLocationPermissionState());
  if (now.background === 'granted') { enableGeofences(); return; }
  // Explain first: Play requires an in-app disclosure before this request.
  if (!confirm('Allow location "all the time" so we can remind you when you arrive?')) return;
  b.requestBackgroundLocation();
}

window.addEventListener('appmint:location-permission', function (e) {
  if (e.detail.background === 'granted') enableGeofences();
  else showMessage('Choose "Allow all the time" in the location settings to use arrival reminders.');
});
```

### `isBackgroundRun`

```js
window.WebToApk.isBackgroundRun(): Boolean
```

False here: the app's own page is never a background run (a headless run answers true).

**Example**

Documented together with `WebToApk.backgroundRegister` - the example there shows this one too.

The raw methods behind `AppMint.background` - use `AppMint.background.register(...)` (it returns promises and handles the run event). Listed here so every bridge method has an example.

**Returns:** `backgroundRegister` → JSON `{ ok: true, name, intervalMinutes }` or `{ ok: false, reason }` (`not_enabled`, `invalid_name` - letters, digits, `_`, `-`, up to 64 - `interval_below_15_minutes`, `invalid_interval`, `schedule_failed`). `backgroundUnregister` → `true` if it existed. `backgroundList` → JSON array. `backgroundRunNow` → JSON `{ ok, name }` or `{ ok: false, reason: 'not_registered' }`. `backgroundDone(runId, resultJson)` → `true` in a background run, always `false` in the app. `isBackgroundRun()` → `false` in the app, `true` in a background run. **Needs:** tick **Run page code while closed**.

```js
const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}
```

### `isBackgroundServiceRunning`

```js
window.WebToApk.isBackgroundServiceRunning(): Boolean
```

Whether the app's foreground service is running (for either keep-running or background location).

**Example**

Documented together with `WebToApk.startBackgroundService` - the example there shows this one too.

Keeps your app running while the user is away: a real Android foreground service with a notification. While it runs the page is not paused, so its JavaScript, `<audio>` and the app's background music carry on (Android still slows the timers of a page nobody can see).

**Returns:** `startBackgroundService` → a JSON string `{ ok: true, status: 'starting', type, notificationShown }` or `{ ok: false, reason }` - reason `not_enabled`, `invalid_options`, `type_not_declared`, `invalid_progress`, `location_permission_required`, `start_not_allowed`. `updateBackgroundService` → `{ ok: true }` or `{ ok: false, reason: 'not_running' }`. `stopBackgroundService` / `isBackgroundServiceRunning` → `true` / `false`. State changes arrive as `appmint:background-service` (and `window.onAppMintBackgroundService(detail)`): `{ state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }`. **Needs:** tick **Keep running in background** (Access step → Background) and pick the service type. On Android 13+ the notification is only visible once the user allowed notifications (`notificationShown: false` otherwise - the service still runs).

```js
function startWorkout() {
  const b = window.WebToApk;
  if (!(b && b.startBackgroundService)) return;             // browser
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({
    title: 'Workout in progress',
    text: '00:00 elapsed',
    stopLabel: 'Stop',                  // a Stop button on the notification
    progress: { value: 0, max: 100 }
  })));
  if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}

function tick(seconds) {
  if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
    WebToApk.updateBackgroundService(JSON.stringify({
      title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
      progress: { value: Math.min(100, seconds / 18), max: 100 }
    }));
  }
}

function finishWorkout() {
  if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}

window.addEventListener('appmint:background-service', function (e) {
  // reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
  // 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
  // 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
  if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});
```

**Notes:** the type must be one the build declared (the one picked in the wizard, plus `location` when Background location is on). Swiping the app away stops a keep-running service unless you pass `keepAfterSwipe: true` (the page itself is gone then). Play Console asks for a foreground-service declaration with a short video for every declared type.

### `isIgnoringBatteryOptimizations`

```js
window.WebToApk.isIgnoringBatteryOptimizations(): Boolean
```

Whether the user exempted this app from battery optimisation (Doze app standby).

**Example**

Checks and asks for the battery-optimisation exemption, so Doze does not defer your background work.

**Returns:** `isIgnoringBatteryOptimizations()` → `true` / `false`. `requestIgnoreBatteryOptimizations()` → `'already_ignoring'`, `'dialog_opened'` (the system's Allow/Deny dialog - only when the build ticked **Ask to skip battery optimisation directly**), `'settings_opened'` (the battery-optimisation list; no permission, no Play declaration) or `'unavailable'`. **Needs:** nothing for the settings list. The direct dialog declares REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, which Play accepts only when it is the app's core function.

```js
document.getElementById('reliable').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.isIgnoringBatteryOptimizations)) return;
  if (b.isIgnoringBatteryOptimizations()) { showMessage('Already unrestricted'); return; }
  const how = b.requestIgnoreBatteryOptimizations();
  if (how === 'settings_opened') showMessage('Find this app in the list and choose "Not optimised".');
});
```

### `removeGeofence`

```js
window.WebToApk.removeGeofence(id: String): Boolean
```

Removes the geofence with this id; false when there was none.

**Example**

Documented together with `WebToApk.addGeofence` - the example there shows this one too.

A circle that fires `enter`, `exit` and/or `dwell` with the app closed, optionally posting a notification. Geofences survive a phone restart (they are added back automatically).

**Returns:** `addGeofence` → a JSON string `{ ok: true, status: 'adding', id }` or `{ ok: false, reason }` - `not_enabled`, `invalid_id`, `invalid_position`, `invalid_radius` (1 m … 100 km), `invalid_events`, `location_permission_required`, `background_permission_required`, `foreign_origin`. Play services' answer arrives as `appmint:geofence-added` (`window.onAppMintGeofenceAdded`) = `{ id, ok, reason? }` (`location_off`, `too_many_geofences` - 100 per app, `play_services_unavailable`). A transition arrives as `appmint:geofence` (`window.onAppMintGeofence`) = `{ id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? }` - `queued: true` when it happened while the app was closed and is handed over at the next start. `removeGeofence` → `true` if it existed; `getGeofences` → a JSON array of what you added. **Needs:** tick **Geofences** in the build, and "Allow all the time" (`requestBackgroundLocation()`).

```js
function watchOffice() {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) return;
  const r = JSON.parse(b.addGeofence(JSON.stringify({
    id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
    events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
    notify: {
      enter: { title: 'At the office', body: 'Tap to check in' },
      exit: { title: 'Left the office', body: 'Tap to check out' }
    }
  })));
  if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}

window.addEventListener('appmint:geofence-added', function (e) {
  if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');
```

### `requestBackgroundLocation`

```js
window.WebToApk.requestBackgroundLocation()
```

Asks for location, then "Allow all the time"; the result arrives as appmint:location-permission.

**Example**

Asks for location, then for "Allow all the time" (Android 10+ asks for them separately; on Android 11+ the second step opens the app's location settings page). Geofences need it; so does restarting location tracking after a phone restart.

**Returns:** nothing directly; the answer arrives as `appmint:location-permission` (and `window.onAppMintLocationPermission(detail)`) = `{ foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }`. `getLocationPermissionState()` → the same object as a JSON string, without asking. **Needs:** **Background location** or **Geofences** in the build.

```js
function askAlways() {
  const b = window.WebToApk;
  if (!(b && b.requestBackgroundLocation)) return;
  const now = JSON.parse(b.getLocationPermissionState());
  if (now.background === 'granted') { enableGeofences(); return; }
  // Explain first: Play requires an in-app disclosure before this request.
  if (!confirm('Allow location "all the time" so we can remind you when you arrive?')) return;
  b.requestBackgroundLocation();
}

window.addEventListener('appmint:location-permission', function (e) {
  if (e.detail.background === 'granted') enableGeofences();
  else showMessage('Choose "Allow all the time" in the location settings to use arrival reminders.');
});
```

### `requestIgnoreBatteryOptimizations`

```js
window.WebToApk.requestIgnoreBatteryOptimizations(): String
```

Asks for the battery exemption: 'already_ignoring' | 'dialog_opened' | 'settings_opened' | 'unavailable'.

**Example**

Documented together with `WebToApk.isIgnoringBatteryOptimizations` - the example there shows this one too.

Checks and asks for the battery-optimisation exemption, so Doze does not defer your background work.

**Returns:** `isIgnoringBatteryOptimizations()` → `true` / `false`. `requestIgnoreBatteryOptimizations()` → `'already_ignoring'`, `'dialog_opened'` (the system's Allow/Deny dialog - only when the build ticked **Ask to skip battery optimisation directly**), `'settings_opened'` (the battery-optimisation list; no permission, no Play declaration) or `'unavailable'`. **Needs:** nothing for the settings list. The direct dialog declares REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, which Play accepts only when it is the app's core function.

```js
document.getElementById('reliable').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.isIgnoringBatteryOptimizations)) return;
  if (b.isIgnoringBatteryOptimizations()) { showMessage('Already unrestricted'); return; }
  const how = b.requestIgnoreBatteryOptimizations();
  if (how === 'settings_opened') showMessage('Find this app in the list and choose "Not optimised".');
});
```

### `startBackgroundService`

```js
window.WebToApk.startBackgroundService(optionsJson: String): String
```

Starts the app's foreground service: {title, text, type?, progress?, stopLabel?, keepAfterSwipe?} → JSON {ok, status, type, notificationShown} or {ok:false, reason}.

**Example**

Keeps your app running while the user is away: a real Android foreground service with a notification. While it runs the page is not paused, so its JavaScript, `<audio>` and the app's background music carry on (Android still slows the timers of a page nobody can see).

**Returns:** `startBackgroundService` → a JSON string `{ ok: true, status: 'starting', type, notificationShown }` or `{ ok: false, reason }` - reason `not_enabled`, `invalid_options`, `type_not_declared`, `invalid_progress`, `location_permission_required`, `start_not_allowed`. `updateBackgroundService` → `{ ok: true }` or `{ ok: false, reason: 'not_running' }`. `stopBackgroundService` / `isBackgroundServiceRunning` → `true` / `false`. State changes arrive as `appmint:background-service` (and `window.onAppMintBackgroundService(detail)`): `{ state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }`. **Needs:** tick **Keep running in background** (Access step → Background) and pick the service type. On Android 13+ the notification is only visible once the user allowed notifications (`notificationShown: false` otherwise - the service still runs).

```js
function startWorkout() {
  const b = window.WebToApk;
  if (!(b && b.startBackgroundService)) return;             // browser
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({
    title: 'Workout in progress',
    text: '00:00 elapsed',
    stopLabel: 'Stop',                  // a Stop button on the notification
    progress: { value: 0, max: 100 }
  })));
  if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}

function tick(seconds) {
  if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
    WebToApk.updateBackgroundService(JSON.stringify({
      title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
      progress: { value: Math.min(100, seconds / 18), max: 100 }
    }));
  }
}

function finishWorkout() {
  if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}

window.addEventListener('appmint:background-service', function (e) {
  // reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
  // 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
  // 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
  if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});
```

**Notes:** the type must be one the build declared (the one picked in the wizard, plus `location` when Background location is on). Swiping the app away stops a keep-running service unless you pass `keepAfterSwipe: true` (the page itself is gone then). Play Console asks for a foreground-service declaration with a short video for every declared type.

### `startLocationUpdates`

```js
window.WebToApk.startLocationUpdates(optionsJson: String): String
```

Location updates {intervalMs, minDistanceM, accuracy, background, uploadUrl?, headers?} → JSON {ok, background, backgroundPermission, precise} or {ok:false, reason}; fixes arrive as appmint:location.

**Example**

Location updates that keep coming while the app is in the background (a run tracker, a delivery app). With `background: true` (the default) they run inside the app's foreground service. Each fix goes to the page as `appmint:location` while the page is there, into a buffer (`getBufferedLocations()`) while it is not, and - when you give an `uploadUrl` - to your server as a JSON POST from native code, so tracking works with the page gone.

**Returns:** a JSON string `{ ok: true, background, backgroundPermission, precise }` or `{ ok: false, reason }` - `not_enabled`, `invalid_options`, `invalid_interval` (1 s … 24 h), `invalid_distance`, `invalid_accuracy`, `invalid_upload_url`, `location_permission_required`, `start_not_allowed`, `foreign_origin`. `stopLocationUpdates()` → `true` if something was running. Fixes: `appmint:location` / `window.onAppMintLocation(detail)` = `{ type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background }`, or `{ type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }`. **Needs:** tick **Background location** (Access step → Background) and the user's location permission first.

```js
function startTracking(token) {
  const b = window.WebToApk;
  if (!(b && b.startLocationUpdates)) return;
  navigator.geolocation.getCurrentPosition(function () {       // asks for location first
    const r = JSON.parse(b.startLocationUpdates(JSON.stringify({
      intervalMs: 10000, minDistanceM: 10, accuracy: 'high', background: true,
      uploadUrl: 'https://api.example.com/track',
      headers: { Authorization: 'Bearer ' + token }
    })));
    if (!r.ok) showMessage('Tracking not started: ' + r.reason);
  });
}

window.addEventListener('appmint:location', function (e) {
  if (e.detail.type === 'fix') drawPoint(e.detail.lat, e.detail.lng);
  else showMessage('Location stopped: ' + e.detail.reason);
});

function stopTracking() { if (window.WebToApk) WebToApk.stopLocationUpdates(); }
```

**Notes:** a location service started from the app keeps getting fixes in the background without "Allow all the time"; restarting it after a phone restart needs it (`requestBackgroundLocation()`). A POST that fails is handed to WorkManager and sent when the network is back. Phones without Google Play services answer `play_services_unavailable`. Play Console needs a background-location declaration.

### `stopBackgroundService`

```js
window.WebToApk.stopBackgroundService(): Boolean
```

Stops the service started by startBackgroundService (background location, if running, continues).

**Example**

Documented together with `WebToApk.startBackgroundService` - the example there shows this one too.

Keeps your app running while the user is away: a real Android foreground service with a notification. While it runs the page is not paused, so its JavaScript, `<audio>` and the app's background music carry on (Android still slows the timers of a page nobody can see).

**Returns:** `startBackgroundService` → a JSON string `{ ok: true, status: 'starting', type, notificationShown }` or `{ ok: false, reason }` - reason `not_enabled`, `invalid_options`, `type_not_declared`, `invalid_progress`, `location_permission_required`, `start_not_allowed`. `updateBackgroundService` → `{ ok: true }` or `{ ok: false, reason: 'not_running' }`. `stopBackgroundService` / `isBackgroundServiceRunning` → `true` / `false`. State changes arrive as `appmint:background-service` (and `window.onAppMintBackgroundService(detail)`): `{ state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }`. **Needs:** tick **Keep running in background** (Access step → Background) and pick the service type. On Android 13+ the notification is only visible once the user allowed notifications (`notificationShown: false` otherwise - the service still runs).

```js
function startWorkout() {
  const b = window.WebToApk;
  if (!(b && b.startBackgroundService)) return;             // browser
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({
    title: 'Workout in progress',
    text: '00:00 elapsed',
    stopLabel: 'Stop',                  // a Stop button on the notification
    progress: { value: 0, max: 100 }
  })));
  if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}

function tick(seconds) {
  if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
    WebToApk.updateBackgroundService(JSON.stringify({
      title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
      progress: { value: Math.min(100, seconds / 18), max: 100 }
    }));
  }
}

function finishWorkout() {
  if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}

window.addEventListener('appmint:background-service', function (e) {
  // reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
  // 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
  // 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
  if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});
```

**Notes:** the type must be one the build declared (the one picked in the wizard, plus `location` when Background location is on). Swiping the app away stops a keep-running service unless you pass `keepAfterSwipe: true` (the page itself is gone then). Play Console asks for a foreground-service declaration with a short video for every declared type.

### `stopLocationUpdates`

```js
window.WebToApk.stopLocationUpdates(): Boolean
```

Stops the location updates started by startLocationUpdates; false when none were running.

**Example**

Documented together with `WebToApk.startLocationUpdates` - the example there shows this one too.

Location updates that keep coming while the app is in the background (a run tracker, a delivery app). With `background: true` (the default) they run inside the app's foreground service. Each fix goes to the page as `appmint:location` while the page is there, into a buffer (`getBufferedLocations()`) while it is not, and - when you give an `uploadUrl` - to your server as a JSON POST from native code, so tracking works with the page gone.

**Returns:** a JSON string `{ ok: true, background, backgroundPermission, precise }` or `{ ok: false, reason }` - `not_enabled`, `invalid_options`, `invalid_interval` (1 s … 24 h), `invalid_distance`, `invalid_accuracy`, `invalid_upload_url`, `location_permission_required`, `start_not_allowed`, `foreign_origin`. `stopLocationUpdates()` → `true` if something was running. Fixes: `appmint:location` / `window.onAppMintLocation(detail)` = `{ type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background }`, or `{ type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }`. **Needs:** tick **Background location** (Access step → Background) and the user's location permission first.

```js
function startTracking(token) {
  const b = window.WebToApk;
  if (!(b && b.startLocationUpdates)) return;
  navigator.geolocation.getCurrentPosition(function () {       // asks for location first
    const r = JSON.parse(b.startLocationUpdates(JSON.stringify({
      intervalMs: 10000, minDistanceM: 10, accuracy: 'high', background: true,
      uploadUrl: 'https://api.example.com/track',
      headers: { Authorization: 'Bearer ' + token }
    })));
    if (!r.ok) showMessage('Tracking not started: ' + r.reason);
  });
}

window.addEventListener('appmint:location', function (e) {
  if (e.detail.type === 'fix') drawPoint(e.detail.lat, e.detail.lng);
  else showMessage('Location stopped: ' + e.detail.reason);
});

function stopTracking() { if (window.WebToApk) WebToApk.stopLocationUpdates(); }
```

**Notes:** a location service started from the app keeps getting fixes in the background without "Allow all the time"; restarting it after a phone restart needs it (`requestBackgroundLocation()`). A POST that fails is handed to WorkManager and sent when the network is back. Phones without Google Play services answer `play_services_unavailable`. Play Console needs a background-location declaration.

### `updateBackgroundService`

```js
window.WebToApk.updateBackgroundService(optionsJson: String): String
```

Changes the running service's notification (same options as startBackgroundService) → JSON {ok} or {ok:false, reason:'not_running'}.

**Example**

Documented together with `WebToApk.startBackgroundService` - the example there shows this one too.

Keeps your app running while the user is away: a real Android foreground service with a notification. While it runs the page is not paused, so its JavaScript, `<audio>` and the app's background music carry on (Android still slows the timers of a page nobody can see).

**Returns:** `startBackgroundService` → a JSON string `{ ok: true, status: 'starting', type, notificationShown }` or `{ ok: false, reason }` - reason `not_enabled`, `invalid_options`, `type_not_declared`, `invalid_progress`, `location_permission_required`, `start_not_allowed`. `updateBackgroundService` → `{ ok: true }` or `{ ok: false, reason: 'not_running' }`. `stopBackgroundService` / `isBackgroundServiceRunning` → `true` / `false`. State changes arrive as `appmint:background-service` (and `window.onAppMintBackgroundService(detail)`): `{ state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }`. **Needs:** tick **Keep running in background** (Access step → Background) and pick the service type. On Android 13+ the notification is only visible once the user allowed notifications (`notificationShown: false` otherwise - the service still runs).

```js
function startWorkout() {
  const b = window.WebToApk;
  if (!(b && b.startBackgroundService)) return;             // browser
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({
    title: 'Workout in progress',
    text: '00:00 elapsed',
    stopLabel: 'Stop',                  // a Stop button on the notification
    progress: { value: 0, max: 100 }
  })));
  if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}

function tick(seconds) {
  if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
    WebToApk.updateBackgroundService(JSON.stringify({
      title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
      progress: { value: Math.min(100, seconds / 18), max: 100 }
    }));
  }
}

function finishWorkout() {
  if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}

window.addEventListener('appmint:background-service', function (e) {
  // reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
  // 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
  // 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
  if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});
```

**Notes:** the type must be one the build declared (the one picked in the wizard, plus `location` when Background location is on). Swiping the app away stops a keep-running service unless you pass `keepAfterSwipe: true` (the page itself is gone then). Play Console asks for a foreground-service declaration with a short video for every declared type.

