JavaScript bridge API
Phone, SMS and the call log
Placing a call, sending or drafting a message, and reading history. Every one of these is permission-gated and Play asks about them.
composeSms bridge#
window.WebToApk.composeSms(phoneNumber: String, message: String): Boolean
Opens the user's SMS app with the message prefilled. Needs no permission and cannot be rejected by Play - the right default for anything non-automated. Returns false when the phone has no SMS app (or it could not be opened).
Example
Opens the user's SMS app with the number and the text already filled in. The user presses send.
Returns: true when the SMS app opened, false when the phone has no SMS app or it could not be opened (synchronous). Needs: nothing - no build switch, no permission.
A "Text us" button.
document.getElementById('textUs').addEventListener('click', function () {
if (window.WebToApk && window.WebToApk.composeSms) {
var ok = WebToApk.composeSms('+911234567890', 'Hello, I need help with my order');
if (!ok) showMessage('Could not open the SMS app.');
} else {
// In a normal browser the standard sms: link does the same job.
location.href = 'sms:+911234567890?body=' + encodeURIComponent('Hello, I need help with my order');
}
});Notes: Nothing is sent until the user taps send in their own SMS app, so this method itself needs no SMS permission. Use this for every message the user writes or approves; use sendSms only when the app really must send by itself. For AI-built apps and Google AI Studio imports the build scans your code; a composeSms( call does not tick SMS (read & send) - only sendSms and listSms do - so a "Text us" button adds no Play-restricted permission.
listCallLog bridge#
window.WebToApk.listCallLog(requestId: String, limit: Int, offset: Int, sinceMillis: String)
WebToApk.listCallLog(id, limit, offset, sinceMillis) Delivered as `appmint:calllog` / window.onAppMintCallLog.
Described by its group, Call log, rather than on its own.
Example
Reads the phone's call history, newest call first.
Returns: nothing now. The answer is the appmint:calllog event (and window.onAppMintCallLog) with detail = { requestId, calls: [{ number, name, type, date, duration }] } or { requestId, error }. date is milliseconds since 1970; duration is seconds. Needs: turn on Call log (read) in Step 4 (Access) when you build; the user is asked for permission on the first call.
Show the last 50 calls.
function loadCalls() {
if (!(window.WebToApk && window.WebToApk.listCallLog)) return;
var id = 'calls-' + Date.now();
window.addEventListener('appmint:calllog', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:calllog', handler);
if (e.detail.error) { showMessage('Call log: ' + e.detail.error); return; }
e.detail.calls.forEach(function (c) {
// c.type: incoming | outgoing | missed | voicemail | rejected | blocked | other
addRow((c.name || c.number) + ' - ' + c.type + ' - ' +
new Date(c.date).toLocaleString() + ' - ' + c.duration + 's');
});
});
WebToApk.listCallLog(id, 50, 0, '0'); // limit, offset, since (string)
}Only calls from the last 24 hours, using the page hook. sinceMillis is a STRING because the number is too big for the bridge's int.
window.onAppMintCallLog = function (detail) {
if (detail.requestId !== 'today') return;
if (detail.error) return;
document.getElementById('count').textContent = detail.calls.length + ' calls today';
};
if (window.WebToApk && window.WebToApk.listCallLog) {
WebToApk.listCallLog('today', 500, 0, String(Date.now() - 24 * 60 * 60 * 1000));
}Notes: limit is clamped to 1-2000. Errors: not_enabled, permission_denied, foreign_origin. Call log is a Google Play restricted permission: you must file an approved Permissions Declaration in Play Console, or the app is rejected.
listSms bridge#
window.WebToApk.listSms(requestId: String, box: String, limit: Int, offset: Int)
WebToApk.composeSms(number, message) <- NO permission, opens the SMS app WebToApk.sendSms(id, number, message, wantDelivery) WebToApk.listSms(id, "inbox"|"sent"|"draft", limit, offset) Send results: `appmint:sms` / window.onAppMintSms {requestId, status:"sent"|"delivered"|"failed", ok, reason?} Inbound messages: `appmint:sms-received` / window.onAppMintSmsReceived.
Described by its group, SMS, rather than on its own.
Example
Reads SMS messages from the phone's inbox, sent box or drafts, newest first. This file also shows how to receive new messages as they arrive.
Returns: nothing now. The answer is the appmint:sms event (and window.onAppMintSms) with detail = { requestId, messages: [{ id, address, body, date, read, box }] }. On failure: { requestId, status: 'failed', ok: false, reason }. Needs: turn on SMS (read & send) in Step 4 (Access) when you build; the user is asked for permission on the first call.
Read the latest 30 inbox messages.
function loadInbox() {
if (!(window.WebToApk && window.WebToApk.listSms)) return;
var id = 'inbox-' + Date.now();
window.addEventListener('appmint:sms', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:sms', handler);
if (e.detail.status === 'failed') { showMessage('Cannot read SMS: ' + e.detail.reason); return; }
e.detail.messages.forEach(function (m) {
addRow(m.address + ': ' + m.body + (m.read ? '' : ' (new)') +
' - ' + new Date(m.date).toLocaleString());
});
});
WebToApk.listSms(id, 'inbox', 30, 0); // box: 'inbox' | 'sent' | 'draft'
}New messages as they arrive (appmint:sms-received, also window.onAppMintSmsReceived). Messages that arrive while the app is closed are kept (the last 50) and given to your page when the app opens again.
window.addEventListener('appmint:sms-received', function (e) {
// e.detail = { address: '+91...', body: 'Your OTP is 4821', date: 1737000000000 }
var code = (e.detail.body.match(/\b\d{4,6}\b/) || [])[0];
if (code) document.getElementById('otp').value = code;
});
// or, as a page hook:
window.onAppMintSmsReceived = function (sms) {
console.log('SMS from ' + sms.address);
};Notes: limit is clamped to 1-500. Any unknown box name reads the inbox. reason values: not_enabled, permission_denied, foreign_origin. Received messages still reach the user's normal SMS app too. SMS is a Google Play restricted permission: you must file an approved Permissions Declaration in Play Console (usually only default SMS apps qualify), or the app is rejected.
makePhoneCall bridge#
window.WebToApk.makePhoneCall(phoneNumber: String)
Calls [phoneNumber]. - Phone permission ON in the build: places the call directly. The first time, Android asks the user for the Phone permission; if they refuse, the dialer opens with the number filled in so they can still place it themselves. - Phone permission OFF: opens the dialer with the number filled in (the dialer needs no permission) and the user taps Call. - A page that is not the app's own (an external site open in the app) always gets the dialer, never a direct call. A plain `tel:` link in your HTML also opens the dialer and is often enough.
Example
Starts a phone call to a number, or opens the dialer with the number filled in.
Returns: nothing. Needs: nothing to open the dialer. Turn on Phone in Step 4 (Access) when you build if the call should start directly.
A "Call the shop" button.
document.getElementById('callShop').addEventListener('click', function () {
if (window.WebToApk && window.WebToApk.makePhoneCall) {
WebToApk.makePhoneCall('+911234567890');
}
});No switch needed: a plain tel: link opens the dialer with the number, in the app and in a browser. This is often all you need.
<a href="tel:+911234567890">Call the shop</a>Notes: With Phone off, the dialer opens with the number and the user presses call. With Phone on, the call starts directly; the first time, Android asks the user for the phone-call permission, and if they refuse, the dialer opens instead so they can still place the call. A page that is not the app's own (an external site open inside the app) always gets the dialer. Pass the number as a plain string. Nothing is reported back - you cannot know if the call was placed. The Phone switch adds the CALL_PHONE permission to your Play listing; declare it in the Data safety form and only turn it on if you need it.
sendSms bridge#
window.WebToApk.sendSms(
WebToApk.composeSms(number, message) <- NO permission, opens the SMS app WebToApk.sendSms(id, number, message, wantDelivery) WebToApk.listSms(id, "inbox"|"sent"|"draft", limit, offset) Send results: `appmint:sms` / window.onAppMintSms {requestId, status:"sent"|"delivered"|"failed", ok, reason?} Inbound messages: `appmint:sms-received` / window.onAppMintSmsReceived.
Described by its group, SMS, rather than on its own.
Example
Sends an SMS directly from the app, without opening the SMS app, and reports what really happened.
Returns: nothing now. The answer is the appmint:sms event (and window.onAppMintSms) with detail = { requestId, status: 'sent' | 'delivered' | 'failed', ok, reason? }. With a delivery report you get TWO answers on success: sent, then delivered. Needs: turn on SMS (read & send) in Step 4 (Access) when you build; the user is asked for permission on the first call.
Send a code and follow its status. Do not remove the listener after the first answer when you asked for a delivery report.
function sendCode(phone, code) {
if (!(window.WebToApk && window.WebToApk.sendSms)) {
showMessage('Sending SMS works only inside the app.');
return;
}
var id = 'sms-' + Date.now();
function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
var d = e.detail;
if (d.status === 'sent' && d.reason === 'no_delivery_report') {
showStatus('Sent (the network gave no delivery report)');
window.removeEventListener('appmint:sms', handler);
} else if (d.status === 'sent') {
showStatus('Sent');
} else if (d.status === 'delivered') {
showStatus('Delivered');
window.removeEventListener('appmint:sms', handler);
} else { // 'failed'
showStatus('Failed: ' + d.reason);
window.removeEventListener('appmint:sms', handler);
}
}
window.addEventListener('appmint:sms', handler);
WebToApk.sendSms(id, phone, 'Your code is ' + code, true); // true = delivery report
}No delivery report. Pass false as the last value: you get exactly one answer, sent or failed.
window.onAppMintSms = function (d) {
if (d.requestId === 'alert-1') showStatus(d.ok ? 'Alert sent' : 'Failed: ' + d.reason);
};
if (window.WebToApk && window.WebToApk.sendSms) {
WebToApk.sendSms('alert-1', '+911234567890', 'Gate opened at ' + new Date().toLocaleTimeString(), false);
}Notes: Long text is split into parts and sent as one message. If no delivery report arrives within 60 seconds you get status: 'sent' with reason: 'no_delivery_report' - the message was still sent. reason values: not_enabled, permission_denied, foreign_origin, invalid_arguments (empty number or text), no_service, radio_off, generic_failure, null_pdu, not_delivered, and others. Use a string requestId. Each SMS may cost the user money. SMS is a Google Play restricted permission: you must file an approved Permissions Declaration in Play Console (usually only default SMS apps qualify), so prefer composeSms when the user can press send.