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 = `
`;
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);
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.worksonly —localhost:3000is 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