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.
Ownership
Section titled “Ownership”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.
Listen And Read Current State
Section titled “Listen And Read Current State”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 Semantics
Section titled “Phase Semantics”| 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.
Open For A Selected Product
Section titled “Open For A Selected Product”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.
Custom Progress Presentation
Section titled “Custom Progress Presentation”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.
Read And Export The Result
Section titled “Read And Export The Result”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.
Privacy And Compatibility
Section titled “Privacy And Compatibility”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 Error Codes
Section titled “Public Error Codes”| 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.
Inline Presentation (Preview)
Section titled “Inline Presentation (Preview)”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.
Consent Templates (Preview)
Section titled “Consent Templates (Preview)”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.
Compatibility And Page Load
Section titled “Compatibility And Page Load”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.