Construction cost estimators are one of the most common tools built on top of material pricing data — and one of the hardest to build from scratch. Prices vary by product, region, and spec. Most developers end up scraping PDFs or hardcoding stale numbers.

The Substrata API gives you structured price ranges (price_min_usd / price_max_usd) for 500+ building products across 12 categories. This tutorial walks you through fetching that data, filtering by category and spec, and wiring it into a simple estimator that takes room dimensions as input.

Prerequisites

What you need
  • Node.js 18+ — uses native fetch, no extra HTTP library needed
  • A free Substrata API keyget one here, no credit card required. Free tier is 100 calls/day.
  • Basic JavaScript comfort — this is ~50 lines, no framework

Set your key as an environment variable so it's not hardcoded:

terminal
export SUBSTRATA_API_KEY=sk_sub_your_key_here

Step 1: Fetch available categories

1 GET /v1/categories

Before searching for products, pull the category list. This tells you what's available and what subcategories exist within each category — useful for building a dynamic UI or validating user inputs.

estimator.js
const BASE_URL = 'https://substrata-34wm.polsia.app/v1'; const API_KEY = process.env.SUBSTRATA_API_KEY; async function fetchCategories() { const res = await fetch(`${BASE_URL}/categories`, { headers: { 'Authorization': `Bearer ${API_KEY}` } }); const data = await res.json(); return data.data; // array of { category, subcategories, product_count } } // Usage: const categories = await fetchCategories(); console.log(categories.map(c => c.category)); // ['concrete', 'doors', 'flooring', 'hvac', 'insulation', // 'roofing', 'structural', 'windows', ...]

The product_count field on each category tells you how many products are available to filter — helpful for showing users what's in scope before they make a selection.

Step 2: Search products by category

2 GET /v1/products?category=insulation

Once the user picks a category, fetch matching products. The /v1/products endpoint supports filtering by category, subcategory, and any performance spec. For a cost estimator, the most useful filters are category and subcategory — they narrow the product list to what's relevant for the job.

estimator.js
async function fetchProducts(category, subcategory = null) { const params = new URLSearchParams({ category, limit: 20 }); if (subcategory) params.set('subcategory', subcategory); const res = await fetch(`${BASE_URL}/products?${params}`, { headers: { 'Authorization': `Bearer ${API_KEY}` } }); const data = await res.json(); return data.data; // array of products with price_min_usd, price_max_usd } // Fetch batt insulation products: const insulationProducts = await fetchProducts('insulation', 'batt'); // Fetch all flooring products: const flooringProducts = await fetchProducts('flooring');

Each product in the response includes the full spec payload. For a cost estimator, you mainly need name, price_min_usd, price_max_usd, and — for area-based materials — coverage_sqft when it's available.

Step 3: Read pricing data and calculate per-sqft costs

3 price_min_usd / price_max_usd → cost per sqft

The Substrata API returns price_min_usd and price_max_usd for every product. These are per-unit prices — typically per bag, per panel, per roll, or per sheet depending on the product category. To get a per-sqft cost, divide by coverage.

Here's how the pricing fields look in a product response:

API response (product object)
{ "id": 2847, "name": "Owens Corning EcoTouch R-19 Batt", "manufacturer": "Owens Corning", "category": "insulation", "subcategory": "batt", "r_value": 19, "thickness_in": 6.25, "coverage_sqft": 40, // sqft per bag/roll "price_min_usd": 28.50, // low end, per unit "price_max_usd": 38.00, // high end, per unit "energy_star": true }

To get a per-sqft cost range, divide price by coverage:

estimator.js
function perSqftCost(product) { const coverage = product.coverage_sqft || 1; // fallback if not set return { low: product.price_min_usd / coverage, high: product.price_max_usd / coverage }; } // Example: Owens Corning R-19 batt @ $28.50–$38.00 per 40 sqft roll // → $0.71–$0.95 per sqft

For products that are sold by the unit rather than by area coverage (like a specific HVAC unit), use price_min_usd / price_max_usd as-is and multiply by quantity instead.

Step 4: Build the estimator logic

4 Room dimensions + materials → cost estimate

Now wire it together. The estimator takes a room definition — dimensions and a list of material categories to include — fetches price data for each, and outputs a cost range. The midpoint gives a single number to headline; the range communicates honest uncertainty.

estimator.js
async function estimateRoom(room) { // room = { name, sqft, materials: ['insulation', 'flooring'] } const lineItems = []; for (const category of room.materials) { const products = await fetchProducts(category); if (!products.length) continue; // Use the median-priced product as representative const sorted = products.sort((a, b) => (a.price_min_usd + a.price_max_usd) / 2 - (b.price_min_usd + b.price_max_usd) / 2 ); const mid = sorted[Math.floor(sorted.length / 2)]; const rate = perSqftCost(mid); lineItems.push({ category, product: mid.name, sqft: room.sqft, low: +(rate.low * room.sqft).toFixed(2), high: +(rate.high * room.sqft).toFixed(2), midpoint: +((rate.low + rate.high) / 2 * room.sqft).toFixed(2) }); } const totalLow = lineItems.reduce((s, l) => s + l.low, 0); const totalHigh = lineItems.reduce((s, l) => s + l.high, 0); return { room: room.name, sqft: room.sqft, lineItems, totalLow, totalHigh }; }

Complete working example

Here's the full script — paste it into estimator.js, set SUBSTRATA_API_KEY, and run with node estimator.js. It estimates the material cost for a master bedroom renovation covering insulation, flooring, and roofing.

estimator.js — full script
// Construction Cost Estimator — Substrata API // Usage: SUBSTRATA_API_KEY=sk_sub_xxx node estimator.js // Sign up: https://substrata-34wm.polsia.app/signup const BASE = 'https://substrata-34wm.polsia.app/v1'; const KEY = process.env.SUBSTRATA_API_KEY; if (!KEY) { console.error('Set SUBSTRATA_API_KEY — free key at https://substrata-34wm.polsia.app/signup'); process.exit(1); } async function get(path) { const r = await fetch(`${BASE}${path}`, { headers: { Authorization: `Bearer ${KEY}` } }); return (await r.json()).data; } function perSqft(p) { const cov = p.coverage_sqft || 1; return { low: p.price_min_usd / cov, high: p.price_max_usd / cov }; } async function estimate(rooms) { // Cache product fetches so we don't hit the same category twice const cache = {}; for (const room of rooms) { const lines = []; for (const cat of room.materials) { if (!cache[cat]) cache[cat] = await get(`/products?category=${cat}&limit=50`); const prods = cache[cat]; if (!prods?.length) continue; // Pick median-priced product const sorted = [...prods].sort((a, b) => (a.price_min_usd + a.price_max_usd) / 2 - (b.price_min_usd + b.price_max_usd) / 2 ); const p = sorted[Math.floor(sorted.length / 2)]; const rate = perSqft(p); lines.push({ cat, name: p.name, low: +(rate.low * room.sqft).toFixed(2), high: +(rate.high * room.sqft).toFixed(2) }); } const lo = lines.reduce((s, l) => s + l.low, 0).toFixed(2); const hi = lines.reduce((s, l) => s + l.high, 0).toFixed(2); console.log(`\n📐 ${room.name} (${room.sqft} sqft)`); lines.forEach(l => console.log(` ${l.cat.padEnd(12)} ${l.name.slice(0, 30).padEnd(32)} $${l.low}–$${l.high}`) ); console.log(` ${'TOTAL'.padEnd(46)} $${lo}–$${hi}`); } } // Define your rooms and the material categories to price estimate([ { name: 'Master Bedroom', sqft: 240, materials: ['insulation', 'flooring'] }, { name: 'Living Room', sqft: 380, materials: ['flooring'] }, { name: 'Roof Deck', sqft: 1800, materials: ['roofing'] } ]);

Run it:

terminal
node estimator.js
output
📐 Master Bedroom (240 sqft) insulation Owens Corning EcoTouch R-19 Batt $170.40–$228.00 flooring Shaw Endura Plus LVP $384.00–$552.00 TOTAL $554.40–$780.00 📐 Living Room (380 sqft) flooring Shaw Endura Plus LVP $608.00–$874.00 TOTAL $608.00–$874.00 📐 Roof Deck (1800 sqft) roofing Owens Corning Duration Shingles $1980.00–$2880.00 TOTAL $1980.00–$2880.00

What to build next

This script is intentionally minimal — a foundation, not a finished product. Here's how to extend it:

The full API reference — every filter parameter, response schema, and example query — is in the documentation. The price index shows live data for all categories if you want to browse before querying.

Get your free API key

100 API calls per day, no credit card. Insulation, flooring, roofing, HVAC, windows — all queryable via REST with price_min_usd and price_max_usd on every product.

Get API Key → Read Docs