Bodygram

Body2Fit Integration

Integrate the Headless SDK to give shoppers accurate size recommendations using Bodygram's Body2Fit API.

Who this is for

This guide is for brands that have onboarded with Bodygram and want to build a custom size-recommendation experience — without using the pre-built Body2Fit widget.

With the Headless SDK you own the entire UI. The SDK handles the scanner iframe, photo capture events, measurement estimation, and size recommendation API calls. You decide how and where everything appears on your page.


Before you start

You need three things in place before writing any code.

A Bodygram client key

Your client key is issued by Bodygram during onboarding. It identifies your brand and authorizes API calls to the Body2Fit service.

The client key is safe to include in frontend code — it is not a secret. Do not confuse it with a platform API key, which must never be exposed client-side.

If you do not have a client key yet, contact contact@bodygram.com to start the onboarding process.

Garments uploaded to Bodygram

Size recommendations are garment-specific. Before the SDK can return a size, Bodygram needs your garment measurement data on file.

Your garments are uploaded during onboarding. Each garment is identified by two values you will use throughout the integration:

  • brandId — assigned by Bodygram and provided to you during onboarding.
  • garmentSKU / garmentSku — the same SKU you shared with Bodygram when uploading your garments. The SDK is inconsistent with the casing across methods: getBody2FitIsProductSupported and getBody2FitSizeFitting use garmentSKU (all-caps), while getBody2FitSizeRecommendation uses garmentSku (lowercase ku).

If you need to add or update garments after onboarding, contact your Bodygram account manager.

The Headless SDK loaded

Load the SDK script in your page <head> before calling any SDK methods:

<!-- Prod (stable) -->
<script src="https://headless.body2fit.bodygram.com/sdk.umd.js"></script>

<!-- Beta (latest) -->
<script src="https://headless.stg.body2fit.bodygram.cc/sdk.umd.js"></script>

The UMD build above defines BodygramSDK on window. An ES module build (sdk.es.js) is also available for bundler-based setups. For React, Next.js, Vue, and ES module usage see Loading the SDK.


Initialize the SDK

Create one instance per page with your client key:

const sdk = new BodygramSDK({
  clientKey: 'org_1Ft28OBw925ToHy0FlaIwH',
  locale: 'en',        // optional — 'en' | 'ja' | 'ko' | 'fr' | 'es' | 'zh' | ...
});
OptionTypeRequiredDescription
clientKeystringYesYour Bodygram client key. Throws if empty or missing.
localestringNoLanguage for the scanner UI. Throws if provided but unsupported. See all supported locales.
containerstring | HTMLElementNoCSS selector or DOM element where the scanner iframe mounts. When omitted, the SDK creates a <div> and appends it to <body>.

Check garment support

Not every garment in your catalogue may be set up in Bodygram. Before showing a "Find my size" button or triggering Scanflow, check whether the current garment is supported — and only render the experience if it is. The SDK returns true if the garment is supported, false otherwise.

const supported = await sdk.getBody2FitIsProductSupported({
  brandId,     // provided by Bodygram during onboarding
  garmentSKU,  // note: SKU is all-caps here — different from other SDK methods
});

if (supported) {
  // show the scan button or size recommendation UI
}

garmentSKU — note the capitalisation. This method uses garmentSKU (all-caps SKU), as does getBody2FitSizeFitting, while getBody2FitSizeRecommendation uses garmentSku (lowercase ku). This is an inconsistency in the SDK itself — double-check the casing when calling each method.

This call is lightweight and safe to make on every product page load.


Capture photos with Scanflow

The Headless SDK embeds Bodygram's camera scanner as an invisible iframe on your page. When a user is ready to scan, the iframe opens full-screen, guides them through capturing a front and side photo, and passes the images back to your code as base64-encoded strings.

You stay in control of when the scanner opens, how it's triggered, and what happens after the photos are captured.

Scanflow is one of three capture paths. You can also capture a single image with sdk.selfie(), or skip the camera entirely and estimate from stats alone.

For a full explanation of the scanner lifecycle, supported camera constraints, silhouette options, and event handling, see the Scanflow guide.

Call sdk.scan() to open the camera interface and wait for the user to complete both shots:

const { front, side } = await sdk.scan({
  camera: {
    facingMode: 'environment',
    width: { ideal: 1080 },
    height: { ideal: 1920 },
  },
  silhouette: 'male',    // 'male' | 'female' — defaults to 'female'
  welcomeModal: 'how-to-scan', // 'tap-to-start' | 'how-to-scan' | false
});

// front and side are base64-encoded jpeg strings
console.log(front); // 'data:image/jpeg;base64,...'
console.log(side);  // 'data:image/jpeg;base64,...'

Both values are base64-encoded jpegs ready to pass directly into the estimation methods below.


Capture a selfie

For a lighter alternative to the two-photo Scanflow, call sdk.selfie() to capture a single image. The SDK opens the selfie iframe full-screen, guides the user with face-tracking, and resolves with one base64 image you can pass straight to Selfie estimation.

const capture = await sdk.selfie({
  facingMode: 'user',        // 'user' | 'environment' — defaults to 'user'
  orientation: 'auto',       // 'auto' | 'portrait' | 'landscape' — defaults to 'auto'
  requireGyro: false,        // require device-orientation permission before starting
  isDebug: false,            // show on-screen debug overlay
});

// CaptureResult — image is a base64 data URL ready for getBody2FitSelfieEstimation()
const { image, width, height, capturedAt } = capture;
console.log(image); // 'data:image/jpeg;base64,...'
OptionTypeDefaultDescription
facingMode'user' | 'environment''user'Which camera to open. Selfie defaults to the front-facing camera.
orientation'auto' | 'portrait' | 'landscape''auto'Lock the capture orientation, or let the SDK pick based on the device.
requireGyrobooleanfalseWhen true, the SDK requests DeviceOrientationEvent permission on iOS before starting.
isDebugbooleanfalseRenders an on-screen overlay with face-tracking diagnostics. Useful during integration.

If the user closes the selfie sheet without capturing, the promise rejects with code: 'SELFIE_CLOSED_BY_USER'. If the iframe sends a malformed payload, it rejects with code: 'INVALID_SELFIE_CAPTURE_PAYLOAD'. Handle both alongside any other rejection paths.

try {
  const { image } = await sdk.selfie();
  // …pass `image` to sdk.getBody2FitSelfieEstimation()
} catch (err) {
  if (err.code === 'SELFIE_CLOSED_BY_USER') {
    // user cancelled — fall back to stats input or re-prompt
  }
}

Get an estimation token

Before you can request a size recommendation, you need an estimation token. This token is generated by Bodygram's estimation engine and represents the user's body measurements — computed from two photos and the user's stats, from a single selfie, or from stats alone if you want to skip the camera entirely.

Once created, the token can be saved (in localStorage, a session store, or your backend) and reused on future visits. Returning users can skip the scan entirely.

Photo estimation

Pass the front and side images from sdk.scan() along with the user's basic stats. This is the most accurate method.

const estimation = await sdk.getBody2FitPhotoEstimation({
  front,                // base64 jpeg from sdk.scan()
  side,                 // base64 jpeg from sdk.scan()
  age: 25,              // years
  gender: 'male',       // 'male' | 'female'
  height: 1800,         // millimeters — 180 cm → 1800
  weight: 70000,        // grams — 70 kg → 70000
});

const { token } = estimation.estimations.estimationToken;
const { measurements } = estimation.estimations;

localStorage.setItem('bg-estimation-token', token);

Selfie estimation

Pass the image from sdk.selfie() (or any base64 jpeg you already have on hand) along with the user's height. Every other field is optional — the SDK supports two complementary flows depending on what the user provides:

  • Predict weight — omit weight and (optionally) pass a bodyType of 'SLIM', 'AVERAGE', or 'CURVY'. The model predicts the user's weight from the selfie and applies the body-type adjustment.
  • Known weight — pass weight in grams. bodyType is ignored when weight is provided; supplying both logs a console.warn.
// Predict-weight flow — no weight, optional bodyType
const selfieEstimation = await sdk.getBody2FitSelfieEstimation({
  selfie: image,        // 'data:image/jpeg;base64,...' from sdk.selfie()
  height: 1800,         // millimeters — required (500–2500)
  age: 25,              // optional (0–100)
  gender: 'male',       // optional — 'male' | 'female'
  bodyType: 'AVERAGE',  // optional — 'SLIM' | 'AVERAGE' | 'CURVY'
});

// Known-weight flow — weight provided, bodyType ignored
const selfieEstimationWithWeight = await sdk.getBody2FitSelfieEstimation({
  selfie: image,
  height: 1800,
  weight: 70000,        // grams (10000–200000) — switches to known-weight model
  age: 25,
  gender: 'male',
});

const { token } = selfieEstimation.estimations.estimationToken;
const { measurements } = selfieEstimation.estimations;

Selfie images must be JPEG. The SDK enforces a data:image/jpeg;base64, prefix and rejects PNG or other formats. sdk.selfie() already returns a jpeg; if you are bringing your own image, convert it to jpeg first.

The response uses the same structure as the other estimation methods — only input.inputType differs ("SELFIE_FLOW"; current backends may return "INPUT_TYPE_UNSPECIFIED" on this path, so handle both if you discriminate on it).

Stats estimation

If the user skips scanning or you want to offer a no-camera path, use stats alone. The SDK sends height, age, gender, and weight to Bodygram's estimation engine and returns an equivalent token.

const estimation = await sdk.getBody2FitStatsEstimation({
  age: 25,              // years
  gender: 'male',       // 'male' | 'female'
  height: 1800,         // millimeters — 180 cm → 1800
  weight: 70000,        // grams — 70 kg → 70000
});

// The token encodes the user's estimated measurements.
// Save it to skip re-estimation on future visits.
const { token } = estimation.estimations.estimationToken;

// The measurements array contains individual body dimensions.
const { measurements } = estimation.estimations;

Save token wherever makes sense for your app — localStorage, a React context, a server-side session:

localStorage.setItem('bg-estimation-token', token);

Response shape

All three methods return the same structure. The only difference is input.inputType: "CAMERA_FLOW" for photo estimation, "MANUAL_STATS" for stats estimation, and "SELFIE_FLOW" for selfie estimation (current backends may emit "INPUT_TYPE_UNSPECIFIED" on the selfie path — treat it as the selfie response).

Example success response:

{
  "estimations": {
    "estimationToken": {
      "token": "BY1AGJaDM680SNg2bbAI_jj3dB1us..."
    },
    "measurements": [
      { "estimationType": "HEIGHT",     "value": 1800, "unit": "MILLIMETERS" },
      { "estimationType": "WEIGHT",     "value": 70000, "unit": "GRAMS" },
      { "estimationType": "BUST_GIRTH", "value": 898,  "unit": "MILLIMETERS" },
      { "estimationType": "WAIST_GIRTH","value": 753,  "unit": "MILLIMETERS" },
      { "estimationType": "HIP_GIRTH",  "value": 934,  "unit": "MILLIMETERS" }
    ],
    "avatarData": null,
    "input": {
      "preferredSystemOfMeasurement": "METRIC",
      "tightness": 0,
      "inputType": "MANUAL_STATS"
    }
  }
}

See all measurement types in the API reference.

Error response

When estimation fails, the error may look like:

{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "A human-readable description of the error"
  }
}

The error object shape is not guaranteed — code, message, or the error wrapper itself may be missing or structured differently than shown. Always handle errors defensively and do not rely on a specific shape.

Pass token to the size recommendation API in the next step.


Get size recommendations

Size recommendations map a user's estimation token to one or more products. Pass the token from the previous step alongside the brandId and garmentSku for each product you want a recommendation for — you can request multiple products in a single call.

garmentSku — note the capitalisation. This method uses garmentSku (lowercase ku), while getBody2FitIsProductSupported and getBody2FitSizeFitting use garmentSKU (all-caps). This is an inconsistency in the SDK itself.

const recommendations = await sdk.getBody2FitSizeRecommendation({
  estimationToken,
  productInfo: [
    { brandId, garmentSku },
    // add more products if needed
  ],
});

The response is an array in the same order as productInfo. Each entry contains the recommended size and a goodFit flag indicating whether the recommendation is a strong match for the user's measurements.

Example success response:

[
  {
    "recommendation": {
      "recommendedSize": "M",
      "goodFit": true
    },
    "productInfo": {
      "brandId": "bodygram-client",
      "garmentSku": "body2fit-garment-1"
    }
  }
]

Error response

{
  "code": "INVALID_ESTIMATION_TOKEN", // or something else
  "message": "A human-readable description of the error"
}

Common causes include an expired or malformed token, an incorrect brandId, or a garmentSku that does not match what was uploaded to Bodygram — verify all three if you receive an error. The error object shape is not guaranteed and may differ from what is shown above — always handle errors defensively and do not rely on a specific shape.


Get size fitting

Size recommendations tell you which size to pick. Size fitting goes further — it tells you how each available size fits across specific body areas like the chest, waist, and hips. Use this when you want to show users a detailed breakdown: whether a size runs tight or loose at the bust, how the recommended size compares to going one size up, and so on.

garmentSKU — note the capitalisation. This method uses garmentSKU (all-caps), as does getBody2FitIsProductSupported, while getBody2FitSizeRecommendation uses garmentSku (lowercase ku). This is an inconsistency in the SDK itself.

const fitting = await sdk.getBody2FitSizeFitting({
  estimationToken,
  productInfo: [
    { brandId, garmentSKU },
  ],
});

The response is an array in the same order as productInfo. Each entry contains a result with all available sizes for that garment, and for each size a list of fitting points — individual body measurements and their tightness value at that size.

Example success response:

[
  {
    "result": {
      "estimationToken": { "token": "BY1AGJaDM680SNg2bbAI_..." },
      "avatarType": "UPPER_BODY",
      "sizes": [
        {
          "size": { "index": 2, "name": "M" },
          "recommendationRank": 0,
          "neutralRecommendationRank": 0,
          "tightness": 0,
          "fittingPoints": [
            { "fittingPoint": "BUST_GIRTH",  "tightness": -1 },
            { "fittingPoint": "WAIST_GIRTH", "tightness": 0  },
            { "fittingPoint": "HIP_GIRTH",   "tightness": 0  }
          ]
        },
        {
          "size": { "index": 3, "name": "L" },
          "recommendationRank": 1,
          "neutralRecommendationRank": 1,
          "tightness": 1,
          "fittingPoints": [
            { "fittingPoint": "BUST_GIRTH",  "tightness": 0 },
            { "fittingPoint": "WAIST_GIRTH", "tightness": 1 },
            { "fittingPoint": "HIP_GIRTH",   "tightness": 2 }
          ]
        }
      ],
      "sizeGroups": []
    },
    "productInfo": {
      "brandId": "bodygram-client",
      "garmentSKU": "body2fit-garment-1"
    }
  }
]

recommendationRank: 0 is Bodygram's top recommendation for this user — the best fit. Higher ranks (1, 2, ...) are the next best options if the user wants to explore a looser or tighter fit. tightness on each fitting point indicates how the garment fits at that body area — negative values mean tighter, positive means looser, 0 is a neutral fit.

Error response

Common causes include an expired or malformed token, an incorrect brandId, or a garmentSKU that does not match what was uploaded to Bodygram — verify all three if you receive an error. The error object shape is not guaranteed and may differ from what is shown — always handle errors defensively and do not rely on a specific structure.

{
  "code": "INVALID_ESTIMATION_TOKEN", // or something else
  "message": "A human-readable description of the error"
}

Calculate overall tightness

After you receive the size fitting data, use the calculateOverallTightness utility to reduce a single size's fitting points into one classification describing how that size fits overall. It is a pure, static helper exposed on BodygramSDK.utils — it takes one size from the fitting response and returns a tightness category together with the average tightness across that size's fitting points.

const fitting = await sdk.getBody2FitSizeFitting({
  estimationToken,
  productInfo: [
    { brandId, garmentSKU },
  ],
});

const firstSize = fitting[0].result.sizes[0]; // pass a single size from the fitting response

const [tightnessCategory, averageTightness] = BodygramSDK.utils.calculateOverallTightness(firstSize);

console.log(tightnessCategory);   // e.g. 'best-fit'
console.log(averageTightness);    // mean tightness across fitting points, or NaN for the no-fit cases

BodygramSDK.utils is a static member, so call it on the BodygramSDK constructor (or SDK.utils.… when using the ES module import) — not on the sdk instance you created with new BodygramSDK(...).

Input

Pass a single size object from the fitting response (an element of result.sizes). The utility reads only recommendationRank and fittingPoints[].tightness. It validates the input and throws a BodygramSDK: … error if size is missing/not an object, recommendationRank is not a number, or fittingPoints is missing, empty, or contains a point whose tightness is not a number.

Return value

The utility returns a tuple [category, averageTightness].

  1. Category (string) — one of the values below. The suggested label is how Bodygram's own UI presents each category; you can map them to your own copy.

    CategorySuggested labelWhen it is returned
    'perfect'Perfect FitEvery fitting point is exactly neutral (tightness 0).
    'best-fit'Best FitThe recommended size (recommendationRank 0), neutral average, and every fitting point within ±1.
    'might-fit'Please check the fitThe recommended size (recommendationRank 0), neutral average, but at least one fitting point beyond ±1.
    'variable-fit'Fit variesA non-recommended size (recommendationRank ≠ 0), neutral average, and every fitting point within ±1.
    'big'A little loosePositive average tightness (runs loose) with every fitting point within ±1.
    'too-big'LoosePositive average tightness with at least one fitting point beyond ±1.
    'small'A little tightNegative average tightness (runs tight) with every fitting point within ±1.
    'too-small'TightNegative average tightness with at least one fitting point beyond ±1.
    'no-fit-big'Loose FitAt least one fitting point reaches ±2.5 or beyond and the average is positive; too loose to fit.
    'no-fit-small'Tight FitAt least one fitting point reaches ±2.5 or beyond and the average is negative; too tight to fit.
  2. Average tightness (number) — the mean tightness across the size's fitting points, following the same sign convention as the size fitting response above (negative = tighter, positive = looser). It is 0 for 'perfect'/'best-fit'/'might-fit'/'variable-fit', and NaN for the 'no-fit-big' and 'no-fit-small' cases where the size is not viable.

Use the category to drive user-facing messaging and the numeric value for custom visualizations or analytics.


Full example

A complete end-to-end integration in plain HTML. Copy this, replace the placeholder values, and open it in a browser.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Body2Fit Example</title>

    <!-- Load the Bodygram Headless SDK -->
    <script src="https://headless.body2fit.bodygram.com/sdk.umd.js"></script>
  </head>
  <body>
    <button id="startBtn">Find my size</button>

    <script>
      // ─── Replace these with your actual values from Bodygram ───────────────
      const CLIENT_KEY  = 'YOUR_CLIENT_KEY';   // issued during onboarding
      const BRAND_ID    = 'YOUR_BRAND_ID';     // provided by Bodygram
      const GARMENT_SKU = 'YOUR_GARMENT_SKU';  // the SKU you uploaded to Bodygram
      // ───────────────────────────────────────────────────────────────────────

      async function start() {
        // Step 1 — Initialize the SDK with your client key.
        // The SDK sets up the scanner iframe in the background.
        const sdk = new BodygramSDK({
          clientKey: CLIENT_KEY,
          locale: 'en', // change to match your user's language
        });

        // Step 2 — Check whether this garment is supported before going further.
        // Only show the size-finding experience when Bodygram has data for the product.
        const supported = await sdk.getBody2FitIsProductSupported({
          brandId: BRAND_ID,
          garmentSKU: GARMENT_SKU, // note: SKU is all-caps here
        });

        if (!supported) {
          console.log('This garment is not supported by Bodygram.');
          return;
        }

        // Step 3 — Open Scanflow so the user can take a front and side photo.
        // Both images come back as base64-encoded jpegs.
        //
        // Want to skip the camera entirely? Replace this block with:
        //   const estimation = await sdk.getBody2FitStatsEstimation({
        //     age: 25, gender: 'male', height: 1800, weight: 70000,
        //   });
        const { front, side } = await sdk.scan({
          silhouette: 'male',       // 'male' | 'female'
          welcomeModal: 'how-to-scan',
        });

        // Step 4 — Create an estimation token from the photos and user stats.
        // The token represents the user's body measurements and is reusable —
        // save it to skip the scan on future visits.
        const estimation = await sdk.getBody2FitPhotoEstimation({
          front,
          side,
          age: 25,        // replace with actual user input
          gender: 'male', // replace with actual user input
          height: 1800,   // millimeters — 180 cm → 1800
          weight: 70000,  // grams — 70 kg → 70000
        });

        const estimationToken = estimation.estimations.estimationToken.token;

        // Optional: save the token so returning users skip the scan next time.
        localStorage.setItem('bg-estimation-token', estimationToken);

        // Step 5 — Get the size recommendation for the product.
        // You can pass multiple products in a single call.
        const recommendations = await sdk.getBody2FitSizeRecommendation({
          estimationToken,
          productInfo: [{ brandId: BRAND_ID, garmentSku: GARMENT_SKU }],
        });

        const { recommendedSize, goodFit } = recommendations[0].recommendation;
        console.log(`Recommended size: ${recommendedSize} (good fit: ${goodFit})`);
      }

      document.getElementById('startBtn').onclick = start;
    </script>
  </body>
</html>

Want to see the full flow in action? The Body2Fit Playground lets you run every step live — product support check, scan, estimation, and size recommendation — against Bodygram's demo garments.

On this page