Size-related returns are the most expensive problem in fashion e-commerce. The category consistently accounts for more than half of all returns, and the cost compounds: reverse logistics, restocking labor, inventory hold time, and the customer lifetime value lost when the return experience is poor.
The technology to address this exists. The gap is in implementation — specifically, in making sizing features that users actually complete, rather than features with high drop-off rates that provide data only for the users who already know their measurements.
This is a technical playbook for building a sizing flow that works.
The input collection problem
Most sizing features fail at the data collection step, not the algorithm step. The pattern is predictable: the form asks for too many measurements, users don’t know them, they abandon.
The minimum viable input for a useful size prediction is:
- Sex
- Height (virtually everyone knows this)
- Weight (most know this; some prefer not to share)
From these three data points, a body measurement prediction API can return torso circumferences (chest, waist, hip) with enough accuracy to distinguish between size buckets for most garment categories.
The optional upgrade is one circumference measurement. If users know their waist size — and many do, from jeans sizing — adding it improves circumference predictions meaningfully. Frame this as an optional field: “For more accurate recommendations, add your waist size (in inches or cm).”
What not to ask for:
- Bra size (high abandonment, measurement methodology varies)
- Inseam measured with a tape (users don’t have a tape)
- Hip circumference (most users don’t know this)
- Any measurement that requires equipment or assistance
The right question set takes 30 seconds to complete and has a completion rate that makes it worth building. A seven-measurement form takes 5 minutes and most users abandon it.
The integration architecture
The API call should happen server-side — never expose your API key in the browser.
import requests
def get_size_profile(gender: str, height_cm: float, weight_kg: float,
waist_cm: float = None, region: str = "GLOBAL") -> dict:
anchors = {
"body_height": height_cm * 10, # convert to mm
"body_mass": weight_kg
}
if waist_cm:
anchors["waist_circumference_omphalion"] = waist_cm * 10
payload = {
"input_data": {
"input_unit_system": "metric",
"subject": {
"gender": gender.lower(),
"input_origin_region": region
},
"anchors": anchors
},
"output_settings": {
"calculation": {
"calculation_model": "AUTO",
"target_region": region,
"body_build_type": "CIVILIAN"
},
"requested_dimensions": {
"specific_dimensions": [
"chest_circumference",
"waist_circumference_natural",
"hip_circumference",
"inseam_length",
"shoulder_breadth"
]
},
"output_format": {
"unit_system": "metric",
"include_range_95": True,
"confidence_score_threshold": 65
}
}
}
response = requests.post(
"https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict",
json=payload,
headers={
"X-RapidAPI-Key": "YOUR_API_KEY",
"X-RapidAPI-Host": "dimensionspot-bodysize-engine.p.rapidapi.com",
"Content-Type": "application/json"
}
)
return response.json()
Size chart mapping
The API returns measurements in millimeters. Your size chart lives in some unit — inches, centimeters, or brand-specific measurement points. The mapping layer translates between them.
def map_to_size(chest_mm: float, waist_mm: float, hip_mm: float,
size_chart: dict) -> str:
"""
size_chart example:
{
"XS": {"chest": (800, 855), "waist": (640, 695), "hip": (870, 925)},
"S": {"chest": (855, 910), "waist": (695, 750), "hip": (925, 980)},
"M": {"chest": (910, 965), "waist": (750, 805), "hip": (980, 1035)},
"L": {"chest": (965, 1020),"waist": (805, 860), "hip": (1035, 1090)},
"XL": {"chest": (1020, 1080),"waist":(860, 920), "hip": (1090, 1150)},
}
All values in mm.
"""
candidates = []
for size, ranges in size_chart.items():
chest_ok = ranges["chest"][0] <= chest_mm < ranges["chest"][1]
waist_ok = ranges["waist"][0] <= waist_mm < ranges["waist"][1]
hip_ok = ranges["hip"][0] <= hip_mm < ranges["hip"][1]
match_count = sum([chest_ok, waist_ok, hip_ok])
candidates.append((size, match_count))
# Return size with most matches; if tie, prefer the looser fit
candidates.sort(key=lambda x: -x[1])
return candidates[0][0] if candidates else "M"
For garments where fit priority varies — tops prioritize chest, trousers prioritize waist and inseam — weight the match logic accordingly.
Handling uncertainty: the border case
The prediction interval matters most for users who sit near a size boundary. If a user’s predicted chest circumference is 911mm and the M/L boundary is at 910mm, the prediction falls in L by 1mm — well within the margin of uncertainty.
def recommend_with_confidence(dims: dict, size_chart: dict) -> dict:
chest = dims["chest_circumference"]
waist = dims["waist_circumference_natural"]
hip = dims["hip_circumference"]
# Primary recommendation
primary = map_to_size(chest["value"], waist["value"], hip["value"], size_chart)
# Check if range_95 upper bound crosses into next size
chest_upper = chest["range_95"][1] if chest.get("range_95") else chest["value"] * 1.06
alt_size = map_to_size(chest_upper, waist["value"], hip["value"], size_chart)
return {
"primary": primary,
"alternative": alt_size if alt_size != primary else None,
"note": "Measurements near size boundary — we recommend sizing up if you prefer a relaxed fit."
if alt_size != primary else None
}
Showing the alternative keeps users from returning when the fit is unexpectedly tight. “We recommend M, or L if you prefer more room” is a low-cost intervention that addresses the border-case return.
Regional calibration for global platforms
If your platform ships internationally, the regional calibration parameter matters. Body proportions differ systematically across populations — a 170cm, 65kg woman in Japan and a 170cm, 65kg woman in Germany have statistically different average circumferences.
# Map user's country to a regional calibration profile
REGION_MAP = {
"DE": "EUROPE", "FR": "EUROPE", "GB": "EUROPE", "IT": "EUROPE",
"JP": "ASIA_PACIFIC", "KR": "ASIA_PACIFIC", "CN": "ASIA_PACIFIC",
"IN": "INDIA",
"BR": "LATAM", "MX": "LATAM", "CO": "LATAM",
"NG": "AFRICA", "ZA": "AFRICA", "EG": "AFRICA",
"SA": "MIDDLE_EAST", "AE": "MIDDLE_EAST",
}
def get_region(country_code: str) -> str:
return REGION_MAP.get(country_code.upper(), "GLOBAL")
Setting both input_origin_region and target_region to the user’s region produces predictions calibrated to their population’s body proportions. For a platform with significant Asian or Indian traffic, this is a meaningful accuracy improvement on torso circumference predictions.
What to store downstream
The stateless API stores nothing. Your decision is what to persist in your own infrastructure.
Minimum: Don’t store the raw measurements at all. Store only the recommended size per garment category: user_id → {tops: "M", trousers: "32R", dresses: "S"}.
Useful: Cache the measurement profile per user session (not across sessions) for multi-garment shopping. This avoids repeated API calls during a single browsing session.
Avoid: Storing raw biometric measurements (chest_circumference: 932mm) linked to a user ID in a durable database. This creates biometric data in your infrastructure, which may trigger GDPR special category obligations. The size recommendation, not the measurement, is what you need.
Measuring success
The metric to track is fit-related return rate, segmented by users who used the sizing feature vs. those who didn’t. Most platforms see meaningful improvement when the sizing flow completion rate is high — the problem is usually completion rate, not algorithm accuracy.
Track sizing feature completion rate as your primary implementation metric, and fit return rate as your north star. If completion rate is below 60%, the input collection form is the problem. If completion rate is high but return rate doesn’t improve, the size chart mapping is the problem.