Create Free APK

JavaScript bridge API

Contacts

Reading, searching, picking and writing - the picker needs no permission at all.

navigator.contacts web standard#

navigator.contacts.select(properties, options) — Contact Picker

Example

The standard Contact Picker API: the phone's own contact picker opens, and the page gets only the person the user chose - no contacts permission.

Returns: a Promise of an array: [{ name: [string], tel: [string] }], or [] when the user closes the picker. Needs: nothing to switch on; call it from a tap (a user gesture is required, else SecurityError).

document.getElementById('pick').addEventListener('click', async () => {
  if (!('contacts' in navigator)) { showManualEntry(); return; }
  const props = await navigator.contacts.getProperties();   // ['name', 'tel']
  const [person] = await navigator.contacts.select(props, { multiple: false });
  if (!person) return;                                       // closed without choosing
  document.getElementById('name').value = person.name[0] || '';
  document.getElementById('phone').value = person.tel[0] || '';
});

Notes: the app supports name and tel - the number the user tapped. email, address and icon would need the full contacts permission, so they are not offered and asking for them rejects with TypeError. One person per pick (multiple is allowed but the Android picker returns one). Works in the top page only. For listing or searching all contacts use WebToApk.listContacts (needs the Contacts switch).

addContact bridge#

window.WebToApk.addContact(requestId: String, contactJson: String)

The system picker (no permission needed) and writing a new contact (WRITE_CONTACTS). The read side - listing and searching - is under the Contacts group above.

Described by its group, Contacts: pick and add, rather than on its own.

Example

Saves a new contact straight into the phone's address book, without opening the Contacts app.

Returns: nothing now. The answer is the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, ok: true, id: '123' } on success, { requestId, ok: false, id: null } when the save failed, or { requestId, error }. Needs: turn on Edit Contacts in Step 4 (Access) when you build; the user is asked for permission on the first call.

Save a customer. The contact is passed as a JSON string.

function saveCustomer(name, phone, email) {
  if (!(window.WebToApk && window.WebToApk.addContact)) return;
  var id = 'add-' + Date.now();

  window.addEventListener('appmint:contacts', function handler(e) {
    if (!e.detail || e.detail.requestId !== id) return;
    window.removeEventListener('appmint:contacts', handler);

    if (e.detail.error === 'not_enabled') { showMessage('Turn on Edit Contacts when you build'); return; }
    if (e.detail.error) { showMessage('Not saved: ' + e.detail.error); return; }
    showMessage(e.detail.ok ? 'Saved to contacts' : 'Could not save the contact');
  });

  WebToApk.addContact(id, JSON.stringify({
    displayName: name,
    phones: [{ number: phone, type: 'mobile' }],   // type: mobile | home | work
    emails: [email]
  }));
}

Short form. Single values are accepted too: { name: 'Ali', phone: '+911234567890', email: '[email protected]' }.

Notes: Errors: not_enabled, permission_denied. The returned id is the new raw-contact id. A phone type other than home or work is saved as mobile. The contact is saved on the device (no account). Writing contacts is a sensitive permission: your Play Store listing must declare it in the Data safety form and privacy policy.

getContact bridge#

window.WebToApk.getContact(requestId: String, contactId: String)

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO permission needed (system picker) WebToApk.addContact(id, json) <- needs the Contacts-write permission

Described by its group, Contacts, rather than on its own.

Example

Reads one contact again by its id (the id from listContacts or searchContacts).

Returns: nothing now. The answer is the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type }], emails: [...] } }. contact is null when that contact no longer exists. On failure: { requestId, error }. Needs: turn on Contacts in Step 4 (Access) when you build.

Refresh a saved contact using the page hook instead of an event listener.

var pendingId = '';

window.onAppMintContacts = function (detail) {
  if (detail.requestId !== pendingId) return;
  if (detail.error) { showMessage('Could not read the contact: ' + detail.error); return; }
  if (!detail.contact) { showMessage('This contact was deleted.'); return; }

  var c = detail.contact;
  document.getElementById('name').textContent = c.displayName;
  document.getElementById('phones').textContent =
    c.phones.map(function (p) { return p.type + ': ' + p.number; }).join(', ');
  document.getElementById('emails').textContent = c.emails.join(', ');
};

function openContact(contactId) {
  if (!(window.WebToApk && window.WebToApk.getContact)) return;
  pendingId = 'one-' + Date.now();
  WebToApk.getContact(pendingId, String(contactId));
}

Notes: window.onAppMintContacts receives every contacts answer (list, search, pick, add), so always check requestId. You can use it or addEventListener('appmint:contacts', ...) - both fire. Errors: not_enabled, permission_denied, foreign_origin. The id from pickContact is a phone-row id, not a contact id, so it does not work here. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.

listContacts bridge#

window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO permission needed (system picker) WebToApk.addContact(id, json) <- needs the Contacts-write permission

Described by its group, Contacts, rather than on its own.

Example

Reads the phone's address book, page by page, sorted A to Z by name.

Returns: nothing now. The answer comes later as the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, contacts: [{ id, displayName, photoUri, phones: [{ number, type }], emails: ['[email protected]'] }] }, or { requestId, error }. Needs: turn on Contacts in Step 4 (Access) when you build; the user is asked for permission on the first call.

Load the first 100 contacts. Match the answer on your own requestId.

function loadContacts() {
  if (!(window.WebToApk && window.WebToApk.listContacts)) {
    showMessage('Contacts work only inside the app.');
    return;
  }
  var id = 'contacts-' + Date.now();

  window.addEventListener('appmint:contacts', function handler(e) {
    if (!e.detail || e.detail.requestId !== id) return;   // another call's answer
    window.removeEventListener('appmint:contacts', handler);

    if (e.detail.error) { showMessage('Could not read contacts: ' + e.detail.error); return; }

    var list = document.getElementById('list');
    list.innerHTML = '';
    e.detail.contacts.forEach(function (c) {
      var li = document.createElement('li');
      var num = c.phones.length ? c.phones[0].number + ' (' + c.phones[0].type + ')' : '';
      li.textContent = c.displayName + ' ' + num;
      list.appendChild(li);
    });
  });

  WebToApk.listContacts(id, 100, 0);   // limit 100, start at 0
}

Next page. Pass the number you already have as offset: WebToApk.listContacts(id, 100, 100).

Notes: limit is clamped to 1-2000. Errors: not_enabled (Contacts was off at build time), permission_denied (the user said No), foreign_origin (called from a page that is not your app's own). Phone type is home, mobile, work, main, work_fax, home_fax or other. photoUri is a content:// address, not an image URL a page can load. To let the user choose just one person, use pickContact instead - it needs no permission. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.

pickContact bridge#

window.WebToApk.pickContact(requestId: String)

System contact picker - needs NO permission, and is the right choice whenever the page just wants the user to choose one person. Picks from the PHONE table, not the contact table: the picker grants read access only to the row it returns, so picking a phone row is what makes the number readable without READ_CONTACTS. Picking a bare contact would return a row with a name and no number, and reading the number from it would need the permission this method exists to avoid.

Example

Opens Android's own contact picker so the user chooses one phone number. No permission is needed.

Returns: nothing now. The answer is the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type: 'picked' }], emails: [] } }. If the user closes the picker: { requestId, error: 'cancelled' }. Needs: nothing - no build switch, no permission prompt.

Fill a form with the chosen person.

function chooseContact() {
  if (!(window.WebToApk && window.WebToApk.pickContact)) {
    showMessage('Open this page in the app to pick a contact.');
    return;
  }
  var id = 'pick-' + Date.now();

  window.addEventListener('appmint:contacts', function handler(e) {
    if (!e.detail || e.detail.requestId !== id) return;
    window.removeEventListener('appmint:contacts', handler);

    if (e.detail.error === 'cancelled') return;              // user closed the picker
    if (e.detail.error) { showMessage('Could not open contacts'); return; }
    if (!e.detail.contact) { showMessage('Could not read that contact'); return; }

    var c = e.detail.contact;
    document.getElementById('name').value = c.displayName;
    document.getElementById('phone').value = c.phones.length ? c.phones[0].number : '';
  });

  WebToApk.pickContact(id);
}

Notes: You get the name and the ONE number the user tapped - never emails, never the rest of the book. emails is always empty. Errors: cancelled, no_picker (no contacts app on the phone). If the read fails after a pick, contact is null. Because nothing is read without the user's choice, this is the Play-friendly way to get a phone number; prefer it over listContacts when you only need one person. For AI-built apps and Google AI Studio imports the build scans your code; a pickContact( call does not tick Contacts - only listContacts, searchContacts and getContact do - so the picker adds no contacts-read permission.

searchContacts bridge#

window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO permission needed (system picker) WebToApk.addContact(id, json) <- needs the Contacts-write permission

Described by its group, Contacts, rather than on its own.

Example

Finds contacts whose name matches a search word, without loading the whole address book.

Returns: nothing now. The answer is the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, contacts: [...] } - the same contact shape as listContacts - or { requestId, error }. Needs: turn on Contacts in Step 4 (Access) when you build; the user is asked for permission on the first call.

Search as the user types. Only the answer to the latest search is shown.

var lastSearchId = '';

function searchPeople(word) {
  if (!(window.WebToApk && window.WebToApk.searchContacts)) return;
  lastSearchId = 'search-' + Date.now();
  WebToApk.searchContacts(lastSearchId, word, 20);   // at most 20 results
}

window.addEventListener('appmint:contacts', function (e) {
  if (!e.detail || e.detail.requestId !== lastSearchId) return;   // old or other request
  if (e.detail.error) { showMessage('Search failed: ' + e.detail.error); return; }

  var names = e.detail.contacts.map(function (c) {
    return c.displayName + (c.phones.length ? ' - ' + c.phones[0].number : '');
  });
  document.getElementById('results').textContent = names.join('\n') || 'No match';
});

document.getElementById('q').addEventListener('input', function (e) {
  searchPeople(e.target.value);
});

Notes: An empty search word returns contacts from the start of the list (like listContacts). limit is clamped to 1-2000 and there is no offset. Errors: not_enabled, permission_denied, foreign_origin. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.

Generated from the app runtime and its example files on every docs build. Read it as Markdown · All families.

Checked against the shipped bridge on 2026-09-23.