Skip to content

Theme integration API

Released to the public Aabhas app and private Pilot on 2026-10-14 IST. The public version is public-theme-contract-44f5841; Pilot uses pilot-theme-contract-44f5841. Each has its compatible backend. Other custom app identities require separate releases. Verify your store’s loaded version. Feature-detect window.AabhasVto?.getState and keep the native experience when it is absent. These hooks target Shopify Liquid themes, not the Hydrogen SDK. They are designed for compatible themes without brand-specific code.

Preview features still require acceptance in your theme. Crop-cancel state, result/close ordering, in-flight section removal and malformed saved-history IDs now have regression coverage. The real pilot modal and unchecked consent gate were verified, and the operator reported a successful pilot try-on after accepting consent. Public release was authorized with result export and Safari/device verification outstanding, along with remaining live integration checks. Validate these before launching your custom experience; do not add theme workarounds that intercept requests or replace controls.

Your theme owns the product rail, environment, decorative animation, copy and branded share composition. Aabhas continues to own the visible native uploader, crop, consent, sign-in, reuse, generation, history, results and attribution.

This API is a presentation extension, not a headless uploader or generation SDK. Do not hide and simulate native controls, intercept fetch/XHR, scrape private markup for photos, replace the app modal, or start a second generation. Use modal customization for supported layout decoration. Leave consent, recovery, required branding and native actions usable.

The aabhas:vto:state event originates on the owning .aabhas-vto-block and bubbles to document and window. It is not cancelable. event.detail is a frozen versioned snapshot, not an instruction or a trusted server receipt.

window.AabhasVto.getState(block) returns that block’s latest snapshot, or null before its runtime is lazily mounted and after it is detached. Reading does not load the runtime or open the modal. The initial mount event can arrive before the snapshot reader is registered; use its detail directly.

Install your listener once per block after aabhas:vto:ready, or immediately if the API already exists. Subscribe before reading, so late initialization misses no transition. Unsubscribe when Shopify removes or replaces your section.

Removal disposes the old controller and its portaled modal. Late completions do not reopen it or change the replacement widget. Reinserted disposed DOM remains inactive; initialize a fresh block. Removal does not cancel a server request, delete stored media, or automatically retry a generation.

function observeTryOn(block, render) {
if (!window.AabhasVto?.getState) return () => {};
let revision = -1;
function update(state) {
if (!state || state.version !== 1 || state.revision <= revision) return;
revision = state.revision;
render(state);
}
function onState(event) {
if (event.target === block) update(event.detail);
}
block.addEventListener('aabhas:vto:state', onState);
update(window.AabhasVto.getState(block));
return () => block.removeEventListener('aabhas:vto:state', onState);
}

Each snapshot contains:

Field Meaning
version Contract version, currently 1; ignore unsupported versions.
widgetId Page-local controller identity; do not reuse it across navigation.
revision Increasing per controller; use it to deduplicate rendering.
phase Processing/presentation phase listed below.
modalOpen Native view visibility (modal or inline), independent of processing.
operationId Page-local attempt ID, or null before an attempt and for history. Not a job ID.
productId, variantId Context IDs as strings, or null if unknown.
result null or { imageUrl, productId, variantId, source }.
error null or { code, message }, with an allowlisted UI code and generic safe message. Use phase for UI state, not error text.

The payload does not include the uploaded photo, customer identifiers, internal job IDs, attribution tokens or raw server errors. It is a browser UI contract, not an analytics, billing or authorization API. Never log or persist the media URL or inline image data. Other scripts on the same page can observe DOM events; only install trusted theme scripts.

Phase Meaning
idle Initialized/reset, uploader reopened, or an unaccepted crop canceled.
cropping A selected file is being opened for the crop editor. It is not yet accepted.
photo_ready A crop was accepted or a saved photo selected for reuse.
preparing A new attempt started; configuration/readiness is being resolved.
uploading Customer media is being resolved: upload, existing reference, or inline data.
submitting The generation request is in flight. This does not prove GPU start or percent complete.
succeeded A result URL/data URL is available for presentation, including history.
failed The flow could not continue. Keep native error and recovery controls visible.

Typical order: idle -> cropping -> photo_ready -> preparing -> uploading -> submitting -> succeeded/failed. Reuse can skip cropping. Internal recovery may repeat upload/submission phases with the same operation ID. A shopper’s new attempt gets a new operation ID. Do not require every intermediate phase or a fixed interval between events.

Render idempotently: revisions also change on modal visibility changes, and the same phase can be emitted more than once. Gate one-time visual effects on a new operation/result, not on every succeeded notification. Do not emit duplicate Aabhas success analytics from these events.

failed describes the browser flow, not proof that a timed-out server request could not have completed. Keep native recovery instructions and never automatically submit another generation from an error listener.

The upload modal closes automatically before generation. modalOpen: false does not mean canceled, failed or complete. Closing never cancels an in-flight request. A successful result opens the native result view. Closing that view retains its result snapshot until reset, new photo/attempt or reopening the uploader. Reopening the uploader clears old terminal results but preserves any still-running attempt.

Canceling a crop (including choosing another photo) returns to idle, with result, error and operationId cleared. Canceling the file picker leaves that state unchanged. Closing an unaccepted crop also returns to idle; late image decoding cannot reopen it. This does not cancel a submitted generation.

History has result.source === 'history' and operationId === null; it must not trigger a fresh generation celebration or success analytics. Use the result’s own product/variant IDs for product links, never the rail’s current selection. Missing or malformed history IDs become null, not a guessed current-product ID. Only string IDs are exposed; omit product/cart shortcuts when identity is unknown. History selection is ignored while photo work or generation is pending.

Existing ready, before-open, login-redirect, modal-ready and result-visible hooks remain supported. modal-ready runs once, not on every open. Pre-mount asset/config errors, sign-in redirects and rejected product opens are not lifecycle events; handle the openForProduct promise as well.

The succeeded snapshot is published after the native result view opens and before result-visible. A legacy hook can read the new snapshot immediately. Closing with the native close control in either hook remains closed; it is not overwritten by a later success publication. If a state/focus listener closes or removes the view first, result-visible is skipped. Nested notifications can advance revisions during a callback, so use the revision guard shown above.

Use one real Aabhas app block inside your custom section. Your rail must supply the selected Shopify product’s real metadata, not just a marketing tile image:

await window.AabhasVto.openForProduct(block, {
id: '123456789',
variantId: '987654321',
title: 'Selected shirt',
imageUrl: 'https://cdn.shopify.com/s/files/example/shirt.jpg',
images: [
{ id: '456789123', url: 'https://cdn.shopify.com/s/files/example/shirt.jpg' }
],
collectionIds: []
});

Use IDs as strings, HTTPS images, the complete ordered product gallery and actual collection IDs. Serialize Liquid values with json and HTML attributes with escape. category, categoryId and tags are optional. The example IDs/URL are placeholders, not a working product. Close the modal before changing products; switching is blocked during media work or generation.

The promise resolves when opening is handled, not when generation completes. Catch all rejections. Known messages include VTO_BUSY, VTO_INVALID_PRODUCT, VTO_BLOCK_NOT_READY, VTO_BLOCK_REMOVED, and VTO_PRODUCT_UNAVAILABLE. Show a local retry/error message; never silently switch garments or call private generation endpoints. Eligibility, consent and account checks remain enforced.

Set this on the owning block before opening, and only after your integration is working. Native is the default; no admin setting is needed.

block.dataset.vtoProgress = 'custom';
// Restore at any time:
block.dataset.vtoProgress = 'native';

This visually suppresses the native countdown while retaining its accessibility semantics. Status/error copy, consent, branding and result controls remain. The option is per block and lasts only for that page; values other than custom retain the native display. Keep decorative animation aria-hidden, respect prefers-reduced-motion, and provide a static waiting state when motion is off.

Loop your animation during preparing, uploading and submitting; stop only on an actual state transition. Do not infer success from elapsed time. The native countdown is an estimate: it reaches “Finishing…” and keeps waiting, rather than declaring success or aborting at zero. There is no universal timing guarantee. Ask Aabhas for recent measurements for your store; request latency does not include all upload and image-loading time.

result.imageUrl is normally a signed/read URL. Strict no_store initial results can be inline image data URLs. No new base64 mode is added for persisted shops. succeeded means the image address is available, not that its pixels are decoded.

For a canvas, set crossOrigin before src and wait for decoding:

async function resultCanvas(result) {
const image = new Image();
image.crossOrigin = 'anonymous';
image.src = result.imageUrl;
await image.decode();
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext('2d');
if (!context) throw new Error('Canvas unavailable');
context.drawImage(image, 0, 0);
// Draw your readable brand assets here, then export with canvas.toBlob().
return canvas;
}

The media response must permit your storefront origin through CORS. The event alone cannot grant pixel access; displaying an image is not proof it is readable. Verify the actual returned URL from every storefront domain you support. Catch load/decode/export failures, disable the custom download and keep native result viewing available. Do not retry generation or use a third-party image proxy to work around CORS. Ask Aabhas to check delivery configuration instead.

All canvas images, including logos/backgrounds, must be readable. Signed URLs expire and this contract does not refresh them; do not treat them as permanent share links. Generate a downloadable file only on shopper action. If a new revision arrives during asynchronous composition, discard the old composite. Do not automatically upload the output to your own server or third-party service.

Copy must match the configured storage policy and browser reuse behavior. Saved photos can remain on the device; no_store is not a claim that nothing is kept anywhere. See consent and privacy before writing privacy text. This integration does not change retention or consent.

Test desktop/mobile, multiple blocks, section replacement, login/consent, reused photos, failure/retry, cross-product history and slow requests. Verify custom export with actual media delivery. Native-only themes need no changes. Hydrogen integrations continue to use their SDK state; this Liquid DOM API is not added to Hydrogen by this release.

Before adoption, also verify cancellation of the crop/file picker, closing from existing result hooks, removing a section while its generation is still pending, and history entries with missing or malformed identity. Automated regressions cover these cases with mocked media/services; still verify your actual theme. A mobile browser viewport does not by itself prove behavior on a real mobile device.

Public UI code Meaning
TRYON_FAILED Unclassified flow failure.
PHOTO_INVALID Missing photo or unsupported file type.
UPLOAD_FAILED Customer media preparation, reference resolution or upload failed.
GENERATION_FAILED Generation submission/result response failed.
GENERATION_TIMEOUT Generation returned HTTP 504; server outcome may be unknown.
LOGIN_REQUIRED Phone session/login could not continue.
UNAVAILABLE Readiness failed or generation returned HTTP 503.

These are safe UI categories, not raw backend/provider codes. Older v1 extensions may omit error.code; always retain a generic fallback. Never automatically resubmit from an error listener, including on timeout.

In the theme editor, choose Try-on presentation: Inline in this block on the Aabhas app block. Place that block inside your theme-owned chamber/container. Alternatively set block.dataset.vtoPresentation = 'inline' before the first open/lazy mount. The default and any unrecognized value remain modal. Changing presentation after mounting is unsupported; mount a fresh block instead.

Inline retains the same native upload, consent, crop, reuse and result controls inside the owning block. It does not portal to document.body, dim the page, lock page scrolling or claim modal ARIA semantics. Escape closes it only when focus is within the view. Container resizing relayouts the crop; removing the block disconnects its observer. Do not move the root with a MutationObserver. Use --aabhas-vto-inline-height on the block before mounting to customize the crop/result height; keep a usable width and visible controls on mobile.

For compatibility, modalOpen means native view visibility in both modes. Upload still closes before generation and the result reopens in the same block. Your chamber can remain visible around it, driven by lifecycle state. Inline is not an always-mounted/headless uploader or a way to bypass native processing.

The block’s Photo consent wording selector provides only these versioned choices. Both retain Privacy/Terms links, withdrawal wording and Aabhas identity:

Template Version Processing wording
Standard shopify-photo-v2 I agree to Aabhas processing my photo to create this virtual try-on.
Compact shopify-photo-v2-compact Use my photo with Aabhas to create my try-on preview.

There is no arbitrary HTML/copy override for the consent purpose or checkbox preselection. A new shopper must explicitly select consent. Existing v1 records are not migrated as acceptance; matching explicit v2 decisions may be remembered. Changing template/version or policy URLs requires another decision. A checkbox choice, not opening/upload/reuse, records consent. Themes must never set it for the shopper. Withdrawal is separate from deleting previously retained media.

A pending withdrawal is remembered on the device and retried when the widget reopens with an identified customer. An anonymous response is not confirmation that an account’s server consent was withdrawn. If device storage is blocked, the pending choice can only be retained for the current page. A newer explicit choice replaces the pending decision.

Backend v2 support preceded the public and pilot extension releases. A cached older extension can retain its old behavior; verify the loaded version rather than assuming that every open browser tab has updated.

The default presentation stays modal; inline and custom progress are opt-in. Existing theme hooks remain available. Feature-detect getState and retain the native experience when it is absent or returns null; do not infer success from elapsed time. Older installed widgets and Hydrogen retain their existing consent configuration. The new theme widget intentionally requires a fresh explicit v2 decision, so its consent experience is not identical to the old widget.

Release the compatible backend before updating the extension. A new widget on an older backend is not a supported combination for saving v2 consent receipts. For rollback, restore the preceding extension before rolling back its backend.

The reviewed candidate’s initial JavaScript and CSS total 5,360 bytes gzip, 21 bytes above the previous version. The main runtime (19,336 bytes gzip) and runtime styles (3,476 bytes gzip) load only after interaction. No additional initial-page request or third-party dependency is introduced. Repeated widgets share the runtime download and same-product configuration request.

These are generated-asset measurements, not a guarantee of a store’s page speed. Your chamber images, scripts and animation add their own cost. Load those assets only when needed, avoid continuous offscreen animation, and respect reduced motion. Preview on your real theme and target devices before enabling custom UI.