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.4/dist/verse-elements.js"></script>

Initialize the SDK:

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

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.4/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>

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 { baseUrl }
... on VideoAsset { baseUrl }
}
}
}
}
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