Skip to content

SDK Code Examples

Practical, copy-pasteable examples for every window.SaveLayer method. The SDK is injected by the theme app embed and talks to SaveLayer through Shopify's signed app proxy.

All methods return a JSON envelope: { ok: true, data: ... } on success or { ok: false, error: { code, message, retryable } } on failure.

Quick start

A minimal save button in five lines:

js
document.querySelector('.save-btn')?.addEventListener('click', async () => {
  const result = await window.SaveLayer.save({
    context: 'wishlist',
    entityType: 'product',
    entityGid: 'gid://shopify/Product/123456789',
    source: 'theme',
  });
  console.log(result); // { ok: true, data: { id, state: 'active' } }
});

SaveLayer.save()

Save an item to a list. Creates the list if it does not exist.

Parameters

FieldTypeRequiredDescription
contextstringYesList identifier, e.g. 'wishlist' or 'favorites'
entityTypestringYesOne of: product, variant, collection, metaobject, article, page, configuration
entityGidstringYesShopify global ID, e.g. 'gid://shopify/Product/123'
sourcestringYesWhere the save originated, e.g. 'theme', 'pdp', 'collection-page'

Example: save button on a product page

js
const saveBtn = document.getElementById('savelayer-save');

saveBtn?.addEventListener('click', async () => {
  const result = await window.SaveLayer.save({
    context: 'wishlist',
    entityType: 'product',
    entityGid: saveBtn.dataset.entityGid,
    source: 'pdp',
  });

  if (result.ok) {
    saveBtn.textContent = 'Saved!';
  } else {
    console.error(result.error.code, result.error.message);
  }
});

Success response

json
{
  "ok": true,
  "data": { "id": "abc123", "state": "active" }
}

Duplicate save (409)

json
{
  "ok": false,
  "error": {
    "code": "DUPLICATE_SAVE",
    "message": "The item is already saved.",
    "retryable": false
  }
}

SaveLayer.remove()

Remove a saved item from a list (soft-delete).

Parameters

FieldTypeRequiredDescription
contextstringYesList identifier
entityTypestringYesEntity type
entityGidstringYesShopify global ID

Example: remove button in a wishlist

js
async function removeItem(entityGid) {
  const result = await window.SaveLayer.remove({
    context: 'wishlist',
    entityType: 'product',
    entityGid,
  });

  if (result.ok) {
    // Remove the item's DOM node
    document.querySelector(`[data-gid="${entityGid}"]`)?.remove();
  } else if (result.error.code === 'ITEM_NOT_FOUND') {
    // Already removed — clean up the UI anyway
    document.querySelector(`[data-gid="${entityGid}"]`)?.remove();
  }
}

Success response

json
{
  "ok": true,
  "data": { "id": "abc123", "state": "removed" }
}

SaveLayer.toggle()

Toggle an item: saves it if not saved, removes it if already saved. Ideal for heart icons.

Parameters: Same as save() (includes source).

Example: heart icon toggle

js
const heart = document.querySelector('.sl-heart');

heart?.addEventListener('click', async () => {
  const result = await window.SaveLayer.toggle({
    context: 'wishlist',
    entityType: 'product',
    entityGid: heart.dataset.entityGid,
    source: 'collection-page',
  });

  if (result.ok) {
    const isSaved = result.data.state === 'active';
    heart.classList.toggle('sl-heart--active', isSaved);
    heart.setAttribute('aria-pressed', String(isSaved));
  }
});

Success response (toggled on)

json
{
  "ok": true,
  "data": { "id": "abc123", "state": "active" }
}

Success response (toggled off)

json
{
  "ok": true,
  "data": { "id": "abc123", "state": "removed" }
}

SaveLayer.list()

Fetch saved items for a given list context. Supports cursor-based pagination.

Parameters

FieldTypeRequiredDefaultDescription
contextstringYesList identifier
limitnumberNo20Items per page (1–100)
cursorstringNoPagination cursor from a previous response

Example: render a wishlist page

js
async function renderWishlist() {
  const container = document.getElementById('wishlist-items');
  const result = await window.SaveLayer.list({ context: 'wishlist', limit: 10 });

  if (!result.ok) {
    container.innerHTML = '<p>Could not load your wishlist.</p>';
    return;
  }

  const { items, pageInfo } = result.data;

  if (items.length === 0) {
    container.innerHTML = '<p>Your wishlist is empty.</p>';
    return;
  }

  container.innerHTML = items
    .map(
      (item) =>
        `<div data-gid="${item.entityGid}">
          <span>${item.entityType}: ${item.entityGid}</span>
          <button onclick="removeItem('${item.entityGid}')">Remove</button>
        </div>`
    )
    .join('');

  // Show "Load more" if there are more pages
  if (pageInfo.hasNextPage) {
    const btn = document.createElement('button');
    btn.textContent = 'Load more';
    btn.addEventListener('click', () => loadMore(pageInfo.endCursor));
    container.appendChild(btn);
  }
}

async function loadMore(cursor) {
  const result = await window.SaveLayer.list({
    context: 'wishlist',
    limit: 10,
    cursor,
  });
  // Append items to the existing list...
}

Success response

json
{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "abc123",
        "entityType": "product",
        "entityGid": "gid://shopify/Product/123",
        "status": "active",
        "savedAt": "2026-03-15T10:30:00Z"
      }
    ],
    "pageInfo": {
      "hasNextPage": true,
      "endCursor": "eyJpZCI6ImFiYzEyMyJ9"
    }
  }
}

SaveLayer.isSaved()

Check whether an item is currently saved. Useful for setting initial UI state on page load.

Parameters

FieldTypeRequiredDescription
contextstringYesList identifier
entityTypestringYesEntity type
entityGidstringYesShopify global ID

Example: set initial heart state on page load

js
document.addEventListener('DOMContentLoaded', async () => {
  const hearts = document.querySelectorAll('[data-sl-check]');

  for (const heart of hearts) {
    const result = await window.SaveLayer.isSaved({
      context: 'wishlist',
      entityType: heart.dataset.entityType,
      entityGid: heart.dataset.entityGid,
    });

    if (result.ok && result.data.saved) {
      heart.classList.add('sl-heart--active');
      heart.setAttribute('aria-pressed', 'true');
    }
  }
});

Batch checking

For collection pages with many products, consider batching isSaved calls or using list() to fetch all saved items at once and comparing client-side.

Success response

json
{
  "ok": true,
  "data": { "saved": true }
}

SaveLayer.on()

Subscribe to SDK events. The SDK emits customer events after successful operations.

Parameters

FieldTypeDescription
eventstringEvent name (see table below)
listenerfunctionCallback receiving the event payload

Returns: An unsubscribe function.

Available events

EventFires afterPayload
savelayer:item_savedSuccessful save()entityGid, entityType, context, listHandle
savelayer:item_removedSuccessful remove()entityGid, entityType, context, listHandle
savelayer:item_toggledSuccessful toggle()Same fields + saved (boolean)
savelayer:list_viewedSuccessful list()context, itemCount

Example: update a save counter badge

js
let saveCount = 0;
const badge = document.getElementById('wishlist-count');

const unsub = window.SaveLayer.on('savelayer:item_saved', (payload) => {
  saveCount++;
  badge.textContent = saveCount;
});

window.SaveLayer.on('savelayer:item_removed', (payload) => {
  saveCount = Math.max(0, saveCount - 1);
  badge.textContent = saveCount;
});

// Call unsub() to stop listening

Example: log all toggle events

js
window.SaveLayer.on('savelayer:item_toggled', (payload) => {
  console.log(
    payload.saved ? 'Saved' : 'Removed',
    payload.entityType,
    payload.entityGid
  );
});

Error handling

All SDK methods return a JSON envelope. Errors include a machine-readable code, a human-readable message, and a retryable boolean.

Common error codes

CodeHTTPMeaningRetryable
AUTH_REQUIRED401Customer is not logged inNo
DUPLICATE_SAVE409Item is already savedNo
ITEM_NOT_FOUND404Item or list does not existNo
VALIDATION_ERROR400Invalid request payloadNo
RATE_LIMITED429Too many requestsYes
PLAN_LIMIT_REACHED402Merchant's plan limit reachedNo
SHOPIFY_UPSTREAM_ERROR502Shopify returned an errorYes
INTERNAL_ERROR500Unexpected server errorYes

Example: robust save with error handling

js
async function safeSave(entityGid) {
  if (!window.SaveLayer) {
    console.warn('SaveLayer SDK not loaded');
    return;
  }

  const result = await window.SaveLayer.save({
    context: 'wishlist',
    entityType: 'product',
    entityGid,
    source: 'theme',
  });

  if (result.ok) {
    return { saved: true };
  }

  switch (result.error.code) {
    case 'AUTH_REQUIRED':
      // Redirect to login
      window.location.href = '/account/login';
      break;
    case 'DUPLICATE_SAVE':
      // Already saved — treat as success
      return { saved: true };
    case 'RATE_LIMITED':
      // Wait and retry
      await new Promise((r) => setTimeout(r, 2000));
      return safeSave(entityGid);
    case 'PLAN_LIMIT_REACHED':
      alert('The store has reached its save limit. Please try again later.');
      break;
    default:
      console.error('SaveLayer error:', result.error.code, result.error.message);
  }

  return { saved: false };
}

Example: network error handling

js
async function resilientToggle(entityGid) {
  try {
    const result = await window.SaveLayer.toggle({
      context: 'wishlist',
      entityType: 'product',
      entityGid,
      source: 'theme',
    });
    return result;
  } catch (err) {
    // Network failure (fetch rejected) — not an API error envelope
    console.error('Network error:', err);
    return { ok: false, error: { code: 'NETWORK_ERROR', message: err.message } };
  }
}