API documentation

Base URL https://api.layerframes.com. All endpoints are JSON over HTTPS. Versioning is in the path; we never break a released version.

Authentication

Every request carries a bearer key. Secret keys (lf_live_…, lf_test_…) are server-side only. Publishable keys (lf_pub_…) are safe in the browser and are scoped to the domains you allowlist in the dashboard.

headers
Authorization: Bearer lf_live_9c2f7ad1e3b04c...
Content-Type: application/json
LayerFrames-Version: 2026-05-01

Frames — catalog ingestion

A frame is one wearable SKU/colorway. Create frames once; renders reference them by id. You can also sync automatically from a Shopify, BigCommerce, or CSV product feed.

POST/v1/frames
skustringrequired

Your internal identifier. Used to map renders back to your catalog.

namestringrequired

Display name, e.g. "Classic Acetate — Black".

images[]string[]required

1–6 product URLs. Front-on and 3/4 views give the best geometry recovery. A GLB model can be supplied instead via model_url.

measurementsobjectoptional

lens_width_mm, bridge_mm, temple_mm, lens_height_mm. Supplying these makes fit scoring exact; without them we estimate from imagery.

lensobjectoptional

tint (none | gradient | solid | mirror), color hex, opacity 0–1, polarized boolean.

metadataobjectoptional

Arbitrary key/value pairs echoed back on every render and webhook.

POST /v1/frames
curl https://api.layerframes.com/v1/frames \
  -H "Authorization: Bearer lf_live_..." \
  -d '{
    "sku": "CA-BLK-52",
    "name": "Classic Acetate — Black",
    "images": ["https://cdn.yourstore.com/ca-blk-front.jpg",
               "https://cdn.yourstore.com/ca-blk-angle.jpg"],
    "measurements": { "lens_width_mm": 52, "bridge_mm": 18, "temple_mm": 145 },
    "lens": { "tint": "none" }
  }'
201 Created
{
  "id": "frm_classic_acetate_black",
  "sku": "CA-BLK-52",
  "status": "processing",     // -> "ready" in ~40s, or subscribe to frame.ready
  "preview_url": null,
  "created_at": "2026-05-01T12:04:11Z"
}

Also available: GET /v1/frames (paginated list), GET /v1/frames/:id, PATCH /v1/frames/:id, DELETE /v1/frames/:id, and POST /v1/frames/bulk for up to 500 SKUs per request.

Try-on renders

The core endpoint. Give it a subject and a frame; get back a photoreal composite. Synchronous by default (p50 ≈ 800 ms); pass async: true for batch jobs and receive a webhook.

POST/v1/tryon
frame_idstringrequired

A ready frame. Pass an array of up to 8 ids to render a whole set from one face pass — much cheaper than 8 separate calls.

subjectobjectrequired

One of image_url, image_base64, or face_id (a stored face profile from a prior session, if the shopper consented).

options.outputstringoptional

png | jpeg | webp | base64. Default png.

options.sizestringoptional

Up to 2048x2048. Default matches the input aspect ratio at 1024 px on the long edge.

options.lens_tintstringoptional

auto follows the frame record; off renders clear lenses for optical SKUs.

options.backgroundstringoptional

keep | blur | remove | a hex color for on-brand PDP renders.

options.return_fitbooleanoptional

Include measurements and a fit score. Default true.

idempotency_keystringoptional

Safe retries. Identical keys within 24 h return the original render.

200 OK
{
  "id": "try_8Kd21mQz",
  "status": "succeeded",
  "renders": [
    { "frame_id": "frm_classic_acetate_black",
      "url": "https://cdn.layerframes.com/r/8Kd21mQz-1.png",
      "expires_at": "2026-05-08T12:04:11Z" }
  ],
  "fit": {
    "score": 0.91,
    "pupillary_distance_mm": 63.4,
    "face_width_mm": 141.2,
    "notes": ["temple_length_ideal", "bridge_slightly_wide"]
  },
  "latency_ms": 812,
  "usage": { "renders": 1 }
}

Live video sessions

For mirror-style try-on, open a WebSocket session and stream frames. The shopper can swap SKUs mid-session with no re-capture; head tracking persists.

WSS/v1/live
browser
const session = await layerframes.live.connect({
  publishableKey: "lf_pub_9c2f...",
  video: document.querySelector("#cam"),
  frameId: "frm_classic_acetate_black",
  fps: 30,                 // 24–30 typical on mobile
  quality: "balanced",     // "fast" | "balanced" | "max"
});

session.on("ready", ({ latencyMs }) => console.log(latencyMs));
session.swapFrame("frm_aviator_gold");        // instant, no re-capture
const still = await session.capture();        // shareable PNG
session.close();

Live sessions bill at one render per second of active streaming, rounded up, and stop billing the moment the tab is hidden.

Fit scoring & recommendations

Rank your catalog against a shopper's measured face. Great for "frames that fit you" rails and for filtering out SKUs that physically won't work.

POST/v1/recommendations
200 OK
{
  "face": { "width_mm": 141.2, "shape": "oval", "pd_mm": 63.4 },
  "results": [
    { "frame_id": "frm_aviator_gold",  "fit_score": 0.94, "reason": "width_match" },
    { "frame_id": "frm_round_tortoise","fit_score": 0.88, "reason": "shape_contrast" },
    { "frame_id": "frm_square_navy",   "fit_score": 0.61, "reason": "bridge_narrow" }
  ]
}

Webhooks

Register endpoints in the dashboard or via POST /v1/webhooks. Every delivery is signed with HMAC-SHA256 in the LayerFrames-Signature header and retried with exponential backoff for 24 hours.

frame.ready

A catalog asset finished processing and can be rendered.

frame.failed

Ingestion failed; payload includes the reason.

tryon.succeeded

An async render completed.

tryon.failed

An async render failed.

session.started

A shopper opened live try-on.

session.added_to_cart

Add-to-cart fired inside a try-on session.

usage.threshold

You crossed 80% or 100% of a billing threshold.

Errors, rate limits & privacy

422 Unprocessable
{
  "error": {
    "type": "no_face_detected",
    "message": "No face was detected in the supplied subject image.",
    "docs_url": "https://layerframes.com/docs#errors",
    "request_id": "req_7Xa9wQ"
  }
}
401 invalid_key

Missing, revoked, or wrong-mode API key.

403 domain_not_allowed

Publishable key used from an unlisted origin.

404 frame_not_found

Unknown frame_id, or the frame is still processing.

422 no_face_detected

Face too small, occluded, or out of frame.

429 rate_limited

Retry after the seconds given in Retry-After.

503 capacity

Render capacity saturated; safe to retry idempotently.

Rate limits: 60 renders/second on Growth, 10/second in sandbox, custom on Enterprise. Subject images are processed in memory and deleted within 60 seconds unless you pass store_face: true with the shopper's consent. Renders are served from a signed CDN URL that expires in 7 days by default.

SDKs

install
npm i @layerframes/node     # server
npm i @layerframes/react    # storefront components
pip install layerframes     # python
# also: PHP, Ruby, Go, and a Shopify app

Want a sandbox key?

We hand out keys on the intro call so we can pre-load your catalog and show you real renders of your own frames.

Schedule a call