architectureecommerceomnichannelsizingdeveloper-guide

Omnichannel Size Recommendation: One Architecture for Web, Mobile, and In-Store

· 5 min read · Martin Hejda

The promise of omnichannel retail is a consistent customer experience across touchpoints. The reality of how sizing features get built is usually: a quick integration on the website, a different quick integration in the mobile app, and eventually an in-store kiosk that runs on completely separate logic because the agency that built it didn’t have time to integrate with the backend.

The result: a user who measured themselves on the website gets recommended size M. The mobile app recommends L. The in-store kiosk tells them to try XL. They return 60% of their online purchases.

Building omnichannel sizing correctly means building it once — a shared sizing service — and exposing it across all channels. Here’s how to design that.


The shared sizing service model

The core principle: sizing logic lives in one place. Every channel calls the same service.

┌──────────────────────────────────────────────────────────┐
│                    Sizing Service                        │
│  ┌───────────────────┐   ┌──────────────────────────┐   │
│  │ Measurement       │   │ Size Chart Registry      │   │
│  │ Prediction Layer  │   │ (brand + category + size │   │
│  │ (height + weight  │   │  → body dimension ranges)│   │
│  │  → 130 dimensions)│   │                          │   │
│  └───────────────────┘   └──────────────────────────┘   │
│            │                         │                   │
│            └─────────┬───────────────┘                   │
│                      ▼                                   │
│            ┌──────────────────┐                          │
│            │ Matching Engine  │                          │
│            │ (dimensions →    │                          │
│            │  size label)     │                          │
│            └──────────────────┘                          │
└──────────────────────────────────────────────────────────┘
         │              │               │
         ▼              ▼               ▼
    Web Frontend    Mobile App    In-Store Kiosk
    (Next.js)       (React Native) (Electron/Kiosk)

Each channel calls the same REST API endpoint. The response is the same regardless of which channel asked.


API design

The sizing service exposes one core endpoint:

POST /sizing/v1/recommend

Request body:

{
  "user_profile": {
    "gender": "female",
    "height_cm": 168,
    "weight_kg": 62,
    "region": "EUROPE"
  },
  "product": {
    "brand": "brand_a",
    "category": "womens_tops",
    "product_id": "optional-for-product-specific-chart"
  },
  "options": {
    "include_adjacent_sizes": true,
    "prefer_direction": "up"
  }
}

Response:

{
  "recommended_size": "M",
  "confidence": "HIGH",
  "adjacent_sizes": {
    "smaller": "S",
    "larger": "L"
  },
  "fit_notes": "Chest fits well. Waist is near the upper boundary of M — consider L if you prefer a relaxed fit.",
  "dimensions_used": {
    "chest_circumference": 924,
    "waist_circumference_natural": 726,
    "hip_circumference": 965
  },
  "size_chart_ranges": {
    "M": {
      "chest_min_mm": 901, "chest_max_mm": 960,
      "waist_min_mm": 711, "waist_max_mm": 760
    }
  }
}

Service implementation

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import requests

app = FastAPI()

class UserProfile(BaseModel):
    gender: str
    height_cm: float
    weight_kg: float
    region: str = "GLOBAL"

class ProductRequest(BaseModel):
    brand: str
    category: str
    product_id: str | None = None

class RecommendOptions(BaseModel):
    include_adjacent_sizes: bool = True
    prefer_direction: str = "up"  # "up" or "down"

class SizingRequest(BaseModel):
    user_profile: UserProfile
    product: ProductRequest
    options: RecommendOptions = RecommendOptions()

@app.post("/sizing/v1/recommend")
async def recommend_size(req: SizingRequest):
    # Step 1: Get body dimension predictions
    dimensions = await _get_predicted_dimensions(req.user_profile)
    
    # Step 2: Load size chart for brand/category
    size_chart = _load_size_chart(req.product.brand, req.product.category)
    if not size_chart:
        raise HTTPException(status_code=404, detail=f"No size chart for {req.product.brand}/{req.product.category}")
    
    # Step 3: Match dimensions to size
    recommendation = _match_to_size(dimensions, size_chart, req.options)
    
    return recommendation

async def _get_predicted_dimensions(profile: UserProfile) -> dict:
    """Call body measurement prediction API."""
    response = requests.post(
        "https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict",
        json={
            "input_data": {
                "input_unit_system": "metric",
                "subject": {"gender": profile.gender, "input_origin_region": profile.region},
                "anchors": {
                    "body_height": int(profile.height_cm * 10),
                    "body_mass": profile.weight_kg
                }
            },
            "output_settings": {
                "calculation": {"target_region": profile.region, "body_build_type": "CIVILIAN"},
                "requested_dimensions": {
                    "specific_dimensions": [
                        "chest_circumference",
                        "waist_circumference_natural",
                        "hip_circumference",
                        "inseam_length",
                        "shoulder_breadth"
                    ]
                },
                "output_format": {"include_range_95": True, "confidence_score_threshold": 60}
            }
        },
        headers={
            "X-RapidAPI-Key": "YOUR_API_KEY",
            "X-RapidAPI-Host": "dimensionspot-bodysize-engine.p.rapidapi.com"
        }
    )
    data = response.json()
    return {
        dim_id: {
            "value_mm": dim.get("value"),
            "range_95": dim.get("range_95")
        }
        for dim_id, dim in data.get("body_dimensions", {}).items()
    }

Handling profile persistence across channels

A user who measured themselves on the website should get the same recommendation on the mobile app without re-entering their measurements. This requires storing the profile on the server side.

import redis
import json
import hashlib

r = redis.Redis()

def get_or_create_sizing_profile(user_id: str, profile_data: dict | None) -> dict:
    """
    Load an existing sizing profile for a user, or create a new one from provided data.
    Anonymous users (no user_id) always use the provided profile data directly.
    """
    if not user_id:
        # Anonymous session: use provided data, no persistence
        return profile_data or {}
    
    cache_key = f"sizing_profile:{user_id}"
    stored = r.get(cache_key)
    
    if stored and not profile_data:
        # Return existing profile
        return json.loads(stored)
    
    if profile_data:
        # Store/update profile
        r.setex(cache_key, 86400 * 180, json.dumps(profile_data))  # 6 months TTL
        return profile_data
    
    return {}

When a user logs in on mobile, their previously-entered web profile is available. When they visit an in-store kiosk with their account, same profile. The sizing recommendation is consistent because the inputs are consistent.


In-store kiosk considerations

In-store kiosks have a few specific requirements:

Offline fallback: A kiosk that fails to reach the sizing service should still work. Pre-load size chart data to local storage and fall back to a simpler heuristic if the backend is unreachable.

Privacy in a public space: Kiosks are shared devices. Don’t display previous users’ data. Don’t store local copies of sizing profiles. Each session should be ephemeral.

Alternative inputs: In-store, staff may assist. Consider allowing staff to input height measured on-site, which improves prediction accuracy compared to self-reported height.

Physical size samples: Kiosk recommendations should translate to nearby rack locations. Include a “find this size in store” function that maps the size recommendation to a physical rack or bin location.


Avoiding channel-specific drift

Over time, separate teams making separate decisions cause channel-specific sizing logic to diverge. Common failure modes:

  • Mobile team hardcodes a “mobile-specific” size chart that never gets updated
  • Website integrates a second sizing vendor for a subset of brands
  • In-store kiosk uses a locally-maintained spreadsheet that’s 18 months out of date

The prevention is architectural: no channel contains sizing logic. Every channel calls the same service. The service owns all size chart data. Size chart updates happen once, in the service, and propagate to all channels immediately.

This requires discipline on the organizational side — teams must resist the temptation to “just hardcode this for the mobile app” — but the architecture makes the right thing easy: there’s one place to update, and all channels benefit.


Testing the shared service across channels

Test the same input across all channels and verify identical output:

def cross_channel_consistency_test(
    user_profile: dict,
    product: dict,
    base_url: str
) -> dict:
    """
    Call the sizing service from 'web', 'mobile', and 'kiosk' contexts
    and verify responses are identical.
    """
    results = {}
    
    for channel in ["web", "mobile", "kiosk"]:
        response = requests.post(
            f"{base_url}/sizing/v1/recommend",
            json={
                "user_profile": user_profile,
                "product": product,
                "options": {}
            },
            headers={"X-Channel": channel}
        )
        results[channel] = response.json()
    
    # Check all channels agree on recommended_size
    sizes = {ch: r["recommended_size"] for ch, r in results.items()}
    consistent = len(set(sizes.values())) == 1
    
    return {
        "consistent": consistent,
        "sizes_by_channel": sizes,
        "details": results
    }

Run this test in your CI pipeline whenever you deploy changes to the sizing service. Any inconsistency between channels indicates a bug.


Omnichannel sizing is an architecture problem before it’s a technology problem. The technology choice — which prediction API, which matching algorithm, which data store — matters less than the decision to centralize sizing logic in a single service from the start. Make that decision early and enforce it organizationally, and the channel-specific implementations become thin wrappers rather than independent systems.

Try DimensionsPot

Free tier — 100 requests/month, no credit card required.

Get API on RapidAPI