March 30, 2026 · GreenCalc Team · 13 min read

How to Build a Renovation Cost Calculator with the GreenCalc API

A complete tutorial: build a web-based calculator that takes a user’s renovation plans and shows estimated grants, loans, tax savings, and out-of-pocket costs — using plain HTML, CSS, and JavaScript.

Renovation cost calculators are among the highest-converting widgets for energy advisor websites, contractor platforms, and green finance apps. Users type in their project details and instantly see how much government funding they can access. In this tutorial, we build one from scratch using the GreenCalc API and vanilla JavaScript — no frameworks required.

What we are building

Our calculator will have three sections:

  1. Input form: Country, income, household size, postal code, property type, and a list of planned works with costs.
  2. Results panel: Breakdown of grants, loans, tax savings, and net out-of-pocket cost.
  3. Scheme details: Expandable list showing each eligible subsidy with its name, type, and amount.

The entire thing is a single HTML file. You can drop it into any website, embed it in an iframe, or adapt it to your framework of choice.

Step 1: The HTML form

Start with a clean form that collects the minimum data needed by the API. Note the use of data- attributes to store the normalized work type codes:

<!-- renovation-calculator.html -->
<form id="calc-form">
  <div class="form-row">
    <label>Country
      <select name="country" required>
        <option value="FR">France</option>
        <option value="DE">Germany</option>
        <option value="IT">Italy</option>
        <option value="BE">Belgium</option>
        <option value="ES">Spain</option>
      </select>
    </label>
    <label>Annual Income (€)
      <input type="number" name="income" value="35000" required>
    </label>
    <label>Household Size
      <input type="number" name="household_size" value="3" min="1" required>
    </label>
  </div>
  <div class="form-row">
    <label>Postal Code
      <input type="text" name="postal_code" value="75012" required>
    </label>
    <label>Property Type
      <select name="property_type">
        <option value="HOUSE">House</option>
        <option value="APARTMENT">Apartment</option>
      </select>
    </label>
    <label>Surface (m²)
      <input type="number" name="surface" value="100">
    </label>
  </div>

  <!-- Work items added dynamically -->
  <div id="works-list"></div>
  <button type="button" id="add-work">+ Add work</button>

  <button type="submit">Calculate Subsidies</button>
</form>

<div id="results" hidden></div>

Step 2: The JavaScript API call

The core function builds the request payload from form data and calls the GreenCalc API. For prototyping, we use the sandbox key directly. In production, you would proxy this through your backend:

const API_URL = 'https://greencalc.io/api/v1/eligibility/simulate';
const API_KEY = 'gc_sandbox_000000000000000000000000000000000';

async function calculateSubsidies(formData) {
  const works = collectWorks(); // reads from dynamic work list

  const body = {
    country_code: formData.get('country'),
    household: {
      annual_income: Number(formData.get('income')),
      household_size: Number(formData.get('household_size')),
      is_owner: true
    },
    property: {
      type: formData.get('property_type'),
      postal_code: formData.get('postal_code'),
      surface_m2: Number(formData.get('surface'))
    },
    planned_works: works
  };

  const res = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'X-Api-Key': API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  });

  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return await res.json();
}

Step 3: The work item collector

Each work item has a type dropdown and a cost input. The collectWorks function reads all work rows:

const WORK_TYPES = [
  { code: 'ROOF_INSULATION', label: 'Roof insulation', hasSurface: true },
  { code: 'WALL_INSULATION_EXTERIOR', label: 'Wall insulation (exterior)', hasSurface: true },
  { code: 'HEAT_PUMP_AIR_WATER', label: 'Heat pump (air-water)', hasSurface: false },
  { code: 'HEAT_PUMP_GEOTHERMAL', label: 'Heat pump (geothermal)', hasSurface: false },
  { code: 'WINDOWS_DOUBLE_GLAZING', label: 'Double-glazing windows', hasSurface: false },
  { code: 'WINDOWS_TRIPLE_GLAZING', label: 'Triple-glazing windows', hasSurface: false },
  { code: 'SOLAR_PANELS_PHOTOVOLTAIC', label: 'Solar panels (PV)', hasSurface: false },
  { code: 'SOLAR_THERMAL', label: 'Solar thermal', hasSurface: false },
  { code: 'CONDENSING_BOILER', label: 'Condensing boiler', hasSurface: false },
  { code: 'FLOOR_INSULATION', label: 'Floor insulation', hasSurface: true }
];

function collectWorks() {
  const rows = document.querySelectorAll('.work-row');
  return Array.from(rows).map(row => {
    const work = {
      work_type: row.querySelector('[name="work_type"]').value,
      estimated_cost_eur: Number(row.querySelector('[name="cost"]').value)
    };
    const surfaceInput = row.querySelector('[name="work_surface"]');
    if (surfaceInput && surfaceInput.value) {
      work.surface_m2 = Number(surfaceInput.value);
    }
    return work;
  });
}

Step 4: Rendering the results

The result renderer takes the API response and builds a clear breakdown. The key is to separate grants (money you get), loans (money you borrow at 0%), and tax savings (money you save later):

function renderResults(data) {
  const el = document.getElementById('results');
  const { summary, eligible_subsidies } = data;

  el.innerHTML = `
    <div class="summary-card">
      <div class="cost-row">
        <span>Total renovation cost</span>
        <strong>€${fmt(summary.total_estimated_cost_eur)}</strong>
      </div>
      <div class="cost-row grant">
        <span>Grants you receive</span>
        <strong>-€${fmt(summary.total_grants_eur)}</strong>
      </div>
      <div class="cost-row tax">
        <span>Tax savings</span>
        <strong>-€${fmt(summary.total_tax_savings_eur)}</strong>
      </div>
      <div class="cost-row total">
        <span>Your out-of-pocket cost</span>
        <strong>€${fmt(summary.estimated_out_of_pocket_eur)}</strong>
      </div>
      ${summary.total_loans_eur > 0 ? `
        <div class="cost-row loan">
          <span>0% government loan available</span>
          <strong>€${fmt(summary.total_loans_eur)}</strong>
        </div>` : ''}
    </div>
    <h3>Eligible subsidies</h3>
    ${eligible_subsidies.map(s => `
      <div class="scheme-card">
        <strong>${s.scheme_name}</strong>
        <span class="badge ${s.type.toLowerCase()}">${s.type}</span>
        <span class="amount">€${fmt(s.amount_eur)}</span>
      </div>
    `).join('')}
  `;
  el.hidden = false;
}

function fmt(n) { return n.toLocaleString('en', { maximumFractionDigits: 0 }); }

Step 5: Wiring it all together

Add the event listener and the dynamic work row builder:

document.getElementById('calc-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const btn = e.target.querySelector('[type="submit"]');
  btn.textContent = 'Calculating...';
  btn.disabled = true;

  try {
    const data = await calculateSubsidies(new FormData(e.target));
    renderResults(data);
  } catch (err) {
    document.getElementById('results').innerHTML =
      `<p class="error">Calculation failed: ${err.message}</p>`;
    document.getElementById('results').hidden = false;
  } finally {
    btn.textContent = 'Calculate Subsidies';
    btn.disabled = false;
  }
});

document.getElementById('add-work').addEventListener('click', () => {
  const row = document.createElement('div');
  row.className = 'work-row';
  row.innerHTML = `
    <select name="work_type">
      ${WORK_TYPES.map(w =>
        \`<option value="\${w.code}">\${w.label}</option>\`
      ).join('')}
    </select>
    <input name="cost" type="number" placeholder="Cost (€)" required>
    <input name="work_surface" type="number" placeholder="m² (if applicable)">
    <button type="button" onclick="this.parentElement.remove()">×</button>
  `;
  document.getElementById('works-list').appendChild(row);
});

Testing with curl

Before building the UI, test the API directly to understand the response format:

curl -X POST https://greencalc.io/api/v1/eligibility/simulate \
  -H "X-Api-Key: gc_sandbox_000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{
    "country_code": "FR",
    "household": {
      "annual_income": 28000,
      "household_size": 2,
      "is_owner": true
    },
    "property": {
      "type": "APARTMENT",
      "postal_code": "33000",
      "surface_m2": 65
    },
    "planned_works": [
      {"work_type": "WINDOWS_DOUBLE_GLAZING", "estimated_cost_eur": 5500}
    ]
  }'

Styling tips

The calculator works with any CSS framework. Here are design patterns that perform well in user testing:

Production tip: For a production calculator, proxy API calls through your backend. Create an endpoint like POST /api/calculate-subsidies on your server that forwards the request to GreenCalc with your live API key. This keeps the key secure and lets you add caching, logging, and rate limiting.

Extending the calculator

Once the basic calculator works, consider these enhancements:

The full API reference with all supported fields is at greencalc.io/docs.

Frequently Asked Questions

Can I call the GreenCalc API from a browser (client-side JavaScript)?

The sandbox key can be used from the browser for prototyping and demos. For production, proxy API calls through your backend to protect your live API key. Live keys should never be exposed in client-side code.

What work types does the GreenCalc API support?

The API supports 11 normalized work types: ROOF_INSULATION, WALL_INSULATION_EXTERIOR, WALL_INSULATION_INTERIOR, FLOOR_INSULATION, WINDOWS_DOUBLE_GLAZING, WINDOWS_TRIPLE_GLAZING, HEAT_PUMP_AIR_WATER, HEAT_PUMP_GEOTHERMAL, CONDENSING_BOILER, SOLAR_PANELS_PHOTOVOLTAIC, and SOLAR_THERMAL. These codes work across all 5 supported countries.

How accurate are the cost estimates from the calculator?

The GreenCalc API calculates subsidy amounts based on official rates. The renovation cost itself (estimated_cost_eur) is provided by the user. For more accurate costs, pair the subsidy calculation with contractor quotes or average market prices per work type and region.

GreenCalc is built by AZMORIS Group. Full API documentation at greencalc.io/docs.