Dive into Sitecore CDP - Remarketing with Sitecore CDP and Facebook Ads

Sergey Baranov on February 9, 2022

In this article I`ll show you how to setup Facebook (Meta) Ads for your existing website and how to use advantages of Sitecore CDP to make your advertising more flexible and save your money.

If you want to know more detailed about Facebook Ads integration and see it in action, here is my meetup presentation with demo:

Create Facebook Pixel with Conversions API.

First of all, you need create Facebook Pixel  and install it on your website. In your facebook account navigate to Events Manager -> Pixel app -> Settings and generate token for Conversions API:

Sitecore CDP - Facebook Conversation API token

The second key that you need is Pixel ID:

Sitecore CDP - Facebook Ads Pixel ID

Once you have Pixel ID and token it is time to test connection. In your facebook account navigate to Events Manager -> Pixel app -> Test Events, copy test event code (TEST37153 in my case) for future testing in Sitecore CDP and open Graph API Explorer:

Sitecore CDP - Test Facebook Events 

Insert your token in Access Token input and click submit button:

Sitecore CDP - Facebook developer console 

If you see response like on screenshot above, your connection is OK. If you return to Test Events tab you will see that your event is successfully tracked:

Sitecore CDP - Facebook events tracking

Install Facebook Pixel on your website.

Once you have Pixel ID, you need to insert facebook pixel integration javascript snippet in a layout page of your website:


<!-- Facebook Pixel Code -->
<script>
  !function(f,b,e,v,n,t,s)
  {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
  n.callMethod.apply(n,arguments):n.queue.push(arguments)};
  if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
  n.queue=[];t=b.createElement(e);t.async=!0;
  t.src=v;s=b.getElementsByTagName(e)[0];
  s.parentNode.insertBefore(t,s)}(window, document,'script',
  'https://connect.facebook.net/en_US/fbevents.js');
  fbq('init', '{your-pixel-id-goes-here}');
  fbq('track', 'PageView');
</script>
<noscript>
  <img height="1" width="1" style="display:none" 
       src="https://www.facebook.com/tr?id={your-pixel-id-goes-here}&ev=PageView&noscript=1"/>
</noscript>
<!-- End Facebook Pixel Code -->

How Facebook matches your website visitors with their facebook accounts?

When facebook`s script is loaded, it hits facebook with fingerprint of visitor device/browser/etc. Facebook stores this fingerprint and generates _fbp cookie based on fingerprint.

There are 2 ways: your visitor is logged in to facebook in the same browser or not :

  1. If facebook knows your visitor account, it includes this information in _fbp cookie, and all events triggered on your website are immediately associated with this visitor facebook account.
  2. The second case: if you visitor is not logged in to facebook, _fbp cookie just contains visitor fingerprint and all events that are triggered on your website are just stored in facebook as anonymous and just used for data mining. But as soon as this visitor login to facebook and facebook identify this visitor account, all his history (that was anonymous in the past) will be immediately recognized by fingerprints and associated with visitor facebook account.

Push events to Facebook and Sitecore CDP.

On frontend side you need to trigger requests to facebook and CDP for all types of your events View, AddToCard, Payment, ClearCard, Identity, etc.There is a snippet of code that will be useful. Main function is addEventTracker that is called in format addEventTracker('View', data, 'boxever, fbq'), where first parameter is event type, third is list of systems where you want to push event, second is event data (see Facebook pixel specification for each event type for more detailes).




export const sendBoxeverCallFlows = (options, callback) => {
  const { params, friendlyId } = options;

  if (window.Boxever) {
    const flowData = {
      clientKey: Boxever.client_key,
      browserId: Boxever.getID(),
      channel: 'WEB',
      language: getCookie('order-cloud#lang').toLowerCase() || 'en',
      pointOfSale: window.location.host,
      currencyCode: localStorage.getItem('ocCurrency') || 'EUR',
      params: params,
      friendlyId: friendlyId,
    };

    Boxever.callFlows(flowData, function (response) {
      if (callback) callback(response);
    });
  }
};

export const sendBoxeverEvent = (type, options, callback) => {
  const { page, currency, ext } = options;

  _boxeverq.push(function () {
    const boxeverEvent = {
      browser_id: Boxever.getID(),
      channel: 'WEB',
      type: type,
      language: getCookie('order-cloud#lang').toUpperCase() || 'EN',
      pos: window.location.host,
      currency: currency,
      page: page || '/',
      ext: ext,
    };

    Boxever.eventCreate(
      boxeverEvent,
      function (data) {
        if (callback) callback();
      },
      'json'
    );
  });
};

export const sendBoxeverSearchEvent = (query, options, callback) => {
  const { page, currency, ext } = options;

  _boxeverq.push(function () {
    let boxeverEvent = {
      browser_id: Boxever.getID(),
      channel: 'WEB',
      type: 'SEARCH',
      language: getCookie('order-cloud#lang').toUpperCase() || 'EN',
      pos: window.location.host,
      currency: currency,
      page: page || '/',
      search_parameters: {
        search_input: query,
      },
      ext: ext,
    };

    boxeverEvent = Boxever.addUTMParams(boxeverEvent);
    Boxever.eventCreate(
      boxeverEvent,
      function (data) {
        if (callback) callback();
      },
      'json'
    );
  });
};

export const sendBoxeverIdentityEvent = (user, options, callback) => {
  const { page, currency, ext } = options;

  _boxeverq.push(function () {
    const boxeverEvent = {
      browser_id: Boxever.getID(),
      channel: 'WEB',
      type: 'IDENTITY',
      language: getCookie('order-cloud#lang').toUpperCase() || 'EN',
      pos: window.location.host,
      currency: currency,
      page: page || '/',
      email: user.Email,
      firstName: user.FirstName,
      lastName: user.LastName,
      ext: ext,
    };

    Boxever.eventCreate(
      boxeverEvent,
      function (data) {
        if (callback) callback();
      },
      'json'
    );
  });
};

export const sendBoxeverAddEvent = (product, options, callback) => {
  const { page, currency } = options;

  _boxeverq.push(function () {
    const boxeverEvent = {
      browser_id: Boxever.getID(),
      channel: 'WEB',
      type: 'ADD',
      language: getCookie('order-cloud#lang').toUpperCase() || 'EN',
      pos: window.location.host,
      currency: currency,
      page: page || '/',
      product: {
        type: 'STANDARD',
        productId: product.ID,
        name: product.Name,
        orderedAt: new Date(Date.now()).toISOString(),
        quantity: product.Quantity,
        price: product.Price.toFixed(2),
        currencyCode: currency || 'EUR',
        originalPrice: product.Price.toFixed(2),
        originalCurrencyCode: currency || 'EUR',
        referenceId: product.ReferenceId,
        item_id: product.ID,
        ext: product.Ext,
      },
    };

    Boxever.eventCreate(
      boxeverEvent,
      function (data) {
        if (callback) callback();
      },
      'json'
    );
  });
};

export const sendBoxeverConfirmEvent = (product, options, callback) => {
  const { page, currency } = options;

  _boxeverq.push(function () {
    const boxeverEvent = {
      browser_id: Boxever.getID(),
      channel: 'WEB',
      type: 'CONFIRM',
      language: getCookie('order-cloud#lang').toUpperCase() || 'EN',
      pos: window.location.host,
      currency: currency,
      page: page || '/',
      product: product,
    };

    Boxever.eventCreate(
      boxeverEvent,
      function (data) {
        if (callback) callback();
      },
      'json'
    );
  });
};

export const sendBoxeverPaymentEvent = (options, callback) => {
  const { page, currency } = options;

  _boxeverq.push(function () {
    const boxeverEvent = {
      browser_id: Boxever.getID(),
      channel: 'WEB',
      type: 'PAYMENT',
      language: getCookie('order-cloud#lang').toUpperCase() || 'EN',
      pos: window.location.host,
      currency: currency,
      page: page || '/',
      pay_type: 'Other',
    };

    Boxever.eventCreate(
      boxeverEvent,
      function (data) {
        if (callback) callback();
      },
      'json'
    );
  });
};

export const sendBoxeverCheckoutEvent = (order, options, callback) => {
  const { page, currency, ext } = options;

  _boxeverq.push(function () {
    const boxeverEvent = {
      browser_id: Boxever.getID(),
      channel: 'WEB',
      type: 'CHECKOUT',
      language: getCookie('order-cloud#lang').toUpperCase() || 'EN',
      pos: window.location.host,
      currency: currency,
      page: page || '/',
      referenceId: order.ID,
      status: order.Status,
      ext: ext,
    };

    Boxever.eventCreate(
      boxeverEvent,
      function (data) {
        if (callback) callback();
      },
      'json'
    );
  });
};

const uuidv4 = () => {
  return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  );
};

const sha256 = async (message) => {
  // encode as UTF-8
  const msgBuffer = new TextEncoder().encode(message);                    
  // hash the message
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
  // convert ArrayBuffer to Array
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  // convert bytes to hex string
  const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('');

  return hashHex;
};
/* eslint-enable */

export const addEventTracker = (type, data, trackBy) => {
  const isBoxever = trackBy.indexOf('boxever') > -1;
  const isFbq = trackBy.indexOf('fbq') > -1;
  const guid = isFbq ? uuidv4() : undefined;
  const currency = localStorage.getItem('ocCurrency') || 'EUR';

  switch (type) {
    case 'Identity': {
      const { user } = data;

      sha256(user.Email).then((ecrEmail) => {
        if (isBoxever && window.Boxever) {
          sendBoxeverIdentityEvent(user, { page: window.location.pathname, currency: currency });
          getBoxeverUser(user.Email).then((response) => {
            if (response && response.items.length > 0) {
              setBoxeverUserExtensions(response.items[0].href, 'external_id', ecrEmail);
            }
          });
        }
        if (isFbq && window.fbq) {
          fbqInit({
            external_id: ecrEmail,
          });
        }
      });
      break;
    }

    case 'View': {
      const page = data.page;

      if (isBoxever && window.Boxever) {
        sendBoxeverEvent('VIEW', { page: page, currency: currency });
      }
      break;
    }

    case 'Search': {
      const { query } = data;

      if (isBoxever && window.Boxever) {
        sendBoxeverSearchEvent(query, { page: window.location.pathname, currency: currency });
      }
      break;
    }

    case 'ViewProduct': {
      const { product, page } = data;

      if (isBoxever && window.Boxever) {
        sendBoxeverEvent('VIEW', {
          page: page,
          currency: currency,
          ext: {
            productId: product.ID,
            facebook_event_id: guid,
            fbp: getCookie('_fbp'),
          },
        });
      }
      if (isFbq && window.fbq) {
        fbqTrack(
          'ViewContent',
          {
            content_ids: [product.ID],
            content_name: product.Name,
            value: product.PriceSchedule.PriceBreaks[0].Price,
            currency: currency,
            contents: [
              {
                id: product.ID,
              },
            ],
            content_type: 'product',
          },
          { eventID: guid }
        );
      }
      break;
    }

    case 'Checkout': {
      const { cart, order, page } = data;

      if (isBoxever && window.Boxever) {
        sendBoxeverEvent('VIEW', {
          page: page,
          currency: currency,
          ext: {
            facebook_event_id: guid,
            fbp: getCookie('_fbp'),
            cart: cart.map((ci) => {
              return {
                ProductID: ci.ProductID,
                Quantity: ci.Quantity,
                UnitPrice: ci.UnitPrice,
              };
            }),
            total: order.Total,
          },
        });
      }
      if (isFbq && window.fbq) {
        fbqTrack(
          'InitiateCheckout',
          {
            content_ids: cart.map((li) => li.ProductID),
            content_name: 'Checkout',
            value: order.Total,
            currency: currency,
            contents: cart.map((li) => {
              return { id: li.ProductID };
            }),
            content_type: 'product',
          },
          { eventID: guid }
        );
      }
      break;
    }

    case 'ClearCart': {
      if (isBoxever && window.Boxever) {
        sendBoxeverEvent('CLEAR_CART', { page: window.location.pathname, currency: currency });
      }
      break;
    }

    case 'AddToCart':
    case 'UpdateCart': {
      const { product, quantity, orderId } = data.extendedProduct;
      const { cart } = data;

      if (isBoxever && window.Boxever) {
        sendBoxeverAddEvent(
          {
            Name: product.Name,
            Quantity: quantity,
            Price: product.PriceSchedule.PriceBreaks[0].Price,
            originalPrice: product.PriceSchedule.PriceBreaks[0].Price,
            ID: product.ID,
            ReferenceId: orderId,
            Ext: {
              cart: cart,
              total: cart.reduce(
                (total, item) => total + Number(item.UnitPrice) * Number(item.Quantity),
                0
              ),
              image: product.xp.Images[0].Url || 'http://via.placeholder.com/300x300?text=No Image',
              facebook_event_id: guid,
              fbp: getCookie('_fbp'),
            },
          },
          { page: window.location.pathname, currency: currency }
        );
      }
      if (isFbq && window.fbq) {
        fbqTrack(
          'AddToCart',
          {
            content_ids: [product.ID],
            content_name: product.Name,
            value: product.PriceSchedule.PriceBreaks[0].Price,
            currency: currency,
            contents: [
              {
                id: product.ID,
                quantity: quantity,
              },
            ],
            content_type: 'product',
          },
          { eventID: guid }
        );
      }
      break;
    }

    case 'Payment': {
      if (isBoxever && window.Boxever) {
        sendBoxeverPaymentEvent({ page: window.location.pathname, currency: currency });
      }
      break;
    }

    case 'Confirm': {
      const { productList } = data;

      if (isBoxever && window.Boxever) {
        sendBoxeverConfirmEvent(productList, {
          page: window.location.pathname,
          currency: currency,
        });
      }
      break;
    }

    case 'Purchase': {
      const { cart, order, page } = data;

      if (isBoxever && window.Boxever) {
        sendBoxeverCheckoutEvent(
          { ID: order.ID, Status: 'PURCHASED' },
          {
            page: window.location.pathname,
            currency: currency,
            ext: {
              facebook_event_id: guid,
              fbp: getCookie('_fbp'),
              cart: cart.map((ci) => {
                return {
                  ProductID: ci.ProductID,
                  Quantity: ci.Quantity,
                  UnitPrice: ci.UnitPrice,
                };
              }),
              total: order.Total,
            },
          }
        );
      }
      if (isFbq && window.fbq) {
        fbqTrack(
          'Purchase',
          {
            content_ids: cart.map((li) => li.ProductID),
            content_name: 'Purchase',
            value: order.Total,
            currency: currency,
            contents: cart.map((li) => {
              return {
                id: li.ProductID,
                quantity: li.Quantity,
              };
            }),
            content_type: 'product',
          },
          { eventID: guid }
        );
      }
      break;
    }

    default: {
      break;
    }
  }
};



How to setup Facebook Conversions API connection in Sitecore CDP?

Login to your Sitecore CDP application account, Connections -> Add Connection-> Destination. Enter Name and Description of your connection, choose None on Athentification tab and configure request parameters:

Replace {pixel_id} and {token} with your real values. To test your connection copy resuest body from recently tested facebook Graph API explorer:

Sitecore CDP - Destination connection to Facebook

If you testing request is successfull,  you will see recieved event in facebook Test Events tab. Save your connection with default mappings, don`t worry about testing Request body: it will be overwritten in triggered experience webhook in the future.

 

How to configure triggered experience?

In Sitecore CDP navigate to Experiences -> Full Stack -> Create Experience -> Triggered Experience. In Choose Connection popup select your recently created Facebook destination connection:

Sitecore CDP - Triggered Experience

For this demo I`ll show you how to forward VIEW event from your website to Facebook Conversions API (for your real solution you can add more events as a trigger). Navigate to Trigger section and add custom trigger as shown on image below:

Sitecore CDP - Add event trigger

Go to Webhook Composer section and click on Edit button. Paste this Freemarker code into API tab:

 


<#assign pageUrl = "https://" + entity.pointOfSale + entity.arbitraryData.page>
<#assign timestamp = (.now?long / 1000)?floor?c>
<#assign guestUnique = guest.ref?replace("-","") + guest.ref?replace("-","")>
{
   "data": [
      {
         "event_name": "ViewContent",
         "event_time": ${timestamp},
         "event_source_url": "${pageUrl}",
         "action_source": "website",
         "user_data": {
            "em": "${guestUnique}",
            "client_user_agent": "Sitecore CDP"
         }
      }
   ],
   "test_event_code" : "TEST37153"
}

Parameters explanation:

  • event_time - timestamp of triggered event in unix format (in seconds),
  • user_data.em - any unique identifier of user in sha256 format. Unfortunatelly, I didn`t find how to caclulate sha256 in Freemarker, so for this demo example I just concatenate 2 times guest.ref value that is unique user identitier and looks like sha256; for real solution I store already sha256-hashed value of user email in extended guest profile properties.
 

Start your experience, navigate to your website and generate some VIEW events. Go to your experience -> Operations -> Execution Repost where you will see statuses of youe triggered VIEW events. If all is OK you will see SUCCESS status:

Sitecore CDP - Triggered Experience Execution Report

Because of we also passed test_event_code parameter, we can see that our events successfully hit facebook:

Sitecore CDP - Success trigger  

NOTE: Remove test_event_code parameter for production events.

In the next article I`ll show you how to setup Facebook Ads with triggered experience more flexible by using decision modeling. Stay updated!

Here goes the table of contents for my Sitecore CDP blog series: