Developer guide

Everything a developer needs to add OnMe to a store: the drop-in widget, the REST API, catalog sync, purchase attribution, errors, webhooks and limits.

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 on localhost, 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.

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)

  1. At checkout, read the session id in the browser: const sid = window.OnMe?.getSessionId(); and save it with the order.
  2. 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)

  1. With pk_test_, button appears on localhost and returns a sample result.
  2. Switch to pk_live_ on the verified domain → real result in ≤ 12 s.
  3. onme:add_to_cart adds the right product.
  4. A test order posted to /v1/orders shows in Dashboard → Analytics within a minute.
  5. Dashboard → Sites shows “Install verified”.

Reference

The complete machine-readable API description is at /docs/openapi.yaml (OpenAPI 3.1).