Widget API examples
Follow the current Closku widget contract: read products, create a session, submit a photo, and poll the returned job.
Reviewed by Closku documentation team ·
Prepare an allowed storefront
Run this browser example on your configured storefront with your merchant UUID and a product UUID from Closku. Sample IDs are placeholders. The production configuration needs an explicit allowed origin; an empty list denies access. The browser supplies its origin header; page JavaScript should not override it.
Add a file input to your custom integration and select a photo you have permission to process. The example reads that file into a base64 data URL; remote HTTP(S) shopper-photo URLs are rejected. Submitting a try-on starts processing and records usage.
<input id="shopper-photo" type="file" accept="image/jpeg,image/png,image/webp">const base = "https://closku.com";
const merchantId = "MERCHANT_UUID";
async function widgetRequest(path, options = {}) {
const response = await fetch(base + path, {
...options,
headers: { "Content-Type": "application/json", ...options.headers }
});
const body = await response.json();
if (!response.ok) throw new Error(body.error || "request_failed");
return body;
}
const { products } = await widgetRequest(
"/api/widget/products?merchantId=" + encodeURIComponent(merchantId)
);
const product = products.find((item) => item.id === "CLOSKU_PRODUCT_UUID");
if (!product) throw new Error("Map an active Closku product first");
const photoFile = document.querySelector("#shopper-photo")?.files?.[0];
if (!photoFile) throw new Error("Select a shopper photo first");
if (!["image/jpeg", "image/png", "image/webp"].includes(photoFile.type)) {
throw new Error("Use JPEG, PNG, or WebP");
}
if (photoFile.size > 10 * 1024 * 1024) {
throw new Error("The widget accepts photos up to 10 MB");
}
const photoDataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error("Could not read the photo"));
reader.readAsDataURL(photoFile);
});Create a session and submit a photo
const session = await widgetRequest("/api/widget/session", {
method: "POST",
body: JSON.stringify({ merchantId, productId: product.id })
});
if (Date.parse(session.expiresAt) <= Date.now()) {
throw new Error("Create a fresh shopper session before submission");
}
const job = await widgetRequest("/api/widget/try-on", {
method: "POST",
body: JSON.stringify({
merchantId,
sessionId: session.sessionId,
productId: product.id,
imageUrl: photoDataUrl
})
});
console.log(job.jobId, job.status); // Keep this ID for status polling.The session response is HTTP 201 and job submission is HTTP 202. The server verifies the session belongs to the merchant and is unexpired before creating a job. A 202 response does not guarantee a completed render. Keep the job ID and inspect its status instead of immediately submitting another job.
Poll to a terminal result
async function waitForTryOn(jobId, maxWaitMs = 90000) {
const deadline = Date.now() + maxWaitMs;
while (Date.now() < deadline) {
const status = await widgetRequest(
"/api/widget/try-on/" + encodeURIComponent(jobId) +
"?merchantId=" + encodeURIComponent(merchantId)
);
if (status.status === "completed") return status.resultUrl;
if (status.status === "failed") {
throw new Error(status.error || "Try-on failed");
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
throw new Error("Still processing; retain the job ID before retrying");
}
const resultUrl = await waitForTryOn(job.jobId);This timeout is an example client waiting limit, not a server SLA. Stop polling when the shopper closes the widget or the job reaches a terminal state. No idempotency key is implemented for these submissions, so do not automatically replay an uncertain POST.
Catalog and billing workflows
Catalog writes use merchant product tools and CSV import. Billing changes use the dashboard and Polar checkout or portal. Merchant outbound webhooks are not implemented; no callback URL belongs in the try-on request.
Next steps
Check response fields.
Handle job states.
Related pages
Configure storefront access.
Resolve the product IDs in the examples.