Example: Daily Spin to Win

Introduction

This guide demonstrates how to run a daily "Spin to Win" promotion where customers spin a wheel and earn a variable number of points based on the result. Custom actions allow you to reward customers with points for specific interactions on your website. Since a custom action awards a fixed number of points, the trick is to create one custom action per prize amount and trigger the one that matches the wheel result.

In this example the wheel has three prizes — 5, 10, and 15 points — so we'll create three custom actions. We'll cover two flows:

  • Logged-in customers — trigger the matching custom action directly.
  • New visitors (the complete Recart example) — the wheel doubles as an email/SMS opt-in popup, and the prize is awarded through Rivo's signup opt-in endpoints once the visitor shares their email or phone number.

Step 1: Create a Custom Action per Prize

In Rivo, create one custom action earning rule for each possible prize on your wheel:

Custom Action NamePoints Awarded
Spin to Win - 55
Spin to Win - 1010
Spin to Win - 1515
📘

Custom action names must match exactly

The custom_action_name you send from JavaScript must match the name configured in Rivo character-for-character, so copy it exactly as it appears in the Rivo Custom Action you've configured.

To make the promotion daily, set each custom action's frequency limit to once per day. If a customer spins again on the same day, the API responds with an error status instead of awarding points a second time.

If your wheel also captures new email or SMS subscribers (as in the Recart example below), enable the Points for email signup and Points for SMS signup earning rules as well — the opt-in endpoints award through those rules.

Step 2: Build the Wheel

The spin-to-win wheel itself is your own code — a custom-built widget or a third-party popup tool. The only requirement is that when a spin completes, your JavaScript knows the number of points the customer won so it can map that result to the matching custom action name.

Step 3: Trigger the Matching Custom Action

When the spin completes for a logged-in customer, send a POST request to the Rivo proxy URL to record the custom action:

<script type="text/javascript">
  const SPIN_TO_WIN_ACTIONS = {
    5: "Spin to Win - 5",
    10: "Spin to Win - 10",
    15: "Spin to Win - 15"
  };

  function recordSpinToWin(pointsWon) {
    const customActionName = SPIN_TO_WIN_ACTIONS[pointsWon];
    const customerId = window.Rivo?.common?.customer?.id;

    if (!customActionName) {
      console.error("No custom action configured for prize:", pointsWon);
      return;
    }

    if (!customerId) {
      // The customer must be logged in to their storefront account
      // for Rivo to know who to award the points to.
      console.log("Customer is not logged in, skipping Rivo custom action");
      return;
    }

    const proxyUrl = `${window.Rivo.global_config.proxy_paths.loy}/loy/customers/${customerId}/record_custom_action`;
    const data = { custom_action_name: customActionName };

    fetch(proxyUrl, {
      method: "POST",
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    })
    .then(function(response) {
      if (response.ok) {
        console.log("Spin to Win points recorded:", customActionName);
      } else {
        // A non-OK response is returned when the daily frequency
        // limit has already been reached for this customer.
        console.error("Failed to record Spin to Win custom action");
      }
    })
    .catch(function(error) {
      console.error("Error recording Spin to Win custom action:", error);
    });
  }

  // Call recordSpinToWin with the wheel result, e.g.:
  // recordSpinToWin(10);
</script>

Call recordSpinToWin(pointsWon) from your wheel's completion handler, passing the number of points the customer landed on. The script looks up the matching custom action name and records it against the logged-in customer.


Complete Example: Recart Spin-the-Wheel Popup

Recart's spin-the-wheel opt-in tool is a popular way to run this promotion, because the wheel doubles as an email/SMS capture popup. That introduces a wrinkle: most visitors spinning the wheel are not logged in, so there's no customer to record a custom action against at the moment the wheel stops.

The complete integration handles both cases:

  • Logged-in customer spins — record the matching custom action immediately (Step 3 above).
  • Anonymous visitor spins — remember the prize, then submit it to Rivo's opt-in endpoints once Recart captures their email or phone number. Rivo creates (or finds) the customer from the email and awards the points through the signup earning rules.

How the events fit together

Recart dispatches browser events at each stage of the popup flow:

  1. recart:optin-tool:spin-the-wheel-completed — the wheel stopped; event.detail.result is the prize. If the customer is logged in, record the custom action now. Otherwise, store the prize in localStorage until we know who the visitor is.
  2. recart:optin-tool:email-captured — the visitor entered their email. Submit the email opt-in with the prize amount, and remember the email in case an SMS opt-in follows.
  3. recart:optin-tool:phone-number-captured — the visitor entered their phone number. Submit the SMS opt-in, identifying the customer by the stored email (or the logged-in customer ID).

As a fallback, if you name your Recart opt-in tools with the prize amount as a suffix — for example "Desktop 10" or "Mobile 20" — the script reads the prize from optinToolName on the capture events, which is useful when each wheel variant awards a fixed prize.

The complete script

Add this to your theme (for example in theme.liquid), and update SPIN_TO_WIN_ACTIONS to match the custom actions you created in Step 1:

<script type="text/javascript">
(function () {
  const ENTRIES_STORAGE_KEY = "rivo_spin_to_win_entries";
  const EMAIL_STORAGE_KEY = "rivo_spin_to_win_email";
  const MAX_ENTRIES = 30;

  const SPIN_TO_WIN_ACTIONS = {
    5: "Spin to Win - 5",
    10: "Spin to Win - 10",
    15: "Spin to Win - 15"
  };

  const submittedOptIns = new Set();

  function toValidEntries(value, source) {
    const entries = Number(value);

    if (!Number.isInteger(entries) || entries <= 0) {
      return null;
    }

    if (entries > MAX_ENTRIES) {
      console.error(`${source} entries cannot exceed ${MAX_ENTRIES}:`, entries);
      return null;
    }

    return entries;
  }

  // Recart opt-in tools named like "Desktop 5" or "Mobile 10"
  // carry the prize amount in their name.
  function getEntriesFromOptinToolName(name) {
    const match = name?.match(/(?:Desktop|Mobile)\s+(\d+)\s*$/i);

    return match ? toValidEntries(match[1], "Recart opt-in tool") : null;
  }

  function getEntries(optinToolName) {
    return (
      getEntriesFromOptinToolName(optinToolName) ||
      toValidEntries(localStorage.getItem(ENTRIES_STORAGE_KEY), "Stored spin result")
    );
  }

  async function postToRivo(endpoint, body) {
    if (!window.RivoAPI?.url) {
      throw new Error("RivoAPI is not available");
    }

    const response = await fetch(window.RivoAPI.url("POST", endpoint), {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      throw new Error(`Rivo API returned ${response.status}`);
    }

    return response.json();
  }

  // Logged-in customers: record the custom action matching the prize.
  async function recordCustomAction(entries) {
    const customActionName = SPIN_TO_WIN_ACTIONS[entries];
    const customerId = window.Rivo?.common?.customer?.id;

    if (!customActionName) {
      console.error("No custom action configured for prize:", entries);
      return;
    }

    const proxyUrl = `${window.Rivo.global_config.proxy_paths.loy}/loy/customers/${customerId}/record_custom_action`;

    try {
      const response = await fetch(proxyUrl, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ custom_action_name: customActionName })
      });

      if (response.ok) {
        console.log("Rivo custom action recorded:", customActionName);
      } else {
        // A non-OK response is returned when the daily frequency
        // limit has already been reached for this customer.
        console.error("Failed to record Rivo custom action:", customActionName);
      }
    } catch (error) {
      console.error("Error recording Rivo custom action:", error);
    }
  }

  // New visitors: award the prize through the email signup opt-in.
  async function submitEmailOptIn(payload) {
    const email = payload?.email?.trim().toLowerCase();

    if (!email) {
      return;
    }

    // Remember the email so a later SMS opt-in can identify the customer.
    localStorage.setItem(EMAIL_STORAGE_KEY, email);

    const entries = getEntries(payload.optinToolName);
    const submissionKey = `email:${email}`;

    if (!entries || submittedOptIns.has(submissionKey)) {
      return;
    }

    submittedOptIns.add(submissionKey);

    try {
      await postToRivo("email_subscribe_opt_ins", {
        email,
        email_entries: entries
      });

      console.log("Rivo email opt-in submitted:", { email, entries });
    } catch (error) {
      submittedOptIns.delete(submissionKey);
      console.error("Unable to submit Rivo email opt-in:", error);
    }
  }

  // New visitors: award the prize through the SMS signup opt-in.
  async function submitSmsOptIn(payload) {
    const phoneNumber = payload?.phoneNumber?.trim();
    const email = localStorage.getItem(EMAIL_STORAGE_KEY)?.trim().toLowerCase();
    const loggedInCustomerId = window.Rivo?.common?.customer?.id;
    const entries = getEntries(payload?.optinToolName);

    const customerIdentifier = email
      ? { email }
      : { logged_in_customer_id: loggedInCustomerId };

    const submissionKey = email
      ? `sms:email:${email}:${phoneNumber}`
      : `sms:customer:${loggedInCustomerId}:${phoneNumber}`;

    if (
      !phoneNumber ||
      (!email && !loggedInCustomerId) ||
      !entries ||
      submittedOptIns.has(submissionKey)
    ) {
      return;
    }

    submittedOptIns.add(submissionKey);

    try {
      await postToRivo("sms_subscribe_opt_ins", {
        ...customerIdentifier,
        sms_entries: entries
      });

      console.log("Rivo SMS opt-in submitted:", { email, phoneNumber, entries });
    } catch (error) {
      submittedOptIns.delete(submissionKey);
      console.error("Unable to submit Rivo SMS opt-in:", error);
    }
  }

  window.addEventListener(
    "recart:optin-tool:spin-the-wheel-completed",
    function (event) {
      const entries = toValidEntries(event.detail?.result, "Recart spin result");

      if (!entries) {
        // Preserve the previous valid stored prize if the new result is invalid.
        return;
      }

      if (window.Rivo?.common?.customer?.id) {
        // Logged-in customers are awarded immediately via the custom action.
        recordCustomAction(entries);
        return;
      }

      // Anonymous visitors: store the prize until an email or
      // phone number is captured.
      localStorage.setItem(ENTRIES_STORAGE_KEY, entries.toString());
    }
  );

  window.addEventListener(
    "recart:optin-tool:email-captured",
    function (event) {
      submitEmailOptIn(event.detail);
    }
  );

  window.addEventListener(
    "recart:optin-tool:phone-number-captured",
    function (event) {
      submitSmsOptIn(event.detail);
    }
  );
})();
</script>
📘

Prerequisites

  • The Rivo JavaScript API must be enabled so window.RivoAPI is available on your storefront.
  • The custom actions from Step 1 must be active for the logged-in flow.
  • The Points for email signup and Points for SMS signup earning rules must be enabled for the opt-in flow.

Opt-in endpoint reference

Both endpoints create or find the Rivo customer, award the prize through the corresponding signup earning rule, and respond with { "success": true }.

POST email_subscribe_opt_ins

ParameterTypeDescription
emailstringThe subscriber's email address. Required.
email_entriesintegerThe number of points to award. Optional — defaults to the email signup earning rule's points.
logged_in_customer_idintegerThe Shopify customer ID, if known. Optional.

POST sms_subscribe_opt_ins

ParameterTypeDescription
emailstringAn email address identifying the customer. Required unless logged_in_customer_id is sent.
sms_entriesintegerThe number of points to award. Optional — defaults to the SMS signup earning rule's points.
logged_in_customer_idintegerThe Shopify customer ID, if known. Optional.
📘

Opt-in awards are one-time per customer

The email and SMS opt-in endpoints award points once per customer — repeat submissions for the same customer are ignored. That makes them ideal for the first-time subscriber prize, while the custom actions from Step 1 (with a daily frequency limit) handle repeat spins from logged-in customers.


Did this page help you?