1
Start with a complete standalone HTML page. This includes a form with address, bedrooms, baths, and guests inputs, a fetch() call to the AirROI API, and results displayed in a card. Copy and paste this into any HTML file or CMS.
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Airbnb Revenue Estimator</title>
<style>
.estimator { max-width: 480px; margin: 2rem auto; font-family: sans-serif; }
.estimator input, .estimator select { width: 100%; padding: 10px; margin: 6px 0 14px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
.estimator button { width: 100%; padding: 12px; background: #1976d2; color: #fff; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; }
.estimator button:hover { background: #1565c0; }
.estimator button:disabled { background: #bbb; cursor: not-allowed; }
.result-card { margin-top: 1.5rem; padding: 1.5rem; border: 1px solid #e0e0e0; border-radius: 8px; background: #fafafa; }
.result-card h3 { margin: 0 0 1rem; color: #1976d2; }
.metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
.metric:last-child { border-bottom: none; }
.metric-label { color: #666; }
.metric-value { font-weight: 700; }
</style>
</head>
<body>
<div class="estimator">
<h2>Estimate Airbnb Revenue</h2>
<label>Address</label>
<input type="text" id="address" placeholder="123 Main St, Los Angeles, CA">
<label>Bedrooms</label>
<input type="number" id="bedrooms" value="2" min="0" max="20">
<label>Bathrooms</label>
<input type="number" id="baths" value="1" min="0" max="20">
<label>Guests</label>
<input type="number" id="guests" value="4" min="1" max="50">
<button id="estimate-btn" onclick="getEstimate()">Get Estimate</button>
<div id="results"></div>
</div>
<script>
async function getEstimate() {
const btn = document.getElementById('estimate-btn');
const address = document.getElementById('address').value;
const bedrooms = document.getElementById('bedrooms').value;
const baths = document.getElementById('baths').value;
const guests = document.getElementById('guests').value;
btn.disabled = true;
btn.textContent = 'Loading...';
try {
const params = new URLSearchParams({ address, bedrooms, baths, guests });
const res = await fetch(`https://api.airroi.com/calculator/estimate?${params}`, {
headers: { 'X-API-KEY': 'your-api-key' }
});
const data = await res.json();
document.getElementById('results').innerHTML = `
<div class="result-card">
<h3>Revenue Estimate</h3>
<div class="metric"><span class="metric-label">Annual Revenue</span><span class="metric-value">$${data.revenue?.toLocaleString()}</span></div>
<div class="metric"><span class="metric-label">Avg Daily Rate</span><span class="metric-value">$${data.average_daily_rate}</span></div>
<div class="metric"><span class="metric-label">Occupancy</span><span class="metric-value">${Math.round(data.occupancy * 100)}%</span></div>
<div class="metric"><span class="metric-label">Revenue (P25)</span><span class="metric-value">$${data.percentiles?.revenue?.p25?.toLocaleString()}</span></div>
<div class="metric"><span class="metric-label">Revenue (P50)</span><span class="metric-value">$${data.percentiles?.revenue?.p50?.toLocaleString()}</span></div>
<div class="metric"><span class="metric-label">Revenue (P75)</span><span class="metric-value">$${data.percentiles?.revenue?.p75?.toLocaleString()}</span></div>
</div>`;
} catch (e) {
document.getElementById('results').innerHTML = '<p style="color:red">Error: ' + e.message + '</p>';
} finally {
btn.disabled = false;
btn.textContent = 'Get Estimate';
}
}
</script>
</body>
</html>Replace your-api-key with your actual API key. For production, use a backend proxy to keep the key secret.
2
If your site uses React, here is the same widget as a reusable component with useState, useCallback, and props for API key and lead capture callback.
jsx
import React, { useState, useCallback } from "react";
function RevenueEstimator({ apiKey, onLeadCapture }) {
const [form, setForm] = useState({ address: "", bedrooms: 2, baths: 1, guests: 4 });
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleChange = (e) => {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
};
const getEstimate = useCallback(async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({
address: form.address,
bedrooms: form.bedrooms,
baths: form.baths,
guests: form.guests,
});
const res = await fetch(
`https://api.airroi.com/calculator/estimate?${params}`,
{ headers: { "X-API-KEY": apiKey } }
);
if (!res.ok) throw new Error(`API returned ${res.status}`);
const data = await res.json();
setResult(data);
if (onLeadCapture) onLeadCapture(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [form, apiKey, onLeadCapture]);
return (
<div style={{ maxWidth: 480, margin: "2rem auto", fontFamily: "sans-serif" }}>
<h2>Estimate Airbnb Revenue</h2>
<label>Address</label>
<input name="address" value={form.address} onChange={handleChange}
placeholder="123 Main St, Los Angeles, CA"
style={{ width: "100%", padding: 10, margin: "6px 0 14px", border: "1px solid #ddd", borderRadius: 6, boxSizing: "border-box" }} />
<label>Bedrooms</label>
<input name="bedrooms" type="number" value={form.bedrooms} onChange={handleChange}
style={{ width: "100%", padding: 10, margin: "6px 0 14px", border: "1px solid #ddd", borderRadius: 6, boxSizing: "border-box" }} />
<label>Bathrooms</label>
<input name="baths" type="number" value={form.baths} onChange={handleChange}
style={{ width: "100%", padding: 10, margin: "6px 0 14px", border: "1px solid #ddd", borderRadius: 6, boxSizing: "border-box" }} />
<label>Guests</label>
<input name="guests" type="number" value={form.guests} onChange={handleChange}
style={{ width: "100%", padding: 10, margin: "6px 0 14px", border: "1px solid #ddd", borderRadius: 6, boxSizing: "border-box" }} />
<button onClick={getEstimate} disabled={loading}
style={{ width: "100%", padding: 12, background: loading ? "#bbb" : "#1976d2", color: "#fff", border: "none", borderRadius: 6, fontSize: "1rem", cursor: loading ? "not-allowed" : "pointer" }}>
{loading ? "Loading..." : "Get Estimate"}
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{result && (
<div style={{ marginTop: 24, padding: 24, border: "1px solid #e0e0e0", borderRadius: 8, background: "#fafafa" }}>
<h3 style={{ margin: "0 0 16px", color: "#1976d2" }}>Revenue Estimate</h3>
<Metric label="Annual Revenue" value={`$${result.revenue?.toLocaleString()}`} />
<Metric label="Avg Daily Rate" value={`$${result.average_daily_rate}`} />
<Metric label="Occupancy" value={`${Math.round(result.occupancy * 100)}%`} />
<Metric label="Revenue (P25)" value={`$${result.percentiles?.revenue?.p25?.toLocaleString()}`} />
<Metric label="Revenue (P50)" value={`$${result.percentiles?.revenue?.p50?.toLocaleString()}`} />
<Metric label="Revenue (P75)" value={`$${result.percentiles?.revenue?.p75?.toLocaleString()}`} />
</div>
)}
</div>
);
}
function Metric({ label, value }) {
return (
<div style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: "1px solid #eee" }}>
<span style={{ color: "#666" }}>{label}</span>
<span style={{ fontWeight: 700 }}>{value}</span>
</div>
);
}
export default RevenueEstimator;Usage: <RevenueEstimator apiKey="your-key" onLeadCapture={handleLead} />
3
The /calculator/estimate endpoint accepts an address parameter as an alternative to lat/lng coordinates. This simplifies the user experience since visitors can type a street address instead of looking up coordinates.
javascript
// The /calculator/estimate endpoint accepts an "address" parameter
// as an alternative to lat/lng coordinates.
// HTML/JS version — just use the address input directly:
const params = new URLSearchParams({
address: document.getElementById('address').value,
bedrooms: document.getElementById('bedrooms').value,
baths: document.getElementById('baths').value,
guests: document.getElementById('guests').value,
});
const res = await fetch(
`https://api.airroi.com/calculator/estimate?${params}`,
{ headers: { 'X-API-KEY': 'your-api-key' } }
);
// The API will geocode the address automatically.
// Supported formats:
// "123 Main St, Los Angeles, CA"
// "Miami Beach, FL"
// "90210" (zip code)4
Show revenue at percentiles (p25/p50/p75/p90), a monthly revenue bar chart, occupancy percentage, and average daily rate. This gives users a comprehensive view of earning potential.
javascript
// Display revenue at percentiles + monthly chart + occupancy
function renderResults(data) {
const { revenue, average_daily_rate, occupancy, percentiles,
monthly_revenue_distributions } = data;
// Percentile breakdown
const percentileRows = [
{ label: "Conservative (P25)", value: percentiles.revenue.p25 },
{ label: "Expected (P50)", value: percentiles.revenue.p50 },
{ label: "Optimistic (P75)", value: percentiles.revenue.p75 },
{ label: "Top Performer (P90)", value: percentiles.revenue.p90 },
];
// Monthly chart as simple bar
const months = ["Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"];
const maxMonthly = Math.max(...monthly_revenue_distributions);
const chartHTML = monthly_revenue_distributions.map((val, i) => {
const pct = (val / maxMonthly) * 100;
return `<div style="display:flex;align-items:center;gap:8px;margin:4px 0">
<span style="width:30px;font-size:12px">${months[i]}</span>
<div style="height:18px;background:#1976d2;border-radius:4px;
width:${pct}%"></div>
<span style="font-size:12px">$${val.toLocaleString()}</span>
</div>`;
}).join("");
return `
<h3>Revenue by Percentile</h3>
${percentileRows.map(r => `<p>${r.label}: $${r.value.toLocaleString()}</p>`).join("")}
<h3>Occupancy</h3>
<p>${Math.round(occupancy * 100)}% average occupancy rate</p>
<h3>ADR</h3>
<p>$${average_daily_rate} average daily rate</p>
<h3>Monthly Revenue Distribution</h3>
${chartHTML}`;
}5
Monetize the widget by gating detailed results behind an email capture. Show the first two metrics (annual revenue and occupancy) for free, then blur the remaining data and require an email to unlock the full report.
javascript
// Lead capture overlay — show preview, gate full results behind email
function renderWithGate(data) {
const resultsDiv = document.getElementById('results');
// Always show these "free" fields
const preview = `
<div class="result-card">
<h3>Revenue Estimate</h3>
<div class="metric">
<span class="metric-label">Annual Revenue</span>
<span class="metric-value">$${data.revenue?.toLocaleString()}</span>
</div>
<div class="metric">
<span class="metric-label">Occupancy</span>
<span class="metric-value">${Math.round(data.occupancy * 100)}%</span>
</div>
</div>`;
// Gate the detailed breakdown
const gate = `
<div style="position:relative;margin-top:1rem">
<div style="filter:blur(6px);pointer-events:none;user-select:none">
<div class="metric"><span>Revenue (P25)</span><span>$38,200</span></div>
<div class="metric"><span>Revenue (P50)</span><span>$49,800</span></div>
<div class="metric"><span>Revenue (P75)</span><span>$62,500</span></div>
<div class="metric"><span>Monthly breakdown</span><span>...</span></div>
</div>
<div style="position:absolute;inset:0;display:flex;flex-direction:column;
align-items:center;justify-content:center;
background:rgba(255,255,255,0.85);border-radius:8px">
<p style="font-weight:600;margin-bottom:8px">
Enter your email for the full report
</p>
<input type="email" id="gate-email" placeholder="you@example.com"
style="padding:10px;border:1px solid #ddd;border-radius:6px;
width:260px;margin-bottom:8px" />
<button onclick="unlockResults()"
style="padding:10px 24px;background:#1976d2;color:#fff;
border:none;border-radius:6px;cursor:pointer">
Unlock Full Report
</button>
</div>
</div>`;
resultsDiv.innerHTML = preview + gate;
}
function unlockResults() {
const email = document.getElementById('gate-email').value;
if (!email) return alert('Please enter your email');
// Send lead to your CRM / email list
fetch('/api/leads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, source: 'revenue-widget' })
});
// Remove the gate and show full results
document.querySelector('[style*="blur"]').style.filter = 'none';
document.querySelector('[style*="blur"]').style.pointerEvents = 'auto';
document.querySelector('[style*="position:absolute"]').remove();
}6
Customize colors, fonts, and layout using CSS variables. The widget includes responsive design and a dark mode variant out of the box.
css
/* CSS variables for easy customization */
:root {
--estimator-primary: #1976d2;
--estimator-primary-hover: #1565c0;
--estimator-bg: #ffffff;
--estimator-card-bg: #fafafa;
--estimator-border: #e0e0e0;
--estimator-text: #333333;
--estimator-text-muted: #666666;
--estimator-font: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--estimator-radius: 8px;
}
/* Dark mode variant */
@media (prefers-color-scheme: dark) {
:root {
--estimator-bg: #1a1a1a;
--estimator-card-bg: #2a2a2a;
--estimator-border: #444444;
--estimator-text: #e0e0e0;
--estimator-text-muted: #aaaaaa;
}
}
/* Responsive layout */
.estimator {
max-width: 480px;
margin: 2rem auto;
padding: 0 1rem;
font-family: var(--estimator-font);
color: var(--estimator-text);
background: var(--estimator-bg);
}
@media (max-width: 600px) {
.estimator {
max-width: 100%;
margin: 1rem;
}
.estimator h2 { font-size: 1.3rem; }
.estimator button { font-size: 0.9rem; }
}
/* Override colors by setting CSS variables on a parent */
.my-brand .estimator {
--estimator-primary: #e91e63;
--estimator-primary-hover: #c2185b;
}To override colors for your brand, set the CSS variables on a parent element. The widget will inherit the values automatically.
Keep exploring the AirROI API with these related tutorials.
No. Never embed your API key directly in frontend code that is visible to users. Instead, create a lightweight backend proxy (e.g., a serverless function on Vercel, Netlify, or AWS Lambda) that holds your API key and forwards requests to the AirROI API. Your frontend calls your proxy, and the proxy calls AirROI with the key attached server-side.
Yes. The /calculator/estimate endpoint accepts an 'address' query parameter as an alternative to lat and lng. Pass a URL-encoded address string like ?address=123%20Main%20St%2C%20Los%20Angeles%2C%20CA and the API will geocode it automatically. This is simpler for end users who do not know coordinates.
The AirROI API supports CORS for authenticated requests. If you encounter CORS errors, make sure you are including the X-API-KEY header correctly. For production use, set up a backend proxy to avoid CORS issues entirely and to keep your API key secret.
Absolutely. The widget is just HTML, CSS, and JavaScript that you control entirely. Change colors via CSS variables, swap fonts, adjust layout, add your logo, and modify the result display format. The React version accepts props for easy customization. See Step 6 for CSS variable examples.
The widget itself is free to implement. You only pay for AirROI API calls. Each estimate request costs a fraction of a cent. Sign up at /api/developer/activate to get $10 in starter credits, which covers thousands of estimate calls. For high-traffic sites, contact us for volume pricing.
Yes. Step 5 of this tutorial shows exactly how to implement a lead capture overlay. The approach shows a preview of results (e.g., annual revenue and occupancy) for free, then requires an email address to see the full breakdown including percentile ranges and monthly distributions.
Stay ahead of the curve
Join our newsletter for exclusive insights and updates. No spam ever.