Bodygram

Use API + Scanner UI

Integrate the hosted Bodygram Scanner web app — generate a scan token, build the Scanner URL, and embed it via link, new tab, or iframe.

Guiding your users to capture the best possible photos is often the hardest part of any body-scanning integration. To make that easy, we built Bodygram Scanner — a standalone scanning web app that handles camera permissions, pose guidance, gyro alignment, and image capture out of the box.

This guide walks through generating a short-lived scan token, building the Scanner URL, and embedding it in your app via a link, a new tab, a WebView, or an <iframe>.

Don't have an account yet? Sign up free at platform.bodygram.com — you get 5 free scans to explore the platform.

Your API key must only ever be used on your server. Generate the scan token server-side and hand the token to the client — never ship the API key to the browser.


Prerequisites

  • A Bodygram Organization ID and API key (from your account page)
  • A server endpoint capable of issuing scan tokens

Export your credentials so the snippets below can pick them up:

export API_KEY=YourApiKey
export ORG_ID=YourOrgId

Create a scan token

Each scan token grants its bearer permission to perform a single scan on your behalf. Tokens have a limited lifetime (1 year by default) and are designed to be safely shared with the client.

Call POST /api/orgs/{ORG_ID}/scan-tokens with at minimum the scans:create scope:

curl -X POST "https://platform.bodygram.com/api/orgs/${ORG_ID}/scan-tokens" \
  --header "Content-Type: application/json" \
  --header "Authorization: ${API_KEY}" \
  --data '{
    "scope": [
      "api.platform.bodygram.com/scans:create"
    ]
  }'

A successful response looks like this (token truncated for brevity):

{
  "expiresAt": 1695357195,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6Ik..."
}
  • expiresAt — UNIX timestamp (seconds) when the token expires. Expired tokens are rejected by the API.
  • token — the scan token. Keep this handy, you'll use it when building the Scanner URL.

Token format

The scan token is a standard JSON Web Token (JWT) — feel free to decode and inspect it if you want.

Optional parameters

The scan token endpoint accepts a few optional fields for finer control:

FieldDescription
lifetimeToken lifetime in seconds. Maximum and default: 31536000 (1 year).
customScanIdFree-form text (max 512 chars) attached to the scan. Useful for linking scans to your own IDs. Does not need to be unique.
scopeAdditional scopes. api.platform.bodygram.com/scans:read lets the bearer read the scan result after scanning.
curl -X POST "https://platform.bodygram.com/api/orgs/${ORG_ID}/scan-tokens" \
  --header "Content-Type: application/json" \
  --header "Authorization: ${API_KEY}" \
  --data '{
    "customScanId": "my_custom_id",
    "lifetime": 2630000,
    "scope": [
      "api.platform.bodygram.com/scans:create",
      "api.platform.bodygram.com/scans:read"
    ]
  }'

Build the Scanner URL

Once you have a token, build the Scanner URL:

https://platform.bodygram.com/{LOCALE}/{ORG_ID}/scan?token={TOKEN}&system={SYSTEM}

A filled-in example (token truncated):

https://platform.bodygram.com/en/org_123456abcdef/scan?token=eyJhbGciOiJIUzI1NiIsInR5cCI6Ik&system=metric

Path parameters

ParameterDescription
LOCALEUI language code. See Supported locales.
ORG_IDYour organization ID.

Query parameters

ParameterRequiredDescription
tokenYesThe scan token from the previous step.
systemNometric (cm/kg, default) or imperial (ft/in/lb).
heightNoPrefill. Metric: 50250 cm. Imperial: 28 ft.
inchesNoPrefill (imperial only). 011 when height < 8, 02 when height = 8. Ignored in metric.
weightNoPrefill. Metric: 10200 kg. Imperial: 22441 lb.
ageNoPrefill. 590. Values above 90 are clamped to 90.
genderNoPrefill. male or female.
screensNoComma-separated list of screens to show. See Screens.
remove-headerNotrue to hide the Bodygram header on every screen (also hides the close icon on the scan page). Default false.
tapNotrue to show a tap screen before the scan screen. Required on iOS for audio playback. Default false.
show-shareNotrue to show a share button on the result screen. Default false.
can-save-as-imageNotrue to show a "save as image" button on the result screen. Default false.
camera-constraintsNoURL-encoded JSON matching MediaTrackConstraints. Default null.
show-silhouetteNohide-during-guidance to hide the silhouette during guidance. Default always-on.
debuggerNotrue to show a pose debugger on the scan screen. Not for production. Default false.
scan-typeNodefault, no-weight (weight inferred from images), or no-stats (all stats inferred from images).

To skip the input form screen you must supply all of height, weight, age, and gender via query parameters (plus inches on imperial). Otherwise the scan will fail.

Screens

Use the screens query parameter to pick which screens the user sees — and in what order. Defaults to all.

nameDescriptionMandatoryNote
formInput form for age, gender, height, weight.NoTo skip, pass all four values via query parameters.
how-to-scanInstructions on how to scan.No
can-hear-voiceInstructions on voice guidance.NoIf this is the first screen, add &tap=true — iOS needs a tap gesture before playing audio.
gyro-permissionInstructions to place/hold the device.NoRequired if you use the gyro screen.
gyroGyro alignment screen.NoMust be preceded by gyro-permission.
scanCamera + scan UI.YesCamera aspect ratio may differ from the device screen — expect black/white bars on top/bottom or sides.
resultScan result screen.No
allAll screens, in the default order.NoDefault value.

Skip everything except the scan screen

To go straight to scanning, set screens=scan and prefill the stats via query parameters. On iOS, add &tap=true so audio can play.

https://platform.bodygram.com/en/org_123456abcdef/scan?token=eyJhb&system=metric&height=170&weight=70&age=25&gender=male&screens=scan&tap=true

To include gyro alignment before the scan, use screens=gyro-permission,gyro,scan. On iOS the gyro-permission screen is required — it captures the user tap needed to unlock the gyro API.

https://platform.bodygram.com/en/org_123456abcdef/scan?token=eyJhb&system=metric&height=170&weight=70&age=25&gender=male&screens=gyro-permission,gyro,scan

Use the back camera

Set camera-constraints to {"facingMode": "environment"}. URL-encode the JSON to avoid parsing issues:

const cameraConstraints = encodeURIComponent(
  JSON.stringify({ facingMode: 'environment' })
);

Your final URL will look like:

https://platform.bodygram.com/en/org_123456abcdef/scan?token=eyJhb&camera-constraints=%7B%22facingMode%22%3A%22environment%22%7D

Open the Scanner

With the URL in hand, there are four common ways to get your user scanning.

1. Share the URL

The simplest option — send the URL to the user (SMS, email, QR code) and let them open it in their browser. Zero code required.

2. Open in a new tab

If you already have a web app and just want a button that launches the scanner, open the URL in a new tab:

window.open(scannerUrl, '_blank');

3. Render in a WebView

On native Android or iOS apps, point your WebView component at the URL. The user will need to grant camera and sensor permissions. For Android, follow the dedicated Android WebView guide.

4. Embed in an <iframe>

To host the scanner inside your own web page, load it in an <iframe> with the Bodygram Platform SDK. The SDK handles parent ↔ iframe communication so you can react to scanner events.

Include the SDK on the page:

<script src="https://platform.bodygram.com/sdk.js"></script>

Create the iframe with the required allow attributes and the data-bg-scanflow marker:

<iframe
  src="{BODYGRAM_SCANNER_URL}"
  allow="camera; microphone; accelerometer; magnetometer; gyroscope"
  data-bg-scanflow="true"
></iframe>

All of the allow permissions are required. Missing any of them will prevent the scanner from working.

Initialize the SDK after the iframe is in the DOM:

<script>
  window.BGPlatformSDK.init({
    shouldSetupIframe: true,
  });
</script>

How iframe integration works

shouldSetupIframe: true tells the SDK to wire up internal listeners and post messages to the first iframe that has data-bg-scanflow="true". You own the iframe element — the SDK only handles the communication layer.

Initialization timing

Call init() after the iframe is mounted with data-bg-scanflow. If you call it first, the SDK cannot find the iframe and the scanner will not work.


iframe events

The scanner communicates with the parent window using postMessage. Each event has the shape { type, payload }:

typeWhen it firespayload
0User tapped the close (×) button.{}
2Scan completed successfully.Scan result object.
3Scan failed.Error information.
4Scanner started processing the captured images.{ base64_jpeg_front, base64_jpeg_side }
5Model loading status changed.{ status: 'pending' | 'loaded' | 'failed', model }

Listen for events on the parent window and act accordingly — typically you'll close the iframe on type: 0 and forward the completed payload on type: 2.


Complete iframe example

A minimal page that opens the scanner in an iframe on button click, closes it when the user hits ×, and logs the scan lifecycle events:

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Integrate Scanflow</title>
    <script src="https://platform.bodygram.com/sdk.js"></script>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
    </style>
  </head>
  <body>
    <button id="open-scanflow">Open Scanflow</button>
    <div id="anchor" style="width: 100dvw; height: 100dvh;"></div>

    <script>
      function openScanflow() {
        const anchor = document.getElementById('anchor');
        const scanflow = document.createElement('iframe');
        scanflow.src = '{BODYGRAM_SCANNER_URL}';
        scanflow.style.width = '100%';
        scanflow.style.height = '100%';
        scanflow.setAttribute(
          'allow',
          'camera; microphone; gyroscope; accelerometer; magnetometer;'
        );
        scanflow.setAttribute('data-bg-scanflow', 'true');
        anchor.appendChild(scanflow);
        document.getElementById('open-scanflow').style.display = 'none';

        window.BGPlatformSDK.init({ shouldSetupIframe: true });
      }

      function closeScanflow() {
        const anchor = document.getElementById('anchor');
        anchor.removeChild(anchor.firstChild);
        document.getElementById('open-scanflow').style.display = 'block';
        window.BGPlatformSDK.isInitialized = false;
      }

      window.addEventListener('message', (event) => {
        const { type, payload } = event.data;

        if (type === 0) closeScanflow();
        if (type === 2) console.log('Scan completed', payload);
        if (type === 3) console.log('Scan failed', payload);
        if (type === 4) {
          console.log('Processing scanned images', payload);
          console.log('Front (base64 JPEG):', payload.base64_jpeg_front);
          console.log('Side (base64 JPEG):',  payload.base64_jpeg_side);
        }
        if (type === 5) {
          if (payload.status === 'pending') console.log('Model loading');
          if (payload.status === 'loaded')  console.log(payload.model + ' loaded');
          if (payload.status === 'failed')  console.log('Model failed to load');
        }
      });

      document
        .getElementById('open-scanflow')
        .addEventListener('click', openScanflow);
    </script>
  </body>
</html>

Replace {BODYGRAM_SCANNER_URL} with the URL you built earlier. Without the Bodygram Platform SDK loaded and initialized with shouldSetupIframe: true, the iframe integration will not work.


On this page