Skip to main content

Integration Guide

This guide walks through building a custom storefront that uses the GraphQL API for public data and Verse Elements for authenticated actions.

For canonical naming between product concepts and schema types, see Release & Sales Mapping and Glossary and Name Mapping.

Architecture

  • GraphQL API — Fetch collections, artworks, editions, prices, and marketplace data. No auth required.
  • Verse Elements — Handle sign-in, purchase dialogs, reserve checks, and bookmarks. Requires user session.

1. Setup

Add the Elements script from the verse-embedded package to your page:

<script src="https://unpkg.com/verse-embedded@1.1.5/dist/verse-elements.js"></script>

Initialize the SDK:

const elements = new VerseElements({
baseUrl: "https://iframe.verse.works",
});

Finding your artworkId

The artworkId is a UUID that identifies your artwork on Verse. To find it, go to Verse Studio (or sandbox Studio for testing), open your artwork, and click View. This takes you to the artwork page — the URL will be in the form https://verse.works/artworks/{artworkId}. The segment after /artworks/ is your artworkId.

2. Fetch Collection Data (GraphQL API)

Use the public API to get artwork and pricing data:

async function getCollectionData(slug) {
const response = await fetch("https://verse.works/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query ($slug: String!) {
collectionsPage(request: { filter: { slugs: [$slug] }, first: 1 }) {
nodes {
name
artworks {
id
title
primaryMarketListing {
startsAt
endsAt
strategy {
... on PMBuyNowLimitedEditionStrategy {
price { value currency }
maxEditions
stats { issuedEditions }
}
... on PMBuyNowOpenEditionStrategy {
price { value currency }
}
}
}
}
}
}
}
`,
variables: { slug },
}),
});

const { data } = await response.json();
return data.collectionsPage.nodes[0];
}

3. Auth Flow (Verse Elements)

Check if the user is signed in, and prompt sign-in if needed:

async function ensureAuth() {
const isSignedIn = await elements.checkAuth();

if (!isSignedIn) {
const result = await elements.authorise();
if (!result) {
// User closed the dialog
return false;
}
console.log("Signed in as", result.verseUsername);
}

return true;
}

4. Purchase Flow (Verse Elements)

Combine auth check with purchase:

async function handlePurchase(artwork) {
// Ensure user is signed in
const authed = await ensureAuth();
if (!authed) return;

// Optionally check reserves
try {
const result = await elements.checkReserves(artwork.id);
const reserves = result.reserveAccess?.reserves ?? 0;

if (!result.hasAccess) {
alert("Reserve required — you are not eligible for this sale.");
return;
}
// reserves > 0 means user holds reserves; 0 means open to all
} catch (e) {
// No active listing or query failed
console.error(e);
return;
}

// Open purchase dialog
const price = artwork.primaryMarketListing.strategy.price;

elements.openPurchaseDialog(
artwork.id,
{
amount: { value: price.value, currency: price.currency },
},
{
onSuccess({ editionId, editionNumber }) {
showSuccessMessage(`You purchased edition #${editionNumber}!`);
},
onTerminalFailure({ title, message }) {
showErrorMessage(`${title}: ${message}`);
},
onClose() {
// User closed the dialog — no action needed
},
}
);
}

5. Putting It Together

<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/verse-embedded@1.1.5/dist/verse-elements.js"></script>
</head>
<body>
<div id="collection"></div>

<script>
const elements = new VerseElements({
baseUrl: "https://iframe.verse.works",
});

async function init() {
const collection = await getCollectionData("your-collection-slug");
const container = document.getElementById("collection");

for (const artwork of collection.artworks) {
if (!artwork.primaryMarketListing) continue;

const price = artwork.primaryMarketListing.strategy.price;
const card = document.createElement("div");
card.innerHTML = `
<h2>${artwork.title}</h2>
<p>${price.value} ${price.currency}</p>
<button onclick="handlePurchase(${JSON.stringify(artwork).replace(/"/g, '&quot;')})">
Buy Now
</button>
`;
container.appendChild(card);
}
}

init();
</script>
</body>
</html>

Multi-Phase Sales

If your release has multiple phases (e.g. a reserve-gated early phase followed by a public phase), each phase is a separate sale configured on the same artwork in Verse Studio. Verse Elements automatically detects the currently active sale, so your integration code does not need to change between phases.

To always show the correct price, query primaryMarketListing for the artwork — this returns the currently active sale's strategy and price:

async function getCurrentPrice(artworkId) {
const response = await fetch("https://verse.works/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query ($artworkId: ID!) {
artworksPage(request: { filter: { ids: [$artworkId] }, first: 1 }) {
nodes {
primaryMarketListing {
startsAt
endsAt
strategy {
... on PMBuyNowLimitedEditionStrategy {
price { value currency }
}
... on PMBuyNowOpenEditionStrategy {
price { value currency }
}
}
}
}
}
}
`,
variables: { artworkId },
}),
});

const { data } = await response.json();
return data.artworksPage.nodes[0]?.primaryMarketListing;
}

See Sales Mechanics — Multi-Phase Sales for details on configuring phases.

Artist-Curated Flow

For artist-curated projects with user selection enabled (allowUserSelection: true), collectors choose a specific item from the curated set before purchasing. Pass the selected item's id via userInput with the key $curated_project:item_id:

async function loadProjectItems(artworkId) {
const response = await fetch("https://verse.works/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query ($artworkId: ID!) {
artworksPage(request: { filter: { ids: [$artworkId] }, first: 1 }) {
nodes {
artworkKind {
... on ProjectArtworkKind {
allowUserSelection
items {
id
isAvailable
featuresJson
staticAsset {
... on ImageAsset { baseUrlTemplate }
... on VideoAsset { baseUrlTemplate }
}
}
}
}
primaryMarketListing {
strategy {
... on PMBuyNowLimitedEditionStrategy {
price { value currency }
}
... on PMBuyNowOpenEditionStrategy {
price { value currency }
}
}
}
}
}
}
`,
variables: { artworkId },
}),
});

const { data } = await response.json();
return data.artworksPage.nodes[0];
}

async function handleCuratedItemPurchase(artworkId, selectedItemId, price) {
const authed = await ensureAuth();
if (!authed) return;

elements.openPurchaseDialog(
artworkId,
{
amount: { value: price.value, currency: price.currency },
userInput: [
{ key: "$curated_project:item_id", value: selectedItemId },
],
},
{
onSuccess({ editionId, editionNumber }) {
console.log("Purchased edition:", editionNumber);
},
onTerminalFailure({ title, message }) {
console.error(`Failed: ${title}${message}`);
},
onClose() {},
}
);
}

// Usage
const artwork = await loadProjectItems("artwork-id-here");
const { artworkKind, primaryMarketListing } = artwork;
const items = artworkKind.items.filter((item) => item.isAvailable);
const price = primaryMarketListing.strategy.price;

// Render items for the collector to choose from, then on selection:
handleCuratedItemPurchase("artwork-id-here", items[0].id, price);
tip

When allowUserSelection is false, you don't need to pass userInput — the platform assigns a random item automatically. Use the standard purchase flow instead.

Collector-Curated Flow

For collector-curated projects, use createBookmark() to let collectors save their preferred output before purchasing:

async function handleCuratedPurchase(artworkId) {
const authed = await ensureAuth();
if (!authed) return;

// User has already chosen their favorite — create bookmark
const bookmark = await elements.createBookmark(artworkId);

elements.openPurchaseDialog(
artworkId,
{
userInput: [{ key: "$user_hash", value: bookmark.signedToken }],
},
{
onSuccess({ editionId, editionNumber }) {
console.log("Purchased curated edition:", editionNumber);
},
onTerminalFailure({ title, message }) {
console.error(`Failed: ${title}${message}`);
},
onClose() {},
}
);
}

Debugging

When developing and debugging, point both the Elements SDK and API calls to the sandbox environment:

// Verse Elements — use sandbox iframe origin
const elements = new VerseElements({
baseUrl: "https://iframe.demo.verse.works",
});

// GraphQL API — use sandbox endpoint
const response = await fetch("https://demo.verse.works/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "..." }),
});

Production Checklist

  • Domain added to Verse origin allowlist (required for production iframe.verse.works only — localhost:3000 is allowlisted by default; the sandbox requires no allowlisting)
  • HTTPS configured with HTTP-to-HTTPS redirects
  • Error handling for all Elements method calls
  • Tested with the sandbox environment before going live