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.
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.
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.
Open the scanner
Send the customer to scan_url, open it in a new tab, or embed it in an iframe on your site.
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"
}- The link works for one completed scan and expires after 30 minutes.
redirect_urlmust be on one of your allowed domains (test keys may also use localhost).garment:thobe,suit,shalwar_kameezorgeneric.
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:
created→phone_opened(you gettm:phone_connected) →scanning→completed/failed/expired- On completion the desktop fires the same
tm:completedmessage (or redirects toredirect_url), the scan is counted once, the signedscan.completedwebhook is sent, and the scan shows in your dashboard marked completed on phone. Results includecompleted_on: "phone" | "same_device". - The phone finishes with “Scan complete — you can return to your computer”.
- Force a view by appending
?mode=phone(scan here) or?mode=desktop(always show the QR) toSCAN_URL.
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. |
| Privacy | The 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 live | Test scans are labelled TEST MODE and have their own quota. Scans are only visible to keys of the same mode. |
| Quota | Monthly 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. |
| 402 | Payment required: the subscription is unpaid after its grace period. Update payment in Dashboard → Billing. Test-mode scans keep working. |
| 401 / 403 / 409 | Bad 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.
