Yes, everything goes through the OnMe API. A brand’s developer picks one of two ways:
| A. Drop-in widget (recommended) | B. Custom UI on the REST API | |
|---|---|---|
| Effort | ~15 minutes | ~1 day |
| What you build | Paste a script + one tag | Your own button, upload, and result UI |
| Who calls the API | The widget does it for you | Your frontend (public key) + backend (secret key) |
| Good for | Most stores, any platform (Woo, Shopify, custom) | Apps, headless stores, fully branded flows |
Both need: an OnMe account → add site → verify domain → copy keys from Dashboard → API keys.
Keys:
pk_live_…public key: safe in the browser, only works from your verified domain.sk_live_…secret key: server only, never in frontend code.pk_test_…/sk_test_…sandbox keys: work onlocalhost, return sample results, cost nothing.
A. Drop-in widget
A1. Add the script (once, site-wide or on product pages)
<script src="https://cdn.onme.overspill.net/v1/widget.js" data-key="pk_live_xxx" async></script>
A2. Place the button on the product page
<onme-tryon
product-id="SKU-123"
product-image="https://brand.com/images/sku-123.jpg"
category="saree"
locale="bn">
</onme-tryon>
| Attribute | Required | Notes |
|---|---|---|
product-id |
yes | Your SKU/ID; must match the catalog |
product-image |
if not synced | Clean product photo, ≥ 768 px, plain background preferred |
category |
if not synced | top, dress, bottom, saree, three_piece, panjabi, abaya, eyewear, hat, earrings, necklace |
locale |
no | en (default) or bn |
If the product is already in the OnMe catalog (synced in A4), only product-id is needed.
A3. Add-to-cart from the result screen (optional)
window.addEventListener('onme:add_to_cart', (e) => {
const { productId } = e.detail;
myStore.addToCart(productId); // your own cart function
});
Other events: onme:open, onme:result, onme:close. Same events are pushed to window.dataLayer if it exists.
A4. Sync your catalog (server, recommended)
curl -X POST https://api.onme.overspill.net/v1/catalog/sync \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"products":[{"id":"SKU-123","title":"Red silk saree","url":"https://brand.com/p/sku-123",
"image_url":"https://brand.com/images/sku-123.jpg","category":"saree","price":4500,"currency":"BDT"}]}'
Up to 500 products per call; idempotent upsert by id. Products become ready after OnMe prepares the image (usually < 1 min). Alternatives: CSV upload or a product feed URL in the dashboard.
A5. Purchase attribution (lets the dashboard show try-on → sales)
- At checkout, read the session id in the browser:
const sid = window.OnMe?.getSessionId();and save it with the order. - From your server after payment:
curl -X POST https://api.onme.overspill.net/v1/orders \
-H "Authorization: Bearer sk_live_xxx" -H "Content-Type: application/json" \
-d '{"order_id":"ORD-9001","session_id":"<sid>","value":4500,"currency":"BDT",
"items":[{"product_id":"SKU-123","quantity":1}]}'
A6. Content Security Policy (if your site uses CSP)
script-src https://cdn.onme.overspill.net;
connect-src https://api.onme.overspill.net https://s3.us-west-004.backblazeb2.com;
img-src https://cdn.onme.overspill.net https://api.onme.overspill.net blob: data:;
B. Custom UI on the REST API
Base URL https://api.onme.overspill.net/v1. All requests JSON. Browser calls use Authorization: Bearer pk_live_… and must come from your verified domain.
B1. Flow
1. POST /uploads → { upload_url, upload_key } (signed URL, 5 min)
2. PUT upload_url ← photo bytes (jpeg/png/webp, ≤ 12 MB) straight to storage
3. POST /tryons → { id, status:"queued", token }
4. GET /tryons/{id} → poll every 1 s (back off to 2 s) until succeeded | failed
5. Show `result_url` (private signed Worker URL, valid about 45 seconds), then POST the display acknowledgement only after the image has actually loaded.
B2. Example (browser, TypeScript)
const KEY = 'pk_live_xxx';
const api = (path: string, init: RequestInit = {}) =>
fetch(`https://api.onme.overspill.net/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json', ...init.headers },
}).then(async (r) => { if (!r.ok) throw await r.json(); return r.json(); });
export async function tryOn(file: File, productId: string, sessionId: string) {
const { upload_url, upload_key } = await api('/uploads', {
method: 'POST', body: JSON.stringify({ content_type: file.type, size: file.size }),
});
await fetch(upload_url, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
const job = await api('/tryons', {
method: 'POST',
headers: { 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ product_id: productId, upload_key, session_id: sessionId, consent: true }),
});
for (let delay = 1000; ; delay = Math.min(delay * 1.3, 2000)) {
await new Promise((r) => setTimeout(r, delay));
const s = await api(`/tryons/${job.id}`, { headers: { 'X-Job-Token': job.token } });
if (s.status === 'succeeded') {
// Render s.result_url. Only after the image has loaded successfully:
// POST /tryons/{job.id}/displayed with X-Job-Token: job.token.
// Cached repeats and repeated acknowledgements do not consume included usage.
return { url: s.result_url as string, jobId: job.id, jobToken: job.token };
}
if (s.status === 'failed') throw s; // s.error_code, s.hint
}
}
Your UI must show the consent text (“Your photo is deleted within 24 hours and never used to train AI”) and send consent: true.
B3. Errors
| HTTP | error_code |
Meaning / what to show |
|---|---|---|
| 400 | invalid_image |
Unsupported or corrupt file |
| 422 | rejected_framing |
Photo doesn’t fit the category; show hint (e.g. “Use a full-body photo facing the camera”) |
| 422 | rejected_moderation |
Photo not allowed |
| 403 | origin_not_allowed |
Request not from your verified domain |
| 404 | product_not_enabled |
Product not in catalog or disabled |
| 402 | quota_exceeded |
Plan limit reached; hide the try-on button |
| 429 | rate_limited |
Too many requests; respect Retry-After |
B4. Webhooks (optional, server)
Set a URL in Dashboard → Webhooks. Events: tryon.succeeded, tryon.failed, product.ready, usage.threshold (80% / 100%). Signed with OnMe-Signature: t=…,v1=HMAC-SHA256(secret, t + "." + body); reject if older than 5 min.
B5. Limits
- 5 tries per shopper session by default (brand-configurable up to 15).
- 10 requests/s per public key; 60 requests/min per IP.
- Result images: WebP, 768×1024 (garments) or same size as the uploaded photo (accessories, pasted back).
Testing checklist (give this to the developer)
- With
pk_test_, button appears onlocalhostand returns a sample result. - Switch to
pk_live_on the verified domain → real result in ≤ 12 s. onme:add_to_cartadds the right product.- A test order posted to
/v1/ordersshows in Dashboard → Analytics within a minute. - Dashboard → Sites shows “Install verified”.
Reference
The complete machine-readable API description is at /docs/openapi.yaml (OpenAPI 3.1).