WTT AirROI Estimator
Estimate report
What you get in the full report
A clearer picture of earning potential, the market conditions behind it, and the performance levels we would aim to build toward.
Full report value
Your estimate includes the headline numbers plus the market signals behind them, so the report explains both the income potential and what is driving it.
Comparable listings See how similar properties are performing
Historical trends Occupancy, ADR, and revenue movement
Performance scenarios Understand what stronger execution can achieve
How it works
Estimator flow
1 Enter your details.
2 Choose an address to check potential earnings.
3 Press calculate estimate and wait about a minute.
4 Review your findings in the pop-up.
5 Print a copy so you have one.
×
Estimate ready
Your Airbnb revenue estimate
Built from the AirROI estimate with local market context, seasonal demand, pacing, and comparable listings where available.
Average revenue / month —
Average occupancy —
Yearly revenue —
Average daily rate —
Mission
This is the performance mission
Even though the figures below reflect the market average, this is the mission we will aim for with your property. If you follow our systems, invest in the presentation, and execute the right revenue strategy, these are the performance levels we will be working toward.
Modeled on a target occupancy of 90% with stronger pricing.
These scenario figures are aspirational estimates only. Results depend on the market, seasonality, location, property type, presentation, amenities, reviews, pricing, local competition, regulation, and wider economic conditions. They also depend on your willingness to invest in the property and create a space guests genuinely want to book. If a property is simply handed over without upgrades, styling, maintenance, or effort, performance will usually reflect that.
Seasonality
Expected monthly revenue pattern
Percentiles
Market position ranges
Historical trends
Look back across key metrics
Comparables
Comparable listings
Market snapshot
Property and market summary
Currency —
RevPAR —
Active listings —
Booking lead time —
Average length of stay —
Resolved market —
Forward pacing
Upcoming booking pace
Print now
Close
';
win.document.open();
win.document.write(html);
win.document.close();
win.document.body.appendChild(bodyClone);
win.focus();
setTimeout(function(){ win.print(); }, 250);
}
if (modal) {
modal.querySelector('.wtt-modal-close').addEventListener('click', closeModal);
modal.querySelector('.wtt-btn-close').addEventListener('click', closeModal);
modal.querySelector('.wtt-modal-backdrop').addEventListener('click', closeModal);
modal.querySelector('.wtt-btn-print').addEventListener('click', openPrintWindow);
document.addEventListener('keydown', function(event){ if (event.key === 'Escape') closeModal(); });
}
function setManualLatLngMode(enabled){
var latEl = qs('lat');
var lngEl = qs('lng');
if (!latEl || !lngEl) return;
latEl.readOnly = !enabled;
lngEl.readOnly = !enabled;
if (enabled) {
latEl.removeAttribute('readonly');
lngEl.removeAttribute('readonly');
} else {
latEl.setAttribute('readonly', 'readonly');
lngEl.setAttribute('readonly', 'readonly');
}
}
function hasUsableLatLng(fields){
var lat = fields.lat !== '' ? Number(fields.lat) : null;
var lng = fields.lng !== '' ? Number(fields.lng) : null;
return Number.isFinite(lat) && Number.isFinite(lng);
}
function fillAddress(entry){
qs('address_search').value = entry.display_name || '';
qs('address_line').value = entry.address_line || '';
qs('suburb').value = entry.suburb || '';
qs('state').value = entry.state || '';
qs('postcode').value = entry.postcode || '';
qs('country').value = entry.country || '';
qs('lat').value = entry.lat != null ? String(entry.lat) : '';
qs('lng').value = entry.lng != null ? String(entry.lng) : '';
suggestions.innerHTML = '';
suggestions.hidden = true;
}
function geocodeSearchDirect(query){
var params = { format:'jsonv2', limit:Math.max(1, Math.min(8, Number(config.addressLimit) || 5)), addressdetails:1, q:query };
if (config.countryBias) params.countrycodes = config.countryBias;
var url = 'https://nominatim.openstreetmap.org/search?' + buildQuery(params);
return fetchJson(url, { headers:{ Accept:'application/json' } }, 15000, 'Failed while waiting for the address to load. Please try again.').then(function(results){
return Array.isArray(results) ? results.map(normalizeAddressResult).filter(function(entry){ return Number.isFinite(entry.lat) && Number.isFinite(entry.lng); }) : [];
});
}
function geocodeSearchProxy(query){
var body = new URLSearchParams();
body.set('action', 'wtt_airroi_address_search');
body.set('nonce', config.nonce || '');
body.set('query', query);
return fetchJson(config.ajaxUrl, {
method:'POST',
headers:{ 'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8' },
body:body.toString(),
credentials:'same-origin'
}, 20000, 'Failed while waiting for the address to load. Please try again.').then(function(resp){
if (!resp || !resp.success || !resp.data) throw new Error('Address search failed. Please try again.');
return Array.isArray(resp.data.entries) ? resp.data.entries : [];
});
}
var doAddressSearch = debounce(function(){
var query = (qs('address_search').value || '').trim();
if (query.length < 3) {
suggestions.innerHTML = '';
suggestions.hidden = true;
return;
}
var searchPromise = config.mode === 'proxy' ? geocodeSearchProxy(query) : geocodeSearchDirect(query);
searchPromise.then(function(entries){
if (!entries.length) {
suggestions.innerHTML = '
No matching addresses found.
';
suggestions.hidden = false;
suggestions._entries = [];
return;
}
suggestions._entries = entries;
suggestions.innerHTML = entries.map(function(entry, i){
return '
' + esc(entry.display_name || '') + ' ';
}).join('');
suggestions.hidden = false;
}).catch(function(){
suggestions.innerHTML = '
Address search failed. Please try again.
';
suggestions.hidden = false;
suggestions._entries = [];
});
}, 350);
qs('address_search').addEventListener('input', doAddressSearch);
suggestions.addEventListener('click', function(event){
var button = event.target.closest('.wtt-suggestion');
if (!button) return;
var entries = suggestions._entries || [];
var idx = Number(button.getAttribute('data-index'));
if (entries[idx]) fillAddress(entries[idx]);
});
document.addEventListener('click', function(event){
if (!event.target.closest('.wtt-address-field')) suggestions.hidden = true;
});
function getFieldValues(){
var out = {};
Array.prototype.slice.call(form.querySelectorAll('[name]')).forEach(function(el){ out[el.name] = el.value; });
return out;
}
function reportValidity(){
var required = Array.prototype.slice.call(form.querySelectorAll('[name][required]'));
for (var i = 0; i < required.length; i++) {
if (!required[i].reportValidity()) return false;
}
return true;
}
form.addEventListener('keydown', function(event){
if (event.key === 'Enter' && !event.target.closest('.wtt-address-field')) {
event.preventDefault();
submitEstimate();
}
});
function proxyEstimate(fields){
var body = new URLSearchParams();
body.set('action', 'wtt_airroi_estimate');
body.set('nonce', config.nonce || '');
Object.keys(fields).forEach(function(key){ body.set(key, fields[key] == null ? '' : fields[key]); });
return fetchJson(config.ajaxUrl, {
method:'POST',
headers:{ 'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8' },
body:body.toString(),
credentials:'same-origin'
}, Number(config.requestTimeoutMs) || 120000, 'Failed while waiting for the estimate to load. Please try again.').then(function(resp){
if (!resp || !resp.success) {
var msg = resp && resp.data && resp.data.message ? resp.data.message : 'Request failed.';
throw new Error(msg);
}
return resp.data || {};
});
}
function airroiGet(path, query){
if (!config.apiKey) throw new Error('AirROI API key is not set in the HTML config.');
var url = String(config.apiBaseUrl || 'https://api.airroi.com').replace(/\/$/, '') + '/' + String(path).replace(/^\//, '') + '?' + buildQuery(query);
return fetchJson(url, { headers:{ 'X-API-KEY':config.apiKey, 'Accept':'application/json' } }, Number(config.requestTimeoutMs) || 120000, 'Failed while waiting for the estimate to load. Please try again.');
}
function airroiPost(path, body){
if (!config.apiKey) throw new Error('AirROI API key is not set in the HTML config.');
var url = String(config.apiBaseUrl || 'https://api.airroi.com').replace(/\/$/, '') + '/' + String(path).replace(/^\//, '');
return fetchJson(url, {
method:'POST',
headers:{ 'X-API-KEY':config.apiKey, 'Accept':'application/json', 'Content-Type':'application/json' },
body:JSON.stringify(body || {})
}, Number(config.requestTimeoutMs) || 120000, 'Failed while waiting for the estimate to load. Please try again.');
}
function postWebhook(payload){
if (!config.makeWebhookUrl) return Promise.resolve();
return fetch(config.makeWebhookUrl, {
method:'POST',
headers:{ 'Content-Type':'application/json' },
body:JSON.stringify(payload)
}).catch(function(){ return null; });
}
function directEstimate(fields){
var lat = fields.lat !== '' ? Number(fields.lat) : null;
var lng = fields.lng !== '' ? Number(fields.lng) : null;
return airroiGet('calculator/estimate', {
lat: lat,
lng: lng,
bedrooms: fields.bedrooms,
baths: String(fields.baths).replace('+', ''),
guests: fields.guests,
currency: 'native'
}).then(function(estimate){
return airroiGet('markets/lookup', { lat:lat, lng:lng }).catch(function(error){ return { error:error.message }; }).then(function(marketLookup){
var summary = {};
var metrics = {};
var marketPayload = buildMarketPayload(marketLookup);
var chain = Promise.resolve();
if (marketPayload) {
chain = airroiPost('markets/summary', marketPayload).catch(function(){ return {}; }).then(function(result){
summary = result || {};
if (config.advancedMetricsEnabled === false) return null;
var defs = [
['occupancy','markets/metrics/occupancy'],
['adr','markets/metrics/average-daily-rate'],
['revenue','markets/metrics/revenue'],
['pacing','markets/metrics/future/pacing']
];
return Promise.allSettled(defs.map(function(def){ return airroiPost(def[1], marketPayload); })).then(function(results){
defs.forEach(function(def, index){
var item = results[index];
metrics[def[0]] = item.status === 'fulfilled' ? item.value : { error:item.reason && item.reason.message ? item.reason.message : 'Request failed.' };
});
});
});
}
return chain.then(function(){
var bathsValue = String(fields.baths) === '4+' ? 4 : Number(String(fields.baths).replace('+', ''));
var payload = {
lead: {
first_name: fields.first_name,
last_name: fields.last_name,
email: fields.email,
phone: fields.phone
},
property: {
display_address: [fields.address_line, fields.suburb, fields.state, fields.postcode, fields.country].filter(Boolean).join(', '),
address_line: fields.address_line,
suburb: fields.suburb,
state: fields.state,
postcode: fields.postcode,
country: fields.country,
lat: lat,
lng: lng,
bedrooms: Number(fields.bedrooms),
baths: bathsValue,
guests: Number(fields.guests)
},
estimate: estimate,
market_lookup: marketLookup,
market_summary: summary,
metrics: metrics,
comparables: config.includeComparables === false ? [] : trimComparables(estimate, config.maxComparables),
derived: deriveOutput(estimate, summary, metrics),
meta: {
html_mode: 'proxy',
timestamp_utc: new Date().toISOString()
}
};
return postWebhook(payload).then(function(){ return payload; });
});
});
});
}
function renderPayload(payload){
var derived = payload.derived || {};
var market = payload.market_lookup || {};
var cur = derived.currency || 'USD';
modal.querySelector('.wtt-print-address').textContent = payload.property && payload.property.display_address ? payload.property.display_address : '';
modal.querySelector('.wtt-out-monthly').textContent = money(derived.estimated_monthly_revenue, cur);
modal.querySelector('.wtt-out-annual').textContent = money(derived.estimated_annual_revenue, cur);
modal.querySelector('.wtt-out-occ').textContent = pct(derived.occupancy);
modal.querySelector('.wtt-out-adr').textContent = money(derived.average_daily_rate, cur);
modal.querySelector('.wtt-out-cur').textContent = cur || '—';
modal.querySelector('.wtt-out-revpar').textContent = money(derived.revpar, cur);
modal.querySelector('.wtt-out-listings').textContent = derived.active_listings_count != null ? Number(derived.active_listings_count).toLocaleString() : '—';
modal.querySelector('.wtt-out-booking').textContent = derived.booking_lead_time != null ? number(derived.booking_lead_time, 1) + ' days' : '—';
modal.querySelector('.wtt-out-stay').textContent = derived.length_of_stay != null ? number(derived.length_of_stay, 1) + ' nights' : '—';
modal.querySelector('.wtt-out-market').textContent = market.full_name || market.locality || '—';
modal.querySelector('.wtt-scenarios').innerHTML = renderMissionScenarios(derived.performance_scenarios || [], cur);
modal.querySelector('.wtt-monthly-bars').innerHTML = renderMonthlyBars(derived.monthly_distribution || [], cur);
modal.querySelector('.wtt-percentiles').innerHTML = renderPercentiles(derived.percentiles || {}, cur);
modal.querySelector('.wtt-occ-history').innerHTML = renderMiniTable(derived.occupancy_history || [], function(item){
var value = item.avg != null ? item.avg : (item.rate != null ? item.rate : (item.value != null ? item.value : (item.occupancy != null ? item.occupancy : item.count)));
return pct(value);
});
modal.querySelector('.wtt-adr-history').innerHTML = renderMiniTable(derived.adr_history || [], function(item){
var value = item.avg != null ? item.avg : (item.rate != null ? item.rate : (item.value != null ? item.value : (item.average_daily_rate != null ? item.average_daily_rate : item.count)));
return money(value, cur);
});
modal.querySelector('.wtt-revenue-history').innerHTML = renderMiniTable(derived.revenue_history || [], function(item){
var value = item.avg != null ? item.avg : (item.revenue != null ? item.revenue : (item.value != null ? item.value : item.count));
return money(value, cur);
});
modal.querySelector('.wtt-pacing').innerHTML = renderMiniTable(derived.pacing || [], function(item){
if (item.fill_rate != null) return pct(item.fill_rate);
if (item.booked_rate_avg != null) return money(item.booked_rate_avg, cur);
return item.count != null ? Number(item.count).toLocaleString() : '—';
});
var comparables = payload.comparables || [];
var comparablesWrap = modal.querySelector('.wtt-comparables-wrap');
if (comparables.length) {
comparablesWrap.hidden = false;
modal.querySelector('.wtt-comparables').innerHTML = renderComparables(comparables, cur);
} else {
comparablesWrap.hidden = true;
modal.querySelector('.wtt-comparables').innerHTML = '';
}
modal.querySelector('.wtt-raw').textContent = JSON.stringify(payload, null, 2);
}
function submitEstimate(){
if (submitting) return;
var bypass = bypassForCurrentInputs();
if (leadFieldsWrap && !leadFieldsWrap.hidden) {
if (bypass) {
if (!qs('email').value) qs('email').value = 'wtp-test@wealththroughproperty.local';
if (!qs('phone').value) qs('phone').value = '0000000000';
}
if (!reportValidity()) return;
} else {
var required = Array.prototype.slice.call(form.querySelectorAll('[name][required]'));
var currentFields = getFieldValues();
var allowLatLngFallback = hasUsableLatLng(currentFields);
for (var i = 0; i < required.length; i++) {
if (bypass && ['email','phone'].indexOf(required[i].name) !== -1 && !required[i].value) {
required[i].value = required[i].name === 'email' ? 'wtp-test@wealththroughproperty.local' : '0000000000';
}
if (allowLatLngFallback && ['address_line','suburb','state','country'].indexOf(required[i].name) !== -1 && !required[i].value) {
continue;
}
if (!required[i].checkValidity()) {
if (!required[i].value && ['first_name','last_name','email','phone'].indexOf(required[i].name) !== -1) {
if (leadSummary) leadSummary.hidden = true;
if (leadFieldsWrap) leadFieldsWrap.hidden = false;
}
required[i].reportValidity();
return;
}
}
}
var fields = getFieldValues();
if (bypass) {
fields.email = fields.email || 'wtp-test@wealththroughproperty.local';
fields.phone = fields.phone || '0000000000';
}
var lat = fields.lat !== '' ? Number(fields.lat) : null;
var lng = fields.lng !== '' ? Number(fields.lng) : null;
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
setStatus('Please choose a suggested property address or manually enter latitude and longitude.', 'error');
return;
}
if (!fields.address_line && fields.address_search) fields.address_line = fields.address_search;
if (!fields.suburb && fields.address_search) {
var parts = String(fields.address_search).split(',').map(function(part){ return String(part).trim(); }).filter(Boolean);
if (parts.length > 1) fields.suburb = parts[1];
}
if (!fields.state && fields.address_search) {
var partsForState = String(fields.address_search).split(',').map(function(part){ return String(part).trim(); }).filter(Boolean);
if (partsForState.length > 2) {
for (var s = partsForState.length - 1; s >= 0; s--) {
var part = partsForState[s];
if (/^\d{4,10}$/.test(part)) continue;
if (/^australia$|^new zealand$|^united states$|^united kingdom$|^canada$/i.test(part)) continue;
fields.state = part;
break;
}
}
}
if (!fields.country && fields.address_search) {
var partsForCountry = String(fields.address_search).split(',').map(function(part){ return String(part).trim(); }).filter(Boolean);
if (partsForCountry.length) fields.country = partsForCountry[partsForCountry.length - 1];
}
setSubmitting(true);
startLoadingStatus();
var runner = config.mode === 'direct' ? directEstimate(fields) : proxyEstimate(fields);
runner.then(function(payload){
renderPayload(payload);
openModal();
saveLead();
setStatus('Estimate ready. The results popup has opened.', 'success');
}).catch(function(error){
var message = error && error.message ? error.message : 'Failed while waiting for the estimate to load. Please try again.';
if (message === 'Failed to fetch' || /networkerror/i.test(message)) {
message = 'Failed while waiting for the estimate to load. Please try again.';
}
setStatus(message, 'error');
}).finally(function(){
setSubmitting(false);
});
}
submitBtn.addEventListener('click', function(event){
event.preventDefault();
submitEstimate();
});
}
window.WTTAirroiBoot = boot;
if (Array.isArray(window.WTTAirroiQueue)) {
window.WTTAirroiQueue.forEach(function(item){
var root = document.getElementById(item.id);
if (root) boot(root, item.config || {});
});
window.WTTAirroiQueue = [];
}
})();