OV25

Prophecy

Install Orbital Vision's embeddable room-visualisation SDK for OV25 configurators or any product catalogue.

Prophecy adds a complete View it in your room experience to a product page. It can discover the current OV25 configuration automatically, or accept one to four images of any other product.

Version

These examples load @orbital.vision/prophecy@1, the major version. Fixes and new features reach your site without you editing anything, and a breaking change would be a deliberate move to @2. If you would rather freeze one exact build, pin the full version instead and update it yourself.

Installation

Script tag with OV25

One line, with nothing in it:

product-page.html
<iframe
  src="https://configurator.orbital.vision/89-public-key/7218"
></iframe>
 
<script src="https://cdn.jsdelivr.net/npm/@orbital.vision/prophecy@1/dist/prophecy.js"></script>

Prophecy detects the configurator.orbital.vision iframe, reads the organisation, public key and product from its URL, and follows the shopper's selections. You do not pass an organisation ID, key, product ID, product images, configurator selector, or button.

The script does not need to come after the iframe. Prophecy waits for the configurator, so a page that mounts it after hydration, or on a later route of a single-page site, still works. That is true even when the tag has attributes such as data-prophecy-session-endpoint. Include the line once; a second copy will not add a second launcher.

The running instance is available as window.prophecy. Use data-prophecy-configurator only if the page contains multiple configurators.

The dialog carries a small footer reading "Powered by OV25 from Orbital Vision". It is part of the experience and is not configurable.

Placing the launcher

By default Prophecy adds its own button, fixed to the corner of the page. To place it yourself, mark a button in your own markup:

product-page.html
<button type="button" data-prophecy-launcher>See it in your room</button>

Prophecy attaches to the first marked element and follows one that a single-page site mounts and unmounts per route. Once a page has marked a launcher, Prophecy stops offering its own. The button stays hidden until Prophecy has confirmed the shop can generate, so a shopper never sees a control that cannot work.

The widget on the configurator

Prophecy also puts a small control in the top right corner of the configurator itself, so a shopper can add what they are looking at to their room without opening the dialog.

Before they have made anything it is a single pill reading "View this product in your room". After that it becomes a stack of the previews they have made with a plus over the corner. Pressing the plus starts a preview of the current product in the room they last used, without opening anything: the plus becomes a filling ring, then a tick, and the new picture joins the stack. Pressing the stack opens the dialog on their previews.

The plus appears only when the product on screen has no preview yet, and only once the configurator has finished changing to it. Changing the fabric while one is being made offers to make that one too, up to the number of previews your account allows at once.

The widget comes with the script tag. A page that sets data-prophecy-button gets the launcher on that element and no widget:

product-page.html
<iframe src="https://configurator.orbital.vision/89-public-key/7218"></iframe>
 
<script
  src="https://cdn.jsdelivr.net/npm/@orbital.vision/prophecy@1/dist/prophecy.js"
  data-prophecy-button="#see-it-in-your-room"
></script>

From npm, add the widget yourself with one call:

room-preview.ts
prophecy.mountOverlay();

npm

Use the package when Prophecy is part of your application bundle or you need programmatic control.

npm install @orbital.vision/prophecy
room-preview.ts
import Prophecy from '@orbital.vision/prophecy';
 
const prophecy = Prophecy.create({
  sessionEndpoint: '/api/orbital/prophecy/session'
});
 
prophecy.mount('#room-preview');

Prophecy.create() does not wait for the configurator. The iframe must already be on the page, or it throws. On a page that mounts the configurator later, create Prophecy after it:

room-preview.ts
// Runs once the product page, and its configurator iframe, are on screen.
function onProductPageReady() {
  const prophecy = Prophecy.create({
    sessionEndpoint: '/api/orbital/prophecy/session'
  });
 
  prophecy.mount('#room-preview');
  prophecy.mountOverlay();
}

The npm package ships an ES module, an IIFE browser bundle, source maps, and TypeScript declarations. jsDelivr is the recommended browser CDN; npm itself is not a browser asset server.

Non-OV25 products

Pass a stable catalogue reference and one to four JPEG, PNG, or WebP images of the exact same product. Images may be CORS-readable HTTP(S) URLs or browser Blob values, with a maximum of 12 MB per image.

external-product.ts
const prophecy = Prophecy.create({
  productReference: 'shopify:variant:SKU-42',
  productImages: [frontUrl, sideUrl, detailBlob],
  productUrl: location.href,
  sessionEndpoint: '/api/orbital/prophecy/session'
});
 
// Replace identity, images and the result URL together during SPA navigation.
prophecy.setProduct({
  reference: 'shopify:variant:SKU-43',
  images: [nextFrontUrl, nextSideUrl],
  url: nextProductUrl
});

For a declarative integration, provide data-prophecy-product-reference, data-prophecy-product-images, data-prophecy-product-url, and data-prophecy-session-endpoint on the script tag.

Who can generate

The organisation chooses one access mode in the dashboard. It decides whether the section below applies to you at all.

ModeWho can generateWhat your site needs
AnyoneAny shopper on an authorised domainThe script tag
A few free, then sign inAnyone, up to a daily allowance per browser, then signed-in customersThe script tag and the session endpoint below
Signed-in customers onlyOnly customers your server has identifiedThe session endpoint below

In free-trial mode, a shopper who has used their allowance does not get an error. Prophecy tells your page, and you send them to your login:

sign-in.ts
const prophecy = Prophecy.create({
  sessionEndpoint: '/api/orbital/prophecy/session',
  onSignInRequired({ freeGenerations }) {
    showSignInPrompt(`You have used your ${freeGenerations} free previews.`);
  }
});

The free allowance is a prompt, not a limit

It is counted against a marker Prophecy keeps in the browser, so a shopper who clears their browser data starts again. Its job is to move people towards signing in. Spending is capped by the organisation's daily limits and token balance, which are enforced on our servers.

Server-side session exchange

The browser calls a same-origin endpoint on your site. That endpoint authenticates the shopper with your normal session, validates the product being viewed, then exchanges your Prophecy server key for a short-lived, product-bound browser session.

Keep the server key on the server

Never expose ORBITAL_PROPHECY_SERVER_KEY in HTML, JavaScript, a public environment variable, or Prophecy options. Orbital derives organizationId from the key and returns it in the short-lived session.

Create the key on your API Keys page and pick the Prophecy Server Key type. It only buys previews. The general Private API Key also reads your product list and swatches, which is more than a preview integration should be able to do if the key ever leaks.

app/api/orbital/prophecy/session/route.ts
export async function POST(request: Request) {
  const account = await requireSignedInAccount(request);
  const input = await request.json();
 
  // Confirm this product is genuinely available on the current page.
  await assertCustomerCanViewProduct(account, input);
 
  const response = await fetch(
    'https://demo.orbital.vision/api/configurator/room-ai/server/session',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${process.env.ORBITAL_PROPHECY_SERVER_KEY}`
      },
      body: JSON.stringify({
        productId: input.productId,
        productReference: input.productReference,
        productSource: input.productSource,
        resolution: input.resolution,
        origin: new URL(request.url).origin,
        customerId: account.id
      })
    }
  );
 
  return new Response(response.body, {
    status: response.status,
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'no-store'
    }
  });
}

Your route must:

  • accept only authenticated, same-origin JSON POST requests;
  • derive customerId from the signed-in account, never from request input;
  • validate productId or productReference against the page being viewed;
  • keep the Prophecy server key in a server-only environment variable;
  • return Orbital's status and session JSON without caching it.

OV25 products use productSource: 'ov25-configurator'. Supplied-image products use productSource: 'external-images' and may have a null productId.

Availability and token balance

Before showing anything, Prophecy asks whether the site may offer previews at all. Orbital checks the public key against the organisation, that the requesting origin is on the authorised domains list, and that Prophecy is switched on.

That check opens no session and uses none of the shopper's allowance. It does not depend on the product, so one cached answer covers your whole catalogue and it is asked once per page. A session begins only when a shopper opens the dialog.

If the answer is no, Prophecy keeps the launcher hidden, emits availability-change and error, and logs the reason to the console instead of showing a broken interface. Localhost uses development behaviour by default: the launcher stays visible and a failed session shows its message inside the dialog, so errors remain visible while integrating.

On localhost, Prophecy still calls the live API. Until that origin is on your authorised domains list, the dialog shows the live API's refusal. To use a staging host instead:

product-page.html
<script
  src="https://cdn.jsdelivr.net/npm/@orbital.vision/prophecy@1/dist/prophecy.js"
  data-prophecy-endpoint="https://staging.example"
></script>

From npm, pass endpoint: 'https://staging.example' to Prophecy.create().

custom-button.ts
const availability = await prophecy.checkAvailability();
 
if (availability.available) {
  customButton.hidden = false;
}
 
prophecy.on('availability-change', ({ detail }) => {
  customButton.hidden = !detail.available;
});

How long we keep things

WhatKept for
The room photo a shopper uploadsDeleted the moment the preview is generated
A room photo uploaded but never usedUnder a day
The generated preview30 days from generation

Copy a preview you need to keep

The 30 days is a download window, not storage. It does not extend because you saved your own copy, so a URL of ours linked from your records will stop working after 30 days. Copy the file into your own storage when you save the result.

The room photo is the sensitive half, and it does not survive generation: it is deleted as soon as the generator reads it, before the preview exists.

Account-owned history

For signed-in customers, keep room photos and generated results in your database and private object storage rather than local storage. Every history route should derive ownership from your signed session instead of accepting an account ID from the browser.

DataRecommended fieldsStorage rule
Room photosID, private object key, dimensions, created timeReturn short-lived same-origin or CORS URLs
ResultsOrbital job ID, product identity, selection, result keyVerify the job server-side before saving
OwnershipYour internal customer IDAlways derive it from the signed session
history-adapter.ts
const prophecy = Prophecy.create({
  sessionEndpoint: '/api/orbital/prophecy/session',
  accountKey: signedInAccountVersion,
  history: {
    async load({ signal }) {
      const response = await fetch('/api/orbital/prophecy/history', { signal });
      if (!response.ok) throw new Error('History could not be loaded');
      return response.json(); // { roomPhotos, results }
    },
 
    async saveRoom(photo) {
      const body = new FormData();
      body.append('file', photo.blob, 'room.jpg');
      body.append('width', String(photo.width));
      body.append('height', String(photo.height));
      body.append('aspectRatio', photo.aspectRatio);
 
      const response = await fetch('/api/orbital/prophecy/rooms', {
        method: 'POST',
        body,
        signal: photo.signal
      });
      if (!response.ok) throw new Error('Room photo could not be saved');
      return response.json(); // { id }
    },
 
    async saveResult(result) {
      const response = await fetch('/api/orbital/prophecy/results', {
        method: 'POST',
        keepalive: true,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(result),
        signal: result.signal
      });
      if (!response.ok) throw new Error('Result could not be saved');
    },
 
    async clear({ signal }) {
      const response = await fetch('/api/orbital/prophecy/history', {
        method: 'DELETE',
        signal
      });
      if (!response.ok) throw new Error('History could not be cleared');
    }
  }
});
 
// Fence cached sessions and in-memory history when authentication changes.
await prophecy.setAccountContext(nextAccountVersion);

Adapters should honour the supplied AbortSignal. Returned room-photo URLs must be same-origin or CORS-enabled. accountKey is only a stable, non-sensitive browser-side change marker; never use an email address, customer ID, access token, or session token.

Verify results before persistence

Do not trust result metadata posted by the browser. Retrieve the job from Orbital with the same private key and customer identity, then persist only the verified server fields.

verify-result.ts
const verified = await fetch(
  `https://demo.orbital.vision/api/configurator/room-ai/server/jobs/${jobId}`,
  {
    headers: {
      Authorization: `Bearer ${process.env.ORBITAL_PROPHECY_SERVER_KEY}`,
      'X-Orbital-Customer-Id': account.id
    }
  }
).then((response) => response.json());
 
if (verified.status !== 'COMPLETED' || !verified.resultUrl) {
  throw new Error('Unverified Prophecy result');
}
 
await saveResult({
  orbitalJobId: verified.jobId,
  productId: verified.productId,
  productReference: verified.productReference,
  productSource: verified.productSource,
  selectionString: verified.selectionString,
  sourceUrl: verified.resultUrl
});

Events and hooks

Subscribe with a named option callback, onEvent, prophecy.on(), a wildcard listener, or the bubbling prophecy DOM event emitted from the launcher.

analytics.ts
const prophecy = Prophecy.create({
  sessionEndpoint: '/api/orbital/prophecy/session',
  onResult(result) {
    analytics.track('prophecy_generation_complete', result);
  },
  onError(error) {
    errorReporter.capture(error);
  }
});
 
const unsubscribe = prophecy.on('*', (event) => {
  console.log(event.type, event.detail);
});
EventOption callbackWhen it fires
readyonReadyInitialisation completes
availability-changeonAvailabilityChangePreflight availability changes
sign-in-requiredonSignInRequiredA shopper has used their free allowance and needs to sign in
orderonOrderThe shopper pressed Order this product
open / closeonOpen / onCloseDialog visibility changes
view-changeonViewChangeUpload, library, phone, preview, generating, or result view changes
product-changeonProductChangeProduct identity or source changes
account-changeonAccountChangeAccount context is replaced
selection-changeonSelectionChangeOV25 configuration selections change
photo-selected / photo-clearedonPhotoSelected / onPhotoClearedThe active room photo changes
phone-handoff-startonPhoneHandoffStartPhone capture handoff begins
phone-photo-receivedonPhonePhotoReceivedA phone photo reaches the desktop
generation-startonGenerationStartA generation request starts
generation-progressonGenerationProgressPolling returns generation progress
generation-completeonResultA browser result is ready
generation-erroronGenerationErrorGeneration fails or times out
history-load / history-saveonHistoryLoad / onHistorySaveAccount history reads or writes
history-erroronHistoryErrorA history adapter operation fails
erroronErrorAny integration or runtime error
destroyedonDestroyThe instance is permanently removed

Order this product

Every preview remembers two things: the configurator's SKU for that exact configuration, and the page it was generated from, query string and all. When a shopper presses Order this product, Prophecy tells your page first.

Call preventDefault() to keep the shopper on the page and take the order yourself. If you do not, they go to the page the preview was made from.

order.ts
const prophecy = Prophecy.create({
  onOrder(order) {
    order.preventDefault();
    addToBasket(order.sku);
    // order.pageUrl is the page it was made from, query string and all
  }
});

With the script tag, set the hook before the script loads:

product-page.html
<script>
  window.prophecyOptions = {
    onOrder(order) {
      order.preventDefault();
      addToBasket(order.sku);
    }
  };
</script>
<script src="https://cdn.jsdelivr.net/npm/@orbital.vision/prophecy@1/dist/prophecy.js"></script>

What order carries:

FieldMeaning
skuThe configurator's SKU for the configuration in the preview
pageUrlThe page the preview was made from, query string and all
productUrlWhere Prophecy sends the shopper unless you call preventDefault()
productReference / selectionStringThe product and its configuration
resultUrl / jobIdThe preview image and the job that made it

sku and pageUrl are also on every generation-complete result and every saved preview.

Instance API

MethodPurpose
checkAvailability()Preflight setup, origin access, entitlement, and token balance
open() / close() / isOpen()Control and inspect dialog visibility
mount(target)Mount Prophecy's default launcher
mountOverlay(target?)Put the widget on the configurator, or another element; the script tag does this for you
useLauncher(target) / releaseLauncher()Move the launcher to another element, or detach it
setProductId(id)Synchronise an OV25 product change in an SPA
setProduct(product)Atomically switch an external product, its images, and URL
setAccountContext(key)Invalidate sessions and memory on login, logout, or account switching
getState()Read the current public state without subscribing
refreshHistory() / clearHistory()Reload or remove customer-owned history
on(name, handler) / off(name, handler)Add or remove event handlers
destroy()Abort work and remove UI, listeners, and temporary object URLs

Core options

OptionTypePurpose
sessionEndpointstringSame-origin retailer route returning a short-lived session
sessionProviderfunctionProgrammatic alternative to sessionEndpoint
endpointstringOrbital API host for a staging environment; normally omit
configuratorselector | iframeDisambiguate multiple configurators; normally omit
buttonselector | buttonAttach Prophecy to an existing site-owned control
productReferencestringStable SKU, variant ID, or product handle for external images
productImages(string | Blob)[]One to four CORS-readable URLs or image blobs
productUrlstringResult call-to-action and history URL
resolution'1K' | '2K' | '4K'Requested output resolution, subject to entitlement
historyHistoryAdapterAccount-backed load, save, and clear operations
accountKeystring | nullNon-sensitive browser marker for account changes
localHistorybooleanOpt device history in or out
developmentbooleanOverride automatic localhost development behaviour
timeoutMsnumberGeneration polling timeout; default four minutes
buttonLabelstringWording on Prophecy's own launcher button

Declarative attributes

AttributePurpose
data-prophecy-session-endpointSame-origin browser-session endpoint
data-prophecy-configuratorSelector for a specific OV25 iframe
data-prophecy-buttonSelector for a site-owned launcher
data-prophecy-endpointOrbital API host for a staging environment; normally omit
data-prophecy-product-idExplicit OV25 product ID for migration cases
data-prophecy-product-referenceStable external product reference
data-prophecy-product-imagesJSON array containing one to four product-image URLs
data-prophecy-product-urlProduct page URL used by results and history
data-prophecy-resolutionRequested 1K, 2K, or 4K resolution
data-prophecy-local-historyExplicitly enable or disable device history
data-prophecy-autoEnable or disable automatic initialisation

Go-live checklist

  • Test against the version you ship. The @1 URL follows fixes automatically; pin an exact version only if you want to control updates yourself.
  • Keep the Prophecy server key on your server only.
  • Ask Orbital Vision to authorise every production origin.
  • Confirm Prophecy is switched on and the organisation has enough tokens.
  • Test login, logout, account switching, and anonymous-to-account transitions.
  • Keep room photos and results in account-owned private storage.
  • Verify completed jobs from your server before saving result metadata.
  • Exercise the unavailable state so your launcher never exposes a broken feature.

For access, private API keys, authorised origins, or billing setup, contact Orbital Vision.