Personal protective equipment that doesn’t fit is PPE that doesn’t protect. An oversized hard hat shifts on impact. Gloves that are too large reduce grip; too small, and they split or restrict circulation. Hi-vis vests that hang loose can catch on machinery. Safety boots one size off cause fatigue and injury over a full shift.
For companies deploying PPE at scale — construction sites, logistics warehouses, manufacturing plants, events — fitting each worker individually is operationally impractical. The standard workaround is to stock S/M/L/XL in bulk and let workers self-select. Self-selection produces systematically poor fit: workers guess or pick comfort over protection, and the same person often disagrees with themselves from one order to the next.
This guide builds an automated PPE sizing system. Workers submit height and weight at onboarding; the system derives the body dimensions that govern each PPE category and returns a complete kit list with per-item sizes.
Which dimensions govern PPE fit
| PPE item | Primary dimension | Secondary |
|---|---|---|
| Hard hat / bump cap | head_circumference | — |
| Safety glasses | head_breadth, face_breadth | face_length |
| Respirator / dust mask | face_breadth, face_length | — |
| Hi-vis vest / safety vest | chest_circumference, waist_circumference_natural | body_height |
| Work trousers | waist_circumference_natural, hip_circumference, inseam_length | — |
| Gloves | hand_breadth, hand_length | — |
| Safety boots | foot_length, foot_breadth | — |
All of these are available in the FULL_BODY bundle from a single API call.
Step 1: Server endpoint
// ppe-sizing.js
const express = require('express');
const router = express.Router();
const API_URL = 'https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict';
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY;
// ── Size table helpers ──────────────────────────────────────────────────────
function lookupSize(tables, valueMm) {
return tables.find(t => valueMm <= t.maxMm)?.size ?? tables[tables.length - 1].size;
}
// ── Hard hat ────────────────────────────────────────────────────────────────
// EN 397 / ANSI Z89.1 size ranges (head circumference in mm)
function hardHatSize(dims) {
const headCirc = dims.head_circumference ?? 560;
const SIZES = [
{ size: 'S', maxMm: 540 },
{ size: 'M', maxMm: 570 },
{ size: 'L', maxMm: 600 },
{ size: 'XL', maxMm: 630 },
{ size: 'XXL', maxMm: Infinity },
];
return {
size: lookupSize(SIZES, headCirc),
head_circumference_cm: Math.round(headCirc / 10),
note: 'Adjustable ratchet suspension covers ±10 mm within each band.',
};
}
// ── Respirator / dust mask ──────────────────────────────────────────────────
// Face breadth is the governing dimension for respirator seal width
function respiratorSize(dims) {
const faceBreadth = dims.face_breadth ?? 140; // mm
const faceLength = dims.face_length ?? 120; // mm (chin to brow bridge)
// EN 149 / NIOSH fit categories map loosely to face breadth + length
if (faceBreadth < 133 && faceLength < 113) return { size: 'S', note: 'Small face panel recommended.' };
if (faceBreadth < 145 && faceLength < 125) return { size: 'M', note: 'Standard size fits most workers.' };
return { size: 'L', note: 'Large face panel for broader or longer face geometry.' };
}
// ── Hi-vis vest ─────────────────────────────────────────────────────────────
// Chest circumference primary; body height secondary for vest length
function hivIsVestSize(dims) {
const chest = dims.chest_circumference ?? 960;
const height = dims.body_height ?? 1750;
const CHEST_SIZES = [
{ size: 'S', maxMm: 900 },
{ size: 'M', maxMm: 960 },
{ size: 'L', maxMm: 1020 },
{ size: 'XL', maxMm: 1080 },
{ size: 'XXL', maxMm: 1140 },
{ size: 'XXXL', maxMm: Infinity },
];
const byChest = lookupSize(CHEST_SIZES, chest);
// Tall workers (≥190 cm) should size up one for vest length coverage
const sizeIndex = CHEST_SIZES.findIndex(s => s.size === byChest);
const heightNote = height >= 1900 && sizeIndex < CHEST_SIZES.length - 1
? `Consider sizing up to ${CHEST_SIZES[sizeIndex + 1].size} for full torso coverage at your height.`
: null;
return {
size: byChest,
chest_circumference_cm: Math.round(chest / 10),
...(heightNote ? { note: heightNote } : {}),
};
}
// ── Work trousers ───────────────────────────────────────────────────────────
// EN waist/inseam sizing — return waist size + inseam length band
function trouserSize(dims) {
const waist = dims.waist_circumference_natural ?? 860;
const inseam = dims.inseam_length ?? 790;
// Waist in cm → standard trouser waist size (EU sizing)
const waistCm = Math.round(waist / 10);
const inseamCm = Math.round(inseam / 10);
const INSEAM_BANDS = [
{ label: 'Short (S)', maxCm: 74 },
{ label: 'Regular (R)', maxCm: 82 },
{ label: 'Long (L)', maxCm: 90 },
{ label: 'XLong (XL)', maxCm: Infinity },
];
const inseamBand = INSEAM_BANDS.find(b => inseamCm <= b.maxCm)?.label ?? INSEAM_BANDS[INSEAM_BANDS.length - 1].label;
return {
waist_cm: waistCm,
inseam_band: inseamBand,
size_label: `W${waistCm} / ${inseamBand}`,
note: 'Workwear trousers include seat and thigh allowance above dress trouser sizing.',
};
}
// ── Gloves ──────────────────────────────────────────────────────────────────
// EN 388 / ANSI/ISEA 105 — hand circumference at knuckles = hand_breadth × π
function gloveSize(dims) {
const handBreadth = dims.hand_breadth ?? 88; // mm
// Approximate knuckle circumference from hand breadth (oval cross-section)
const knuckleMm = Math.round(handBreadth * Math.PI * 0.92);
const knuckleCm = knuckleMm / 10;
const GLOVE_SIZES = [
{ size: '6 / XS', maxCm: 17.5 },
{ size: '7 / S', maxCm: 18.5 },
{ size: '8 / M', maxCm: 19.5 },
{ size: '9 / L', maxCm: 20.5 },
{ size: '10 / XL', maxCm: 21.5 },
{ size: '11 / XXL', maxCm: Infinity },
];
return {
size: lookupSize(GLOVE_SIZES, knuckleCm),
knuckle_circ_cm: Math.round(knuckleCm * 10) / 10,
note: 'Mechanical gloves: confirm hand length fits the finger stalls before bulk order.',
};
}
// ── Safety boots ────────────────────────────────────────────────────────────
// ISO 20345 / EN ISO 20345 — Mondopoint foot length in mm → EU size
function safetyBootSize(dims) {
const footLength = dims.foot_length ?? 265; // mm
const footBreadth = dims.foot_breadth ?? 100; // mm
// EU shoe size ≈ foot_length_mm / 6.667 (Mondopoint → Paris point)
const euSize = Math.ceil(footLength / 6.667);
// Width fitting: narrow (<95mm), regular (95–105), wide (>105)
const width = footBreadth < 95 ? 'Narrow' : footBreadth > 105 ? 'Wide' : 'Regular';
return {
eu_size: euSize,
width_fit: width,
foot_length_mm: footLength,
note: width !== 'Regular'
? `${width} foot: check brand width availability. Steel-toe boxes are often narrower than standard last.`
: 'Standard width fits most stock safety boots.',
};
}
// ── Main endpoint ────────────────────────────────────────────────────────────
router.post('/ppe-kit', async (req, res) => {
const { gender, height_cm, weight_kg, region, items } = req.body;
if (!gender || !height_cm || !weight_kg) {
return res.status(400).json({ error: 'gender, height_cm, and weight_kg are required' });
}
const heightMm = Math.round(parseFloat(height_cm) * 10);
const weightKg = parseFloat(weight_kg);
const targetRegion = region ?? 'GLOBAL';
const apiRes = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-rapidapi-key': RAPIDAPI_KEY,
'x-rapidapi-host': 'dimensionspot-bodysize-engine.p.rapidapi.com',
},
body: JSON.stringify({
input_data: {
input_unit_system: 'metric',
subject: { gender, input_origin_region: targetRegion },
anchors: { body_height: heightMm, body_mass: weightKg },
},
output_settings: {
calculation: { calculation_model: 'AUTO', target_region: targetRegion, body_build_type: 'CIVILIAN' },
requested_dimensions: { bundle: 'FULL_BODY' },
output_format: { unit_system: 'metric', confidence_score_threshold: 0, include_range_95: false, include_iso_codes: false },
},
}),
});
if (!apiRes.ok) return res.status(502).json({ error: 'Prediction service unavailable' });
const data = await apiRes.json();
const dims = Object.fromEntries(
Object.entries(data.body_dimensions).map(([key, d]) => [key, d.value])
);
// Build kit — allow caller to request specific items, default to full kit
const requestedItems = items ?? ['hard_hat', 'respirator', 'hi_vis_vest', 'trousers', 'gloves', 'boots'];
const kit = {};
if (requestedItems.includes('hard_hat')) kit.hard_hat = hardHatSize(dims);
if (requestedItems.includes('respirator')) kit.respirator = respiratorSize(dims);
if (requestedItems.includes('hi_vis_vest')) kit.hi_vis_vest = hivIsVestSize(dims);
if (requestedItems.includes('trousers')) kit.trousers = trouserSize(dims);
if (requestedItems.includes('gloves')) kit.gloves = gloveSize(dims);
if (requestedItems.includes('boots')) kit.boots = safetyBootSize(dims);
res.json({ ppe_kit: kit });
});
module.exports = router;
Step 2: Bulk onboarding from HR data
For a new site deployment, generate kit lists for an entire workforce from an HR export:
const fs = require('fs');
const Papa = require('papaparse');
const csv = fs.readFileSync('workers.csv', 'utf-8');
const { data: workers } = Papa.parse(csv, { header: true });
// CSV columns: worker_id, gender, height_cm, weight_kg, region
async function generateKits() {
const results = [];
for (let i = 0; i < workers.length; i += 10) {
const batch = workers.slice(i, i + 10);
const batchResults = await Promise.all(
batch.map(async (w) => {
const res = await fetch('https://your-server.com/api/ppe-kit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
gender: w.gender,
height_cm: w.height_cm,
weight_kg: w.weight_kg,
region: w.region ?? 'GLOBAL',
}),
});
const { ppe_kit } = await res.json();
return {
worker_id: w.worker_id,
hard_hat: ppe_kit.hard_hat?.size,
respirator: ppe_kit.respirator?.size,
hi_vis_vest: ppe_kit.hi_vis_vest?.size,
trousers: ppe_kit.trousers?.size_label,
gloves: ppe_kit.gloves?.size,
boots_eu: ppe_kit.boots?.eu_size,
boots_width: ppe_kit.boots?.width_fit,
hard_hat_note: ppe_kit.hard_hat?.note,
boots_note: ppe_kit.boots?.note,
};
})
);
results.push(...batchResults);
if (i + 10 < workers.length) await new Promise(r => setTimeout(r, 150));
}
fs.writeFileSync('ppe_kit_list.csv', Papa.unparse(results));
console.log(`Generated kit lists for ${results.length} workers`);
}
generateKits();
The output CSV feeds directly into a procurement system or site manager’s kit distribution checklist.
Step 3: Self-service onboarding portal
For ongoing hiring, add a self-service form to the worker onboarding portal:
<form id="ppe-form">
<label>Gender
<select name="gender" required>
<option value="male">Male</option>
<option value="female">Female</option>
</select>
</label>
<label>Height (cm)
<input name="height_cm" type="number" min="140" max="220" required>
</label>
<label>Weight (kg)
<input name="weight_kg" type="number" min="40" max="200" required>
</label>
<button type="submit">Get my kit sizes</button>
</form>
<div id="kit-result" hidden>
<h3>Your PPE kit sizes</h3>
<table id="kit-table">
<thead><tr><th>Item</th><th>Size</th><th>Note</th></tr></thead>
<tbody></tbody>
</table>
</div>
<script>
document.getElementById('ppe-form').addEventListener('submit', async e => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target));
const res = await fetch('/api/ppe-kit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const { ppe_kit } = await res.json();
const LABELS = {
hard_hat: 'Hard hat',
respirator: 'Respirator / dust mask',
hi_vis_vest: 'Hi-vis vest',
trousers: 'Work trousers',
gloves: 'Safety gloves',
boots: 'Safety boots',
};
const tbody = document.querySelector('#kit-table tbody');
tbody.innerHTML = '';
for (const [key, val] of Object.entries(ppe_kit)) {
const size = val.size_label ?? `${val.size ?? val.eu_size}${val.width_fit ? ' / ' + val.width_fit : ''}`;
tbody.insertAdjacentHTML('beforeend',
`<tr><td>${LABELS[key] ?? key}</td><td><strong>${size}</strong></td><td>${val.note ?? ''}</td></tr>`
);
}
document.getElementById('kit-result').hidden = false;
});
</script>
Accuracy and safety notes
Predictions are population-level estimates, not individual measurements. For most dimensions (glove size, hat size, vest size), a 5–10% sizing error means one size off — which for adjustable PPE is acceptable. For safety boots, which are not adjustable, the recommendation should be treated as a starting point: confirm with the worker before bulk ordering.
Hard hat sizing is the most reliable. Head circumference has consistently high confidence scores across the FULL_BODY bundle. The ±10 mm adjustment range of ratchet suspensions means even a prediction at the edge of a size band will fit correctly.
Glove sizing involves an approximation. The system estimates knuckle circumference from hand breadth using an elliptical cross-section model. For cut-resistant or impact gloves where exact finger stall fit matters, have workers verify glove size against a hand measurement before committing to bulk stock.
Regional calibration matters. Body proportions differ meaningfully by region. A workforce recruited from multiple countries should specify the correct region value per worker, not a blanket GLOBAL. The seven available profiles (GLOBAL, EUROPE, ASIA_PACIFIC, LATAM, INDIA, AFRICA, MIDDLE_EAST) cover the most common deployment scenarios.