BodyFits

Developers

Add body scanning to any website

A hosted scanner, a signed single-use link and a small REST API. Your secret key stays on your server; your customer only ever sees a short-lived scan link.

Free plan: 100 test scans and 20 live scans a month. Create keys, allowed domains and webhooks yourself in the dashboard.

POST /v1/scan-sessions
Authorization: Bearer sk_test_…

{ "external_ref": "order-1042" }

→ 201
{ "scan_url": "https://…/s/…" }

Illustrative request — see the reference below for exact fields.

1

Get your keys

Sign up for a business account and create a test or live key in the dashboard. pk_… keys are public; sk_… keys are secret and only ever used on your server.

2

Create a scan session

Your server calls POST /v1/scan-sessions. You get back a single-use scan_url that is valid for 30 minutes.

3

Open the scanner

Send the customer to scan_url, open it in a new tab, or embed it in an iframe on your site.

4

Receive the result

We send a signed scan.completed webhook, redirect to your redirect_url with session_id and scan_id, and post a tm:completed message to your page. Fetch the full result with GET /v1/scans/{id}.

1 · Create a scan session (server-side)

Authenticate with your secret key. All fields are optional. Pre-filled values skip those questions for your customer.

curl -X POST https://YOUR-TAILORMEASURMENTS-HOST/v1/scan-sessions \
  -H "Authorization: Bearer $TM_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_ref": "customer-1234",
    "prefill": { "height_cm": 175, "weight_kg": 72, "age": 30, "gender": "male" },
    "garment": "thobe",
    "locale": "en",
    "redirect_url": "https://your-store.com/measurements/done"
  }'
{
  "session_id": "ses_…",
  "token": "eyJhbGciOiJIUzI1NiIs…",
  "scan_url": "https://YOUR-TAILORMEASURMENTS-HOST/s/eyJhbGciOiJIUzI1NiIs…",
  "expires_at": 1798000000,
  "mode": "test"
}

2 · Open the scanner

Redirect, open a new tab, or embed. The iframe needs camera and motion-sensor permissions.

<iframe src="SCAN_URL"
  allow="camera; microphone; accelerometer; gyroscope; magnetometer"
  style="width:100%;height:100dvh;border:0"></iframe>

<script>
  window.addEventListener('message', (e) => {
    if (e.origin !== 'https://YOUR-TAILORMEASURMENTS-HOST') return;
    const m = e.data;
    if (m.type === 'tm:phone_connected') console.log('customer continued on their phone');
    if (m.type === 'tm:completed') console.log('scan', m.scan_id, m.measurements);
    if (m.type === 'tm:closed')    console.log('user closed the scanner');
    if (m.type === 'tm:failed')    console.log('scan failed', m.error);
  });
</script>

Events: tm:ready, tm:phone_connected, tm:completed (scan_id, external_ref, measurements), tm:closed, tm:failed. Messages are only delivered to your allowed domains. Without a parent window, the customer is redirected to redirect_url?session_id=…&scan_id=…&status=completed.

Desktop → phone (QR) handoff

You don’t need to do anything extra. When SCAN_URL is opened on a laptop or desktop (no usable rear camera, no touch, or a wide screen), the scanner shows a branded Continue on your phone screen with a QR code, a copyable link, an expiry countdown and a scan on this device anyway option. The QR opens a short-lived link that is bound to one phone and never contains your API key. Your page keeps the iframe / tab open and sees live progress:

3 · Fetch results (server-side)

curl https://YOUR-TAILORMEASURMENTS-HOST/v1/scans/SCAN_ID -H "Authorization: Bearer $TM_SECRET_KEY"
curl "https://YOUR-TAILORMEASURMENTS-HOST/v1/scans?limit=30" -H "Authorization: Bearer $TM_SECRET_KEY"
curl -X DELETE https://YOUR-TAILORMEASURMENTS-HOST/v1/scans/SCAN_ID -H "Authorization: Bearer $TM_SECRET_KEY"
curl https://YOUR-TAILORMEASURMENTS-HOST/v1/usage -H "Authorization: Bearer $TM_SECRET_KEY"
{
  "entry": {
    "id": "scan_…", "status": "success", "mode": "test",
    "session_id": "ses_…", "external_ref": "customer-1234",
    "input": { "height_cm": 175, "weight_kg": 72, "age": 30, "gender": "male", "garment": "thobe", "unit": "cm" },
    "measurements": [ { "name": "chest", "value": 98.5, "unit": "cm", "confidence": 0.82, … } ],
    "server_measurements": [ … ],   // raw engine output, before any edits by the customer
    "quality": { …, "server_verified": true },
    "files": [
      { "id": "file_…", "kind": "model_glb", "content_type": "model/gltf-binary", "size_bytes": 412345,
        "url": "https://YOUR-TAILORMEASURMENTS-HOST/api/files/file_…?exp=…&sig=…" },   // signed, expires after 15 minutes
      { "id": "file_…", "kind": "photo_front", … }      // only if photo storage is on (off by default)
    ]
  }
}

File URLs are signed and expire after 15 minutes — fetch the scan again for a fresh link. DELETE /v1/scans/{id} deletes the scan and its stored 3D model and photos.

4 · Webhooks

Set one endpoint per organization in Dashboard → Webhooks. Events: scan.completed and scan.failed (plus webhook.test from the “Send test event” button). Any 2xx reply counts as delivered; otherwise we retry after 10 s, 1 min, 5 min, 30 min and 2 h. Every attempt is listed in the dashboard.

POST https://your-server.com/webhooks/tailormeasurments
Content-Type: application/json
TM-Event-Id: evt_…
TM-Event-Type: scan.completed
TM-Signature: t=1798000000,v1=5f2c…  // HMAC-SHA256(secret, "<t>.<raw body>") in hex

{ "id": "evt_…", "type": "scan.completed", "created": 1798000000,
  "data": { "scan": { "id": "scan_…", "external_ref": "customer-1234", "measurements": [ … ], "files": [ … ] } } }

Verify the signature (Node.js)

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyTmSignature(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;          // replay protection
  const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
  const given = Buffer.from(v1, 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}

Python

import hmac, hashlib, time

def verify_tm_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Always verify the raw bytes before parsing JSON, and de-duplicate on id — a retried event keeps the same id. A full working receiver is in examples/client-demo/server.mjs.

Keys, quota and errors

pk_test_ / pk_live_Public identifier. Safe to expose.
sk_test_ / sk_live_Secret. Server only. Shown once when created; we store only a hash. Revoke it in the dashboard and create a new one if it leaks.
PrivacyThe 3D model is kept with each scan. Customer photos are only stored if you turn it on in Settings. Deleting a scan deletes its files.
Test vs liveTest scans are labelled TEST MODE and have their own quota. Scans are only visible to keys of the same mode.
QuotaMonthly live-scan allowance from your plan (see pricing). Only successful scans count. Paid plans with overage keep working past the allowance and the extra scans are shown in Billing; otherwise 429 when exhausted.
402Payment required: the subscription is unpaid after its grace period. Update payment in Dashboard → Billing. Test-mode scans keep working.
401 / 403 / 409Bad or revoked key, or an expired or invalid link / organization suspended / link already used.

Plans and billing

Every new organization starts on Free. Upgrade in Dashboard → Billing, monthly or yearly (yearly is discounted). Upgrades and downgrades are prorated immediately; cancelling keeps your plan until the end of the paid period. If a renewal payment fails you keep your plan for a grace period while the card is retried. Invoices are in Billing. See /pricing for plans, or contact us for Enterprise.