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=YourOrgIdCreate 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:
| Field | Description |
|---|---|
lifetime | Token lifetime in seconds. Maximum and default: 31536000 (1 year). |
customScanId | Free-form text (max 512 chars) attached to the scan. Useful for linking scans to your own IDs. Does not need to be unique. |
scope | Additional 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=metricPath parameters
| Parameter | Description |
|---|---|
LOCALE | UI language code. See Supported locales. |
ORG_ID | Your organization ID. |
Query parameters
| Parameter | Required | Description |
|---|---|---|
token | Yes | The scan token from the previous step. |
system | No | metric (cm/kg, default) or imperial (ft/in/lb). |
height | No | Prefill. Metric: 50–250 cm. Imperial: 2–8 ft. |
inches | No | Prefill (imperial only). 0–11 when height < 8, 0–2 when height = 8. Ignored in metric. |
weight | No | Prefill. Metric: 10–200 kg. Imperial: 22–441 lb. |
age | No | Prefill. 5–90. Values above 90 are clamped to 90. |
gender | No | Prefill. male or female. |
screens | No | Comma-separated list of screens to show. See Screens. |
remove-header | No | true to hide the Bodygram header on every screen (also hides the close icon on the scan page). Default false. |
tap | No | true to show a tap screen before the scan screen. Required on iOS for audio playback. Default false. |
show-share | No | true to show a share button on the result screen. Default false. |
can-save-as-image | No | true to show a "save as image" button on the result screen. Default false. |
camera-constraints | No | URL-encoded JSON matching MediaTrackConstraints. Default null. |
show-silhouette | No | hide-during-guidance to hide the silhouette during guidance. Default always-on. |
debugger | No | true to show a pose debugger on the scan screen. Not for production. Default false. |
scan-type | No | default, 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.
name | Description | Mandatory | Note |
|---|---|---|---|
form | Input form for age, gender, height, weight. | No | To skip, pass all four values via query parameters. |
how-to-scan | Instructions on how to scan. | No | — |
can-hear-voice | Instructions on voice guidance. | No | If this is the first screen, add &tap=true — iOS needs a tap gesture before playing audio. |
gyro-permission | Instructions to place/hold the device. | No | Required if you use the gyro screen. |
gyro | Gyro alignment screen. | No | Must be preceded by gyro-permission. |
scan | Camera + scan UI. | Yes | Camera aspect ratio may differ from the device screen — expect black/white bars on top/bottom or sides. |
result | Scan result screen. | No | — |
all | All screens, in the default order. | No | Default 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=trueTo 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,scanUse 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%7DOpen 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 }:
type | When it fires | payload |
|---|---|---|
0 | User tapped the close (×) button. | {} |
2 | Scan completed successfully. | Scan result object. |
3 | Scan failed. | Error information. |
4 | Scanner started processing the captured images. | { base64_jpeg_front, base64_jpeg_side } |
5 | Model 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.
Related
- Send photos directly (API only) — skip the hosted UI and call the REST API yourself
- Android WebView integration — load the Scanner URL inside a native Android app
- PWA Scanner (offline) — an offline-capable variant of the scanner
- API Reference — full endpoint, schema, and error code reference
