For the complete documentation index, see llms.txt. This page is also available as Markdown.

Shopify Klaviyo sync

Save customer marketing preferences to Shopify and optionally map them to Klaviyo profile properties.

This recipe saves a current preference object to custom.marketing_preferences. When Klaviyo sync is enabled on the Raven, FieldsRaven can map the object's keys to custom Klaviyo profile properties after the Shopify write is accepted.

Create a customer-owned Raven with namespace custom, key marketing_preferences, and type json. Copy its Liquid configuration from Get Code so the page defines window.FR_CUSTOM__CUSTOMER_MARKETING_PREFERENCES. Configure the Klaviyo mapping in FieldsRaven only after the Shopify shape is working.

See the Shopify-to-Klaviyo implementation story and Quick Start for the request contract.

Complete direct recipe

The example preference names are illustrative. Match them to the consent and preference model approved for your store.

{% if customer %}
  {% assign saved_marketing_preferences = customer.metafields.custom.marketing_preferences.value %}
  <script type="application/json" id="fr-preferences-initial">
    {% if saved_marketing_preferences != blank %}{{ saved_marketing_preferences | json }}{% else %}{}{% endif %}
  </script>

  <form id="fr-preferences-form">
    <fieldset>
      <legend>Marketing preferences</legend>

      <label for="fr-preferences-frequency">Email frequency</label>
      <select id="fr-preferences-frequency" name="email_frequency" required>
        <option value="weekly">Weekly</option>
        <option value="monthly">Monthly</option>
        <option value="product-launches">Product launches only</option>
      </select>

      <fieldset>
        <legend>Topics</legend>
        <label for="fr-preferences-topic-guides">
          <input id="fr-preferences-topic-guides" type="checkbox" name="topic_guides" value="true">
          Guides
        </label>
        <label for="fr-preferences-topic-new-products">
          <input id="fr-preferences-topic-new-products" type="checkbox" name="topic_new_products" value="true">
          New products
        </label>
      </fieldset>
    </fieldset>

    <button id="fr-preferences-submit" type="submit">Save preferences</button>
    <p id="fr-preferences-status" aria-live="polite"></p>
  </form>

  <section aria-labelledby="fr-preferences-current-heading">
    <h2 id="fr-preferences-current-heading">Current preferences</h2>
    <p id="fr-preferences-current"></p>
  </section>

  <script>
    const preferencesForm = document.getElementById("fr-preferences-form")
    const preferencesButton = document.getElementById("fr-preferences-submit")
    const preferencesStatus = document.getElementById("fr-preferences-status")
    const preferencesOutput = document.getElementById("fr-preferences-current")
    let savedPreferences = JSON.parse(document.getElementById("fr-preferences-initial").textContent)

    function renderPreferences() {
      const topics = []
      if (savedPreferences.topic_guides) topics.push("guides")
      if (savedPreferences.topic_new_products) topics.push("new products")
      preferencesOutput.textContent = savedPreferences.email_frequency
        ? `Email frequency: ${savedPreferences.email_frequency}; topics: ${topics.join(", ") || "none"}.`
        : "No marketing preferences have been saved yet."
    }

    async function readPreferencesResponse(response) {
      const contentType = response.headers.get("content-type") || ""
      if (!contentType.includes("application/json")) {
        throw new Error("FieldsRaven returned a non-JSON response. Please try again.")
      }

      try {
        return await response.json()
      } catch (error) {
        throw new Error("FieldsRaven returned invalid JSON. Please try again.")
      }
    }

    async function saveMarketingPreferences(preferencesPayload) {
      const config = window.FR_CUSTOM__CUSTOMER_MARKETING_PREFERENCES

      try {
        const response = await fetch("/apps/raven/create_metafield", {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ raven: {
            raven_id: config.ravenId,
            resource_id: config.resourceId,
            raven_mac: config.ravenMac,
            value: JSON.stringify(preferencesPayload)
          } })
        })

        if (response.status === 429) throw new Error("Too many requests. Wait a moment and try again.")
        const result = await readPreferencesResponse(response)
        if (!response.ok) {
          const message = typeof result?.message === "string" ? result.message : "FieldsRaven rejected the preferences."
          throw new Error(message)
        }
        return result
      } catch (error) {
        if (error instanceof TypeError) throw new Error("Network error. Check your connection and try again.")
        throw error
      }
    }

    preferencesForm.addEventListener("submit", async (event) => {
      event.preventDefault()
      preferencesButton.disabled = true
      preferencesButton.setAttribute("aria-busy", "true")
      preferencesStatus.textContent = "Saving…"

      const formData = new FormData(preferencesForm)
      const preferencesPayload = {
        email_frequency: formData.get("email_frequency"),
        topic_guides: formData.get("topic_guides") === "true",
        topic_new_products: formData.get("topic_new_products") === "true"
      }

      try {
        await saveMarketingPreferences(preferencesPayload)
        savedPreferences = preferencesPayload
        renderPreferences()
        preferencesStatus.textContent = "Saved. FieldsRaven accepted and queued the preference update."
      } catch (error) {
        preferencesStatus.textContent = error.message
      } finally {
        preferencesButton.disabled = false
        preferencesButton.setAttribute("aria-busy", "false")
      }
    })

    renderPreferences()
  </script>
{% else %}
  <p><a href="/account/login">Log in</a> to manage your marketing preferences.</p>
{% endif %}

Shopify and Klaviyo boundaries

The JSON object is the Shopify source shape. If Klaviyo sync is enabled, configure FieldsRaven so scalar keys such as email_frequency, topic_guides, and topic_new_products become the intended custom profile properties. FieldsRaven preserves those scalar names; arrays would instead be flattened into numbered properties such as topics_1 and topics_2, which makes them a poor fit for stable preference flags. Changing these keys later also changes the mapping contract.

A successful 200 response means FieldsRaven accepted and queued the metafield write. It does not prove that Shopify or Klaviyo synchronization has completed. Do not show a “synced to Klaviyo” confirmation from this response alone. The merchant owns consent language and the storefront UI; FieldsRaven owns request validation, queueing, and the configured optional integration.

The Storefront Kit is optional. This complete direct request does not depend on it.

Last updated