Create Free APK

JavaScript bridge API

NFC

Reading a tag through window.AndroidNFC, and the write side behind the NDEFReader polyfill.

NDEFReader web standard#

new NDEFReader() — Web NFC

Example

Standard Web NFC: new NDEFReader() reads, writes and locks NFC tags in the installed app, with the same code Chrome on Android accepts.

Returns: Promises and reading / readingerror events, as in the web standard. Records are NDEFRecords: recordType ('text', 'url', 'absolute-url', 'mime', 'smart-poster', 'empty', 'unknown' or an external type such as 'example.com:thing'), mediaType (mime records), id, encoding and lang (text records) and data as a DataView. Needs: turn on Web NFC Support in Step 4 (Access) when you build. Without it NDEFReader does not exist. No runtime permission prompt; navigator.permissions.query({name:'nfc'}) answers granted.

Read tags. Decode data with TextDecoder, exactly as in Chrome:

function describe(record) {
  if (record.recordType === 'text') return new TextDecoder(record.encoding).decode(record.data);
  if (record.recordType === 'url' || record.recordType === 'absolute-url') return new TextDecoder().decode(record.data);
  if (record.recordType === 'mime') return record.mediaType + ' (' + record.data.byteLength + ' bytes)';
  return record.recordType;
}

var scanStop = null;

document.getElementById('read').addEventListener('click', async function () {
  if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
  var reader = new NDEFReader();
  scanStop = new AbortController();
  reader.onreading = function (e) {
    showStatus('Tag ' + e.serialNumber + ': ' + e.message.records.map(describe).join(' | '));
  };
  reader.onreadingerror = function () { showStatus('That tag holds no NFC data this phone can read.'); };
  try {
    await reader.scan({ signal: scanStop.signal });
    showStatus('Hold a tag to the back of the phone.');
  } catch (err) {
    showStatus('NFC error: ' + err.name + ' ' + err.message);   // NotReadableError = NFC switched off
  }
});

document.getElementById('stop').addEventListener('click', function () {
  if (scanStop) scanStop.abort();   // the standard way to stop scanning
});

Write text or a link. The Promise resolves after the user taps a tag:

async function writeTag(kind, value) {
  if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
  var reader = new NDEFReader();
  showStatus('Tap a tag to write it...');
  try {
    if (kind === 'text') await reader.write(value);   // same as { records: [{ recordType: 'text', data: value }] }
    else await reader.write({ records: [{ recordType: 'url', data: value }] }, { overwrite: false });
    showStatus('Tag written.');
  } catch (err) {
    showStatus('Write failed: ' + err.name + ' ' + err.message);   // NotAllowedError: read-only, or overwrite:false on a used tag
  }
}

Lock a tag for ever. makeReadOnly() cannot be undone - ask first:

async function lockTag() {
  if (!('NDEFReader' in window)) return;
  if (!confirm('Lock the next tag you tap? It can never be changed again.')) return;
  try { await new NDEFReader().makeReadOnly(); showStatus('Tag locked.'); }
  catch (err) { showStatus('Lock failed: ' + err.message); }
}

Notes: Record types for write: text (string, or bytes with encoding), url and absolute-url (string), mime (bytes + mediaType), smart-poster ({records: [...]}), an external type 'domain:thing' (bytes or {records: [...]}), unknown (bytes) and empty. A plain string writes a text record; plain bytes write an application/octet-stream mime record. Only readers that called scan() get reading events; AbortController.abort() on the signal stops that reader (there is no stop() in the standard). readingerror fires for a tag that cannot hold NFC data. The phone waits for ONE tag operation at a time: a new write() or makeReadOnly() while one waits replaces it, and the replaced call rejects with AbortError (Chrome would queue it). A signal on write() / makeReadOnly() cancels the wait. Errors: NotSupportedError (no NFC, or the tag cannot hold NDEF, or it is too small), NotReadableError (NFC switched off), NotAllowedError (read-only tag, or overwrite: false on a tag with records), NetworkError (the tag moved away), TypeError (a record the standard does not allow). NDEFReader exists in the main page, not inside iframes.

__nfcCancelWrite bridge#

window.WebToApk.__nfcCancelWrite()

Cancels the waiting write/lock (the polyfill calls it when the operation's `signal` aborts).

Example

Drops a queued NFC write or lock so the next tag tap is read normally. Pages cancel through the web standard - an AbortController signal - and the NDEFReader polyfill calls this for them.

Returns: nothing. Needs: Web NFC Support turned on in Step 4 (Access) when you build.

The standard way, which the app honours - the waiting write() rejects with AbortError:

var controller = null;

async function writeWithCancel(text) {
  if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
  controller = new AbortController();
  showStatus('Tap a tag to write it... (or press Cancel)');
  try {
    await new NDEFReader().write(text, { signal: controller.signal });
    showStatus('Tag written.');
  } catch (err) {
    showStatus(err.name === 'AbortError' ? 'Cancelled.' : 'Write failed: ' + err.message);
  }
}

document.getElementById('cancel').addEventListener('click', function () {
  if (controller) controller.abort();
});

Notes: Do not call WebToApk.__nfcCancelWrite() yourself: called directly it drops the native wait but cannot settle the page's Promise. The signal does both. Cancelling also drops a queued makeReadOnly().

__nfcMakeReadOnly bridge#

window.WebToApk.__nfcMakeReadOnly(): String

Queues a permanent lock (`NDEFReader.makeReadOnly()`) for the next tag tap; returns its op id, "" when NFC is off in this app.

Example

Internal transport behind NDEFReader.makeReadOnly(). Pages use new NDEFReader().makeReadOnly(); never call this directly.

Returns: (through makeReadOnly) a Promise that resolves after the user taps a tag and it is locked, or rejects with a DOMException. Needs: Web NFC Support turned on in Step 4 (Access) when you build.

Locking is permanent. Write the tag first, then lock it, and ask the user before both:

document.getElementById('publish-tag').addEventListener('click', async function () {
  if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
  if (!confirm('Write and lock this tag? A locked tag can never be changed.')) return;
  var reader = new NDEFReader();
  try {
    showStatus('Tap the tag to write it...');
    await reader.write({ records: [{ recordType: 'url', data: 'https://example.com/item/42' }] });
    showStatus('Now tap the same tag again to lock it...');
    await reader.makeReadOnly();
    showStatus('Tag written and locked.');
  } catch (err) {
    showStatus('Failed: ' + err.message);
  }
});

Notes: Each step needs its own tap. The call returns an operation id and the result arrives through window.__appmintNfcDone. Rejections: NotSupportedError "Tag does not support NDEF", NotAllowedError "This tag cannot be made read-only" (many tags cannot be locked), NetworkError when the tag moves away. Starting a write() while a lock waits replaces the lock, which then rejects with AbortError. A signal passed to makeReadOnly({ signal }) cancels the wait.

__nfcState bridge#

window.WebToApk.__nfcState(): String

"on", "off" (NFC switched off in the phone's settings) or "none" (no NFC, or not enabled in this app) - what `NDEFReader.scan()` checks first.

Example

Internal check behind NDEFReader.scan(): whether this app can use NFC right now. Pages call scan() and read its rejection instead.

Returns: 'on', 'off' (NFC is switched off in the phone's settings) or 'none' (the phone has no NFC, or the app was built without Web NFC Support). Needs: Web NFC Support turned on in Step 4 (Access) when you build.

The standard way - scan() rejects with the same answer:

async function startReading() {
  if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
  try {
    await new NDEFReader().scan();
    showStatus('Hold a tag to the back of the phone.');
  } catch (err) {
    if (err.name === 'NotReadableError') showStatus('Turn on NFC in the phone settings, then try again.');
    else if (err.name === 'NotSupportedError') showStatus('This phone has no NFC.');
    else showStatus('NFC error: ' + err.message);
  }
}

Notes: Plumbing for the polyfill; use scan(). window.AndroidNFC.startScan() answers the same question as a boolean and shows a "Please enable NFC" message when NFC is off.

__nfcWrite bridge#

window.WebToApk.__nfcWrite(messageJson: String): String

Queues [messageJson] (`{records:[…], overwrite}`, built by `NDEFReader.write()`) for the next tag tap and returns its op id; "" when NFC is not enabled in this app.

Example

Internal transport behind NDEFReader.write(message). Pages use new NDEFReader().write(...); never call this directly.

Returns: (through write) a Promise that resolves after the user taps a tag and it is written, or rejects with a DOMException. Needs: Web NFC Support turned on in Step 4 (Access) when you build.

document.getElementById('write-table-tag').addEventListener('click', async function () {
  if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
  showStatus('Tap a tag to write it...');
  try {
    await new NDEFReader().write({
      records: [
        { recordType: 'url', data: 'https://example.com/menu' },
        { recordType: 'text', data: 'Table 12', lang: 'en' },
        { recordType: 'mime', mediaType: 'application/json',
          data: new TextEncoder().encode(JSON.stringify({ table: 12 })) }
      ]
    });
    showStatus('Tag written.');
  } catch (err) {
    showStatus('Write failed: ' + err.message);
  }
});

Notes: The polyfill checks the records, turns them into {records:[{recordType, id, data(base64), encoding, lang, mediaType, records?}], overwrite} and passes the JSON string here. The call returns an operation id; the result comes back to the page through window.__appmintNfcDone(id, errorName, message), so an answer for a replaced write never settles a newer one. Unformatted tags are formatted first. Rejections: NotAllowedError ("Tag is read-only", or a tag with records when overwrite is false), NotSupportedError ("Tag does not support NDEF", "Tag too small (N bytes available)"), NetworkError (the tag moved away), SyntaxError (a record the tag format cannot hold). The app also shows its own short "Ready to write" and "NFC tag written" messages.

startScan bridge#

window.AndroidNFC.startScan(): Boolean

Legacy availability probe, present only when the creator enabled NFC. Real NFC work goes through the standard NDEFReader polyfill (window.WebToApk.__nfc*), which is what a page should use; this only reports whether the adapter is on.

Described by its group, Web NFC, rather than on its own.

Example

Tells you whether the phone's NFC is switched on. It lives on window.AndroidNFC, not on window.WebToApk, and it does not read a tag - reading is done with NDEFReader.

Returns: true (synchronously) when the phone has NFC and it is on; false when the phone has no NFC, or when NFC is off (then the app also shows a short "Please enable NFC" message). Needs: Web NFC Support turned on in Step 4 (Access) when you build; without it window.AndroidNFC does not exist.

Check before showing an NFC button:

function nfcReady() {
  if (!window.AndroidNFC || typeof window.AndroidNFC.startScan !== 'function') return false;
  return window.AndroidNFC.startScan();
}

document.getElementById('scan-tag').addEventListener('click', async function () {
  if (!nfcReady()) { showStatus('Turn on NFC in the phone settings, then try again.'); return; }
  var reader = new NDEFReader();
  reader.addEventListener('reading', function (e) { showStatus('Tag ' + e.serialNumber); });
  await reader.scan();
  showStatus('Hold a tag to the back of the phone.');
});

Notes: window.AndroidNFC exists from the first script on the page. After the user turns NFC on in settings and comes back, tag reading starts by itself.

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.