# Contacts - JavaScript bridge API

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

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

### `navigator.contacts`

```js
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`).

```js
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] || '';
});
```

Fill a "Send to" box with only the number, and show a message for each way the picker can refuse:

```js
document.getElementById('choose-number').addEventListener('click', async () => {
  if (!('contacts' in navigator)) return;
  try {
    const [person] = await navigator.contacts.select(['tel']);
    if (person && person.tel.length) document.getElementById('to').value = person.tel[0];
  } catch (err) {
    if (err.name === 'InvalidStateError') showMessage('The contact picker is already open or not available.');
    else if (err.name === 'SecurityError') showMessage('Tap the button to choose a contact.');
    else showMessage('Could not pick a contact: ' + err.message);
  }
});
```

Build a guest list one tap at a time (one person per pick), skipping a number already on the list:

```js
const guests = [];

document.getElementById('add-guest').addEventListener('click', async () => {
  if (!('contacts' in navigator)) { showManualEntry(); return; }
  const [person] = await navigator.contacts.select(['name', 'tel']);
  if (!person || !person.tel.length) return;   // closed, or no number

  const number = person.tel[0];
  if (guests.some((g) => g.number === number)) { showMessage('Already invited'); return; }
  guests.push({ name: person.name[0] || number, number });
  renderGuests(guests);
});
```

**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`

```js
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.

```js
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: 'ali@example.com' }`.

A "Save to contacts" button on a business card page, using the short form and a Promise:

```js
function addOne(contact) {
  return new Promise(function (resolve, reject) {
    var id = 'card-' + Date.now() + '-' + Math.random().toString(36).slice(2);
    window.addEventListener('appmint:contacts', function handler(e) {
      if (!e.detail || e.detail.requestId !== id) return;
      window.removeEventListener('appmint:contacts', handler);
      if (e.detail.error) reject(new Error(e.detail.error));
      else if (!e.detail.ok) reject(new Error('save_failed'));
      else resolve(e.detail.id);
    });
    WebToApk.addContact(id, JSON.stringify(contact));
  });
}

document.getElementById('save-card').addEventListener('click', function () {
  if (!(window.WebToApk && window.WebToApk.addContact)) return;
  addOne({ name: 'Priya Sharma', phone: '+919876543210', email: 'priya@example.com' })
    .then(function () { showMessage('Priya is in your contacts now'); })
    .catch(function (err) {
      showMessage(err.message === 'permission_denied' ? 'Allow Contacts to save this card.' : 'Not saved: ' + err.message);
    });
});
```

Import a team list one person at a time, with work and home numbers, and report how many were saved:

```js
async function importTeam(team) {   // [{ name, work, home, email }]
  if (!(window.WebToApk && window.WebToApk.addContact)) return;
  var saved = 0;
  for (var i = 0; i < team.length; i++) {
    var m = team[i];
    try {
      await addOne({   // addOne() from the example above
        displayName: m.name,
        phones: [{ number: m.work, type: 'work' }, { number: m.home, type: 'home' }],
        emails: m.email ? [m.email] : []
      });
      saved++;
    } catch (err) {
      if (err.message === 'not_enabled' || err.message === 'permission_denied') break;   // the rest would fail too
    }
  }
  showMessage(saved + ' of ' + team.length + ' saved');
}
```

**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`

```js
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.

```js
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));
}
```

Refresh a saved list of favourite contact ids and drop the ones deleted from the phone since:

```js
function getOne(contactId) {
  return new Promise(function (resolve, reject) {
    var id = 'fav-' + contactId + '-' + 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) reject(new Error(e.detail.error)); else resolve(e.detail.contact);
    });
    WebToApk.getContact(id, String(contactId));
  });
}

async function loadFavourites() {
  if (!(window.WebToApk && window.WebToApk.getContact)) return;
  var ids = JSON.parse(localStorage.getItem('favIds') || '[]');
  var people = [];
  for (var i = 0; i < ids.length; i++) {
    var c = await getOne(ids[i]);
    if (c) people.push(c);   // null = deleted from the phone
  }
  localStorage.setItem('favIds', JSON.stringify(people.map(function (c) { return c.id; })));
  renderFavourites(people);
}
```

Check whether the number your app stored for a customer is still on their contact card:

```js
function checkStoredNumber(customer) {   // { contactId, phone }
  if (!(window.WebToApk && window.WebToApk.getContact)) return;
  var id = 'check-' + 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) { showMessage('Could not check: ' + e.detail.error); return; }
    if (!e.detail.contact) { showMessage('This customer is no longer in your contacts.'); return; }

    var digits = function (s) { return String(s).replace(/\D/g, ''); };
    var still = e.detail.contact.phones.some(function (p) {
      return digits(p.number).slice(-10) === digits(customer.phone).slice(-10);
    });
    if (!still) showMessage('The number for ' + e.detail.contact.displayName + ' has changed.');
  });

  WebToApk.getContact(id, String(customer.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`

```js
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: ['a@b.com'] }] }`, 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`.

```js
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)`.

Load the whole address book in pages of 500 until a short page says you reached the end:

```js
function listPage(limit, offset) {
  return new Promise(function (resolve, reject) {
    var id = 'page-' + offset + '-' + 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) reject(new Error(e.detail.error));
      else resolve(e.detail.contacts);
    });
    WebToApk.listContacts(id, limit, offset);
  });
}

async function loadEveryContact() {
  if (!(window.WebToApk && window.WebToApk.listContacts)) return [];
  var all = [];
  while (true) {
    var page = await listPage(500, all.length);
    all = all.concat(page);
    if (page.length < 500) return all;   // last page
  }
}
```

Build an "Invite friends" list from mobile numbers only, and tell the user what to do when reading is refused:

```js
function showInviteList() {
  if (!(window.WebToApk && window.WebToApk.listContacts)) return;
  var id = 'invite-' + 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 === 'permission_denied') { showMessage('Allow Contacts to invite friends.'); return; }
    if (e.detail.error) { showMessage('Contacts are not available: ' + e.detail.error); return; }

    var mobiles = [];
    e.detail.contacts.forEach(function (c) {
      c.phones.forEach(function (p) {
        if (p.type === 'mobile') mobiles.push({ name: c.displayName, number: p.number });
      });
    });
    renderInviteList(mobiles);   // your own list UI
  });

  WebToApk.listContacts(id, 500, 0);
}
```

**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`

```js
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.**

```js
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);
}
```

Keep a short list of emergency contacts. Save the name and number, not the `id`, so the list does not depend on the address book:

```js
function pickOne() {
  return new Promise(function (resolve, reject) {
    var id = 'sos-' + 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') resolve(null);
      else if (e.detail.error) reject(new Error(e.detail.error));
      else resolve(e.detail.contact);
    });
    WebToApk.pickContact(id);
  });
}

document.getElementById('add-sos').addEventListener('click', async function () {
  if (!(window.WebToApk && window.WebToApk.pickContact)) return;
  var c = await pickOne();
  if (!c || !c.phones.length) return;   // closed, or nothing readable
  var list = JSON.parse(localStorage.getItem('sos') || '[]');
  list.push({ name: c.displayName, number: c.phones[0].number });
  localStorage.setItem('sos', JSON.stringify(list));
  renderSosList(list);
});
```

Disable the button while the picker is open, and show the typing fields when the phone has no contacts app:

```js
var pickBtn = document.getElementById('pick');

pickBtn.addEventListener('click', function () {
  if (!(window.WebToApk && window.WebToApk.pickContact)) { showManualEntry(); return; }
  var id = 'recipient-' + Date.now();
  pickBtn.disabled = true;   // one picker at a time

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

    if (e.detail.error === 'no_picker') { showManualEntry(); return; }
    if (e.detail.error) return;   // 'cancelled'
    if (e.detail.contact && e.detail.contact.phones.length) {
      document.getElementById('to').value = e.detail.contact.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. The build scans your code, and a `pickContact(` call does not switch on **Contacts** - only `listContacts`, `searchContacts` and `getContact` do - so the picker adds no contacts-read permission.

### `searchContacts`

```js
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.

```js
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);
});
```

Wrap the search in a Promise and wait until the user stops typing, so fast typing does not start a search per key:

```js
function searchOnce(word, limit) {
  return new Promise(function (resolve, reject) {
    var id = 'find-' + Date.now() + '-' + Math.random().toString(36).slice(2);
    window.addEventListener('appmint:contacts', function handler(e) {
      if (!e.detail || e.detail.requestId !== id) return;
      window.removeEventListener('appmint:contacts', handler);
      if (e.detail.error) reject(new Error(e.detail.error)); else resolve(e.detail.contacts);
    });
    WebToApk.searchContacts(id, word, limit);
  });
}

var timer = null;
document.getElementById('q').addEventListener('input', function (e) {
  clearTimeout(timer);
  var word = e.target.value.trim();
  timer = setTimeout(function () {
    if (!(window.WebToApk && window.WebToApk.searchContacts) || !word) return;
    searchOnce(word, 10)
      .then(function (people) { renderSuggestions(people); })
      .catch(function (err) { showMessage('Search failed: ' + err.message); });
  }, 300);
});
```

Fill the email box from the address book when the typed name matches one person who has an email:

```js
function suggestEmail(name) {
  if (!(window.WebToApk && window.WebToApk.searchContacts) || name.length < 2) return;
  var id = 'email-' + 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) return;   // leave the box for the user to type

    var withEmail = e.detail.contacts.filter(function (c) { return c.emails.length > 0; });
    if (withEmail.length === 1) {
      document.getElementById('email').value = withEmail[0].emails[0];
    }
  });

  WebToApk.searchContacts(id, name, 5);
}

document.getElementById('name').addEventListener('change', function (e) {
  suggestEmail(e.target.value.trim());
});
```

**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.

