Conversions API

Overview

If you would like access to Yelp's conversion API. Please ask for this directly from your assigned Yelp Sales or CS representative.


This technical documentation has been crafted to provide you with all the necessary information to get started with our Conversions api (aka CAPI). From setup instructions to data formatting, you'll find everything you need right here. We've made every effort to ensure that this document is comprehensive and user-friendly, providing you with everything you need to get up and running.

However, we understand that questions may arise along the way or there may be specific queries that are not addressed in this documentation. In such cases, please don't hesitate to reach out to your client success representative or solutions engineer.

Please note the below terms that govern use of Yelp's CAPI:

  1. If you're a Yelp advertiser: by proceeding to use CAPI, you agree and acknowledge that attribution services are subject to Yelp's Attribution terms.
  2. If you're implementing Yelp's CAPI on behalf of an advertiser: by proceeding to use CAPI, you agree and acknowledge that attribution services are subject to Yelp's Attribution terms (the “Terms”) and that you will act as “agent” on behalf of the advertisers for purposes of the Terms.

Please see the below table of contents to help you navigate through this document. Both the API and S3 data methods will require you to prepare your data, however the GTM Tag Template will handle that step for you, so feel free to jump right into that.

Table of Contents

  1. General Steps for Implementation
  2. What Data You’ll Need
  3. Preparing Your Data
    1. Conversion Data Format
    2. Normalizing Data
    3. Hashing Data
    4. Example Formatted Data
    5. Conversion Deduplication
  4. Data Transfer Methods
    1. Uploading Data via S3
    2. Sending Data via API
      1. Conversion APIs
      2. Rate Limiting
      3. Errors
      4. Data Access
    3. Google Tag Manager Integration
      1. Configure your Tagging Server URL
      2. Configure your GA4 User Data
      3. Configure GA4 Event Schema
      4. Final Setup
    4. Appendix
      1. Example API Requests

General Steps for Implementation

  1. Collect the data needed to send to Yelp. See Preparing Your Data section.
  2. Create a Yelp Fusion app and retrieve client ID and API key. See Data Access section.
  3. Send client ID to Yelp via the CAPI integration email thread. See Data Access section.
  4. Yelp will activate the API key. See Data Access section.
  5. Advertiser hashes the below data and sends it to the conversion endpoint at their desired frequency. We recommend sending data at least once every month. Weekly, daily, or in real-time are also fine.

What Data You’ll Need

The following is the information we are asking for to run CAPI. While not all information is required, the more information you send the more accurate CAPI reports will be. Please send us all available information. Any information in bold is strongly encouraged. See “Preparing Your Data” for the chart that outlines the parameter names, constraints, and specifics on the description.

  • Timestamp for the conversion
  • EventID
  • Event name for the type of conversion
  • Source from where the conversion took place
  • Email address and/or phone number for the customer
  • First and last name
  • Birth date
  • Gender
  • Country code
  • City
  • State
  • Zip code
  • Unique IDs (such as membership IDs or others that your business uses)
  • IP address and user agent info
  • Purchase amount (for measuring transactions only)

Preparing Your Data

Conversion Data Format

To properly ingest data into the CAPI system, conversion data must first be formatted as JSON and must match the provided schema below.


{
"event_id": String,
"event_time": Integer,
"event_name": String,
"action_source": String,
"user_data": {
"em": Array[String],
"fn": String,
"ln": String,
"db": String,
"ge": String,
"ph": Array[String],
"country": Array[String],
"st": Array[String],
"zp": Array[String],
"ct": Array[String],
"external_id": Array[String],
"client_ip_address": String,
"client_user_agent": String,
"madid": String
},
"custom_data": {
	"value": Float,
	"currency": String,
"order_id": String,
"content_category": String,
"content_ids": Array[String],
"contents": Array[Object]
}
}

The following is a detailed description of each property within the schema:

Parameter NameTypeRequiredConstraintsDescription
event_id StringNolength <= 128 charsRepresents a unique identifier for the conversion and will be used in the deduplication process. Although it is optional, it is highly recommended.
event_timeIntegerYes< current timeRepresents the date and time the conversion event occurred in the past. It should be formatted as a valid Unix Timestamp in seconds.
event_nameStringYesOptions:

add_payment_info
add_to_cart
add_to_wishlist
purchase
search
checkout
lead
view_content
view_category
signup
watch_video
custom*

length <= 50 chars
Indicates the type of conversion that occurred. Used for aggregation during the attribution process. This is also used in combination with eventid as part of the deduplication process.

Custom values can be used, but must be prefixed with “custom
” and must only contain the following characters: A-Z a-z 0-9 _-

eg. custom_file_download.
action_sourceStringYesOptions:

“app”,
“physical_store”, “website”
Indicates where the conversion took place. Currently limited to “app”, “physical_store” and “website”.
user_dataObjectNoContains all user specific data that can be used during the matching process. The more data that is provided, the higher the chance and quality of a match occurring.
[user_data]
em
Array[String]NoNormalized/HashedCustomer’s email address(s). Can contain special characters but must be lowercase. Trim any leading and trailing spaces.
[user_data]
fn
StringNoNormalized/HashedCustomer’s first name. Should be all lowercase with no spaces and no punctuation.
[user_data]
ln
StringNoNormalized/HashedCustomer’s last name. Should be all lowercase with no spaces and no punctuation.
[user_data]
ph
Array[String]NoNormalized/HashedCustomer’s phone #(s).

Should include country code, area code, and number. Digits only, no special characters and no leading zeros.

eg. 12031231234
[user_data]
db
StringNoNormalized/HashedCustomer’s birth date in the format of YYYYMMDD.

eg. 20010414
[user_data]
ge
StringNoNormalized/HashedCustomer’s gender. Should be one character of (f, m, n) to represent Female, Male, or Non-binary.
[user_data]
country
Array[String]NoNormalized/HashedCustomer’s country. Should be formatted as a two-character lowercase ISO-3166 country code.

Example:
us
[user_data]
st
Array[String]NoNormalized/HashedCustomer’s state or province. Should be formatted as a two-character lowercase abbreviation.

Example:
ca
[user_data]
zp
Array[String]NoNormalized/HashedCustomer’s zip or postal code. Should contain only lowercase alphanumeric characters.

eg. 94101 or m3c0c1
[user_data]
ct
Array[String]NoNormalized/HashedCustomer’s city. Should not contain spaces or punctuation.

Example:
newyork
[user_data]
external_id
Array[String]NoHashedAny unique IDs associated with the conversion user, for example, membership id.
[user_data]
client_ip_address
StringNoNot HashedCustomer’s IP address in either IPV4 or IPV6 format. This should be included with client_user_agent to maximize match rates.

eg. 111.111.111.111 or 2001:db8:3333:4444:5555:6666:7777:8888
[user_data]
client_user_agent
StringNoNot HashedCustomer’s user agent for their browser. This should be included with client_ip_address to maximize match rates.
[user_data]
madid
StringNoCustomer’s mobile advertiser ID. (IDFA/AAID)

This is for mobile events only.
custom_dataObjectYesContains custom data specific to the conversion.
[custom_data]
value
FloatOnly for "purchase" eventsTotal monetary value of the conversion. This is a required field to determine ROAS, so must be included if event_name is “purchase”. This value cannot be negative, Yelp's API will respond with an error if we receive a negative order value.

eg. 100.23
[custom_data]
currency
StringNoOptions:

“USD”
Currency must be a valid ISO 4217 three-digit currency code. Currently limited to “USD”.
[custom_data]
order_id
StringNoTransaction ID or order ID tied to the conversion event.
[custom_data]
content_category
StringNoA custom category that the conversion belongs to.
[custom_data]
content_ids
Array[String]NoSub-item ids associated with this conversion. Could be the SKU ids of all the products included in the same purchase.

Example:
["SKU_ID_1", "SKU_ID_2"]
[custom_data]
contents
Array[Object]NoAdditional information on the contents of this conversion in the form of a JSON object.

Example:
[{'id': 'SKU_ID_1', 'quantity': 1, 'item_price': 99.00}, {'id': 'SKU_ID_2', 'quantity': 1, 'item_price': 1.23}]

Normalizing Data

You’ll notice that some of the data is required to be normalized. Data is considered normalized when it has been converted to all lowercase characters, and all spaces are removed, including at the beginning and end of the string. Additionally, some data is required to have all special characters removed. If this is required, it will be noted in the description column of the table above.

Example data that has not been normalized:

+1 (777) -111-1314

[email protected]

First

Example of the same data after being normalized:

17771111314

[email protected]

first


Hashing Data

For privacy and legal reasons, some user data is required to be encrypted before being sent to Yelp. In these cases, the data should be hashed using the hashing algorithm SHA-256. This will ensure that the data remains private but also that it can be properly matched to Yelp events. If data is required to be hashed, it will be noted in the constraints column of the table above.

❗️

Please note that our system is designed to prevent unhashed personal identification data from being processed.


Example Formatted Data

{      
  "event_id":"bd15c2b0-2375-4f53-91b1-a959cfd30ee1",
    "event_time":1685969768,
    "event_name":"purchase",
    "action_source": "physical_store",
    "user_data":{
      "em": [
        "87924606b4131a8aceeeae8868531fbb9712aaa07a5d3a756b26ce0f5d6ca674"
      ],
      "fn": "96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a",
      "ln": "6627835f988e2c5e50533d491163072d3f4f41f5c8b04630150debb3722ca2dd",
      "db": "d8f65f715e0ca17f3233cfc8033491ca844a33edc92f671404f6821391bce200",
      "ge": "62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a",
      "ph": [
        "9107723e93f766c6f93357d2968323fbf51c2846362eb0ae4ab7c3de3a443a8f"
      ],
      "country": [
        "79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621"
      ],
      "st": [
        "6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126"
      ],
      "zp": [
        "9de3888231a1021acddcaa006b2f2a1ead5e245ae44a6b50e997afa9371e081e"
      ],
      "ct": [
        "fe3383cbbd5c28319f882b69e71ec83b62b5ab5b18db0e091a08b211f50e6ef0"
      ],
      "external_id": ["98e8c7b0d9b0d0000f6fa648addd30b9276a9bde5d8c3f6208556d6c6c612103"],
      "client_ip_address": "111.111.111.111",
      "client_user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1",
      "madid": "ABCABCAB-AAAA-1AAA-AAAA-ABCACBABCABA"
    },
    "custom_data": {
      "value": 100.55,
      "currency": "USD",
      "content_category": "grocery",
      "order_id": "bd15c2b0-2375-4f53-91b1-a959cfd30ee1",
      "content_ids": [
        "abf180ef-083b-45a4-852d-8d323756991b"
      ],
      "contents": [
        {
          "id": "abf180ef-083b-45a4-852d-8d323756991b",
          "quantity": 1,
          "item_price": 100.55
        }
      ]
    }
  }

Conversion Deduplication

To ensure the highest reporting accuracy possible, we’ve provided two fields that can be populated to help with data deduplication. This ensures we only attribute an event you care about (e.g. transaction, lead) to Yelp ads once.

  • event_id
  • event_name

An event_id can be any string that can be used to uniquely identify a conversion. This could be something like an order ID, or an internally assigned identifier. Including an event_id is optional, but is highly recommended to help catch conversion duplicates from being included in attribution reports.

When combined, these two values form a unique key that Yelp can use to differentiate one conversion from another. If conversions are submitted without an event_id, no deduplication can be performed and it is assumed that each conversion is unique and should be included in resulting reports. Additionally, if the same event_id is used for multiple conversions, it could potentially lead to a loss of data.


Data Transfer Methods

Uploading Data via S3

When you have finished preparing your conversion data and have made it secure, it's time to upload it to Yelp. Don't worry, we have made the process easier for you. Here's what you need to do:

  1. Data Format: Data needs to be in JSON format. Files containing multiple conversions should be written as NDJson (Newline Delimited JSON), and should contain one valid JSON formatted conversion per line.
  2. Upload Location: We have created an account for you and provisioned access key pairs. These keys will grant you access to a special storage space called an S3 bucket controlled by Yelp. It allows you to upload files securely.
  3. Access Key: We will provide you with a temporary 1Password link containing the access keys. It will require email verification to access but does not require a 1Password account.
  4. Setting Up AWS Account: To proceed, you need to configure your AWS account with the correct access keys. You can install the AWS Command Line Interface (CLI) tool by following their documentation. Once installed, run the command:
    aws configure
    
  5. Uploading the File: Once your account is configured, uploading the file is a breeze. Use the following command to upload the JSON file to the Yelp S3 bucket:
    aws s3 cp {filename}.json
    s3://yelp-external-capi/{client_id}/{filename}.json
    
    Replace {filename} with the name of your file (which should be unique every time you upload) and {client_id} with the ID provided to you. It's recommended to include the date range of transactions in the file name for better organization.

That's it! Your data will be securely uploaded to Yelp's S3 bucket and ready for further processing.


Sending Data via API

Conversion APIs

To give you as much flexibility as you need, we have built and deployed two different API endpoints: Bulk and Single. The only difference between these two endpoints is that the bulk expects an array of Conversion Events and the single expects a single event.

If you're sending more than 100k events per day. We strongly suggest implementing the Bulk Events endpoint.


NameAPI Request URLDescription
Single Eventhttps://api.yelp.com/v3/conversion/eventPOST Endpoint which takes a single JSON object with the schema listed here.
Bulk Eventshttps://api.yelp.com/v3/conversion/eventsPOST Endpoint which takes an array of JSON objects with the schema listed here. Limited to 1,000 events per request.

Test Events

Our APIs support an optional parameter named "test_event," which is designed to facilitate the testing phase of integration.
This parameter accepts a boolean value. When set to true, it enables the API to perform all validations as it would under normal operational conditions, yet it prevents the submission of data to the production environment.
This feature is particularly useful for verifying that the data being sent conforms to our required structure and formatting specifications, without the risk of introducing test data into the production system. It is important to note that the "test_event" parameter is not mandatory; in its absence, the system assumes a default value of false, indicating standard operation with data being directed to production.


Rate Limiting

As part of our API, we do have some limits on how often you can send data. Each of our endpoints will accept up to 100 requests per second and 50,000 requests per day.

If you expect to send more events per day, let us know and we're happy to discuss the limits. Otherwise we recommend you batch your requests and send them through the Bulk Events endpoint.


Errors

There are several error codes returned that might occur through your use of these endpoints.

NameCodeReason
Unauthorized401Verify API Key is correctly attached in the headers. The error will include which part of the authentication chain you are missing. Otherwise please reach out to us!
Bad Request400Fields which are marked required in the schema must be included and the fields marked hashed must be hashed. We will let you know if fields are not hashed or are not included where necessary.
Too Many Requests429You’ve hit the rate limits for these endpoints. Let us know if you need to modify these and we’re happy to work with you.
Not Found404Verify that our team has reached out to you to let you know that your key is active. The endpoints are currently delisted and not available without the correct API key.

Data Access

When you have finished preparing your system to generate your conversion data in JSON, normalized and hashed, it's time to send it to Yelp. Here's what you need to do:

  1. Sign up for developer access: Head on over to https://www.yelp.com/developers/v3/manage_app and sign in with your consumer Yelp account.

  2. Create New App: You will be presented with a screen asking you to create a new app. Pick a name that will help the application be identified in the future, and please use a contact email that will allow us to contact someone in the event of any discrepancies. In the description, feel free to describe how you intend to use this API. You might be asked to select a plan or tier, it does not matter what you select for this option since we'll be applying custom configurations to your account. When you’re satisfied, you must agree to the Yelp API Terms and Display Requirements. Let us know if you have any questions. Upon completion, click Create New App.

  3. App Setup: When you have created the application, you will be faced with a new screen which allows you to update the information listed previously, as well as show you two key pieces of information:

    1. Client ID: Please copy this ID and send it to your customer success representative. This will allow us to connect your account to this App, and give the API Key access to our Conversion API.
    2. API Key: This is required for using the Conversion API and will be attached to requests made. We will go into further detail in the next steps. Make note of this key and consider it secret. Do not give this to anyone you do not authorize to send conversion events to us. If you believe this key has been leaked or in any way compromised, there is a button further down the page which will allow you to refresh it. It will immediately disable the key and generate a new one.
  4. Client Integration: Once you’ve given us your Client ID for your application, we will enable your API key to have access to our endpoints. CAPI will use the same authentication that’s detailed here.

That’s it! Once we’ve let you know that your client app is approved and ready, you can start hitting our endpoints with data. For example events, please see the Appendix for sample single or bulk events.


Google Tag Manager Integration

We strongly encourage folks to use the API or the S3 method if possible. Debugging any issues that may arise with the Tag Manager implementation can be quite cumbersome.

This document is for integrating the Yelp CAPI Template Tag into your existing GTM Setup. This method will use GTM Server-side tagging with a custom tag template, built on Google Analytics 4 (GA4) so please confirm your system is set up to support this.

Additionally, the GTM integration is slightly different from the API access above, but will use the API under the hood, so please familiarize yourself with the API integration. However the tag itself will handle normalizing and hashing the correct fields for ingestion, so you don’t need to worry about handling that yourself.

Configure your Tagging Server URL

When configuring your web server to send data to your server container, make sure you set first_party_collection flag to true. You must do this to be able to pass the user_data parameters to the Server-Side container. Under Fields to Set, click Add Row and set:

  • Field name: first_party_collection
  • Field value: true

Configure your GA4 User Data

You can create a Data Layer Variable for each of the common GTM event schema’s user_data parameters. You can read more about this here. For example, you can create a variable like user_data_email_address that can be mapped to the Data Layer Variable name eventModel.user_data.email_address.

If you’re not using the data layer, you will need to configure variables for each parameter. See the below table for data mappings.

Yelp Conversion API ParameterGA4 Field NameGTM Data Layer VariablePriority
Email (em)user_data.email_addresseventModel.user_data.email_addressHighest
Phone (ph)user_data.phone_numbereventModel.user_data.phone_numberHigh
First Name (fn)user_data.address.first_nameeventModel.user_data.address.first_nameMedium
Last Name (ln)user_data.address.last_nameeventModel.user_data.address.last_nameMedium
Date of Birth (db)user_data.date_of_birthN/AMedium
Gender (ge)user_data.genderN/AMedium
Country (country)user_data.address.countryeventModel.user_data.address.countryMedium
State/Province (st)user_data.address.regioneventModel.user_data.address.regionMedium
Zip/Postal Code (zp)user_data.address.postal_codeeventModel.user_data.address.postal_codeMedium
City (ct)user_data.address.cityeventModel.user_data.address.city
External ID (external_id)user_data.external_idN/AMedium
Mobile Ad ID (madid)user_data.madidN/AMedium

Configure GA4 Event Schema

You will need to set up an Event Name for your tag. You can set this as a static variable or from a variable. Yelp Event Names generally match Google equivalents, however, we are expecting to support the following events:

  • add_payment_info
  • add_to_cart
  • add_to_wishlist
  • page_view
  • purchase
  • search
  • checkout
  • lead
  • view_content
  • view_category
  • signup
  • watch_video

We will still ingest others, but will append custom_ to the event name.


Final Setup

You will need a listener on your server container to listen to GA4 events. This is normally configured by default.

Finally, install the Yelp Conversion API Template. It is a server-side tag that will convert the standard event model from the GA4 Client to the Conversions API event schema expected and send it to our API. To install this tag, you will need the Fusion API key from the Data Access section above and will need to enter it into the apiAccessToken template parameters in order to have this sent properly.


Appendix

Example API Requests

Single API

curl --request POST \
     --url https://api.yelp.com/v3/conversion/event \
     --header 'Authorization: Bearer API_KEY' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
"event":{
"event_id":"unique_string_to_identify_this_conversion_event",
"event_time":1693946978,
"event_name":"purchase",
"action_source":"website",
"user_data": {
"em": ["hashed_email_string"],
"fn": "hashed_first_name_string",
"ln": "hashed_last_name_string",
"ph":["hashed_phone_number_string"],
"db": "hashed_date_of_birth_string",
"ge": "hashed_gender_string",
"country":["hashed_country_string"],
"st": ["hashed_street_name_string"],
"zp": ["hashed_zipcode_string"],
"ct": ["hashed_city_string"],
"external_id": ["hashed_external_id_string"],
"client_ip_address": "192.168.1.1",
"client_user_agent": "Chrome/51.0.2704.103",
},
"custom_data": {
		"value":10.0,
	"currency":"USD",
}
},
"test_event": true
}'

Bulk API

curl --request POST \
     --url https://api.yelp.com/v3/conversion/events \
     --header 'Authorization: Bearer API_KEY' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
"events":[{
"event_id":"unique_string_to_identify_this_conversion_event",
"event_time":1693946978,
"event_name":"purchase",
"action_source":"website",
"user_data": {
"em": ["hashed_email_string"],
"fn": "hashed_first_name_string",
"ln": "hashed_last_name_string",
"ph":["hashed_phone_number_string"],
"db": "hashed_date_of_birth_string",
"ge": "hashed_gender_string",
"country":["hashed_country_string"],
"st": ["hashed_street_name_string"],
"zp": ["hashed_zipcode_string"],
"ct": ["hashed_city_string"],
"external_id": ["hashed_external_id_string"],
"client_ip_address": "192.168.1.1",
"client_user_agent": "Chrome/51.0.2704.103",
},
"custom_data": {
		"value":10.0,
		"currency":"USD",
}
}]
"test_event": false
}'

Tag Manager Template

___INFO___

{
  "type": "TAG",
  "id": "cvt_temp_public_id",
  "version": 1,
  "securityGroups": [],
  "displayName": "Yelp Conversion API",
  "brand": {
    "id": "brand_dummy",
    "displayName": ""
  },
  "description": "A server-side tag template to send conversion events to Yelp\u0027s Conversion API",
  "containerContexts": [
    "SERVER"
  ]
}


___TEMPLATE_PARAMETERS___

[
  {
    "type": "TEXT",
    "name": "apiAccessToken",
    "displayName": "Fusion API Access Key",
    "simpleValueType": true,
    "valueValidators": [
      {
        "type": "NON_EMPTY"
      }
    ],
    "help": "To use the Conversion API you need an Access Key. Please talk to your rep on enabling this and see https://docs.developer.yelp.com/docs/fusion-intro for generating a token"
  },
  {
    "type": "CHECKBOX",
    "name": "testInput",
    "checkboxText": "Is this a test event?",
    "simpleValueType": true
  }
]


___SANDBOXED_JS_FOR_SERVER___

const getAllEventData = require('getAllEventData');
const sendHttpRequest = require('sendHttpRequest');
const JSON = require('JSON');
const Math = require('Math');
const getTimestampMillis = require('getTimestampMillis');
const sha256Sync = require('sha256Sync');
const getType = require('getType');
const makeNumber = require('makeNumber');

const eventModel = getAllEventData();

const DEFAULT_ACTION_SOURCE = 'website';

// Get Milliseconds since epoch, divided by 1000 to get seconds for Unix Timestamp
const DEFAULT_EVENT_TIME = Math.round(getTimestampMillis() / 1000);

const EVENT_NAME_MAPPINGS = {
    'checkout': 'checkout',
    'buy': 'purchase',
    'pay': 'purchase',
    'payment': 'purchase',
    'purchase': 'purchase',
    'page_view': 'page_view',
    'pageview': 'page_view',
    'pagevisit': 'page_view',
    'gtm.dom': 'page_view',
    'addtocart': 'add_to_cart',
    'add_to_cart': 'add_to_cart',
    'generate_lead': 'lead',
    'lead': 'lead',
    'addpaymentinfo': 'add_payment_info',
    'add_payment_info': 'add_payment_info',
    'addtowishlist': 'add_to_wishlist',
    'add_to_wishlist':'add_to_wishlist',
    'viewcontent': 'view_content',
    'view_content': 'view_content',
    'viewcategory': 'view_category',
    'view_category': 'view_category',
    'sign_up': 'signup',
    'signup': 'signup',
    'watchvideo': 'watch_video',
    'watch_video': 'watch_video',
    'search': 'search',
};

const DEFAULT_EVENT_NAME = 'custom_event';


// Functions
function isAlreadyHashed(input) {
    return input && (input.match('^[A-Fa-f0-9]{64}$') != null);
}

function replaceAll(string, search, replace) {
  return string.split(search).join(replace);
}

function hashFunction(input, removeSpaces) {
    const shouldRemoveSpaces = removeSpaces || false;
    const type = getType(input);
    let formattedInput = input;
    if (type == 'undefined' || input == null || input == 'undefined' || input == '') {
        return undefined;
    }

    if (isAlreadyHashed(input)) {
        return input;
    }

    if (shouldRemoveSpaces) {
        formattedInput = replaceAll(input,' ', '');
    }

    return sha256Sync(formattedInput.trim().toLowerCase(), { outputEncoding: 'hex' });
}

function getYelpEventName(eventName) {
    if (!eventName) return DEFAULT_EVENT_NAME;

    const lowerEventName = eventName.toLowerCase();

    return EVENT_NAME_MAPPINGS[lowerEventName] || 'custom_' + lowerEventName;
}

function normalizeAndHashGender(inputGender) {
    if (getType(inputGender) == 'string' && inputGender.length > 0) {
        return hashFunction(inputGender.charAt(0));
    }
    return null;
}

function normalizeAndHashPhone(inputPhone) {
    if (getType(inputPhone) == 'string') {
        return hashFunction(replaceAll(inputPhone, '-', '').replace('+', '').replace('(', '').replace(')', ''), true);
    }
    return null;
}

function getContentIdsFromItems(items) {
    return items.map(item => (item.item_id || item.item_name) || undefined);
}

function getContentFromItems(items) {
    return items.map(item => {
        return {
            "id": (item.item_id || item.item_name) || undefined,
            "item_price": item.price || undefined,
            "quantity": item.quantity || undefined,
        };
    });
}

function checkPreHashedUserData(userObject, field) {
    const sha256PrefixedField = 'sha256_' + field;
    if (userObject[sha256PrefixedField]) {
        if (isAlreadyHashed(userObject[sha256PrefixedField])) {
            return userObject[sha256PrefixedField];
        }
    }
    if (userObject[field]) {
        return hashFunction(userObject[field]);
    }

    return null;
}

function setUserData(modelUserData) {
    const userData = {};

    const em = checkPreHashedUserData(modelUserData, 'email_address');

    if (em) userData.em = [em];
    else {
        if (modelUserData.em) {
            userData.em = [hashFunction(modelUserData.em)];
        }
    }

    const ph = checkPreHashedUserData(modelUserData, 'phone_number');

    if (ph) userData.ph = [ph];
    else {
        if (modelUserData.ph) {
            userData.ph = [hashFunction(modelUserData.ph)];
        }
    }

    if (modelUserData.gender) {
        userData.ge = normalizeAndHashGender(modelUserData.gender);
    }

    if (modelUserData.date_of_birth) {
        userData.db = hashFunction(modelUserData.date_of_birth);
    }

    if (modelUserData.address != null) {
        const addressData = modelUserData.address;

        const fn = checkPreHashedUserData(addressData, 'first_name');

        if (fn) userData.fn = fn;

        const ln = checkPreHashedUserData(addressData, 'last_name');

        if (ln) userData.ln = ln;

        if (addressData.country)
            userData.country = [hashFunction(addressData.country)];

        if (addressData.region)
            userData.st = [hashFunction(addressData.region)];

        if (addressData.postal_code)
            userData.zp = [hashFunction(addressData.postal_code, true)];

        if (addressData.city)
            userData.ct = [hashFunction(addressData.city, true)];
    }

    if (modelUserData.external_id) {
        userData.external_id = [hashFunction(modelUserData.external_id)];
    }

    if (modelUserData.madid) {
        userData.madid = modelUserData.madid;
    }

    return userData;
}

// Event Data
const event = {};

if (data.testEvent) {
  event.test_event = true;
}

if (eventModel.event_id) {
    event.event_id = eventModel.event_id;
}

event.event_time = eventModel.event_time ? eventModel.event_time : DEFAULT_EVENT_TIME;

event.event_name = getYelpEventName(eventModel.event_name);

event.action_source = eventModel.action_source ? eventModel.action_source : DEFAULT_ACTION_SOURCE;

event.user_data = eventModel.user_data ? setUserData(eventModel.user_data) : {};

event.user_data.client_ip_address = eventModel.ip_override;
event.user_data.client_user_agent = eventModel.user_agent;

event.custom_data = {};

if (eventModel.value) {
    let value = eventModel.value;
    if (getType(value) == 'string') {
        value = makeNumber(value);
    }
    event.custom_data.value = value;
}

if (eventModel.currency) {
    event.custom_data.currency = eventModel.currency;
}

if (eventModel.transaction_id) {
    event.custom_data.order_id = eventModel.transaction_id;
}

if (eventModel.items) {
    const items = eventModel.items;
    event.custom_data.content_ids = getContentIdsFromItems(items);

    if (items.item_category) event.custom_data.content_category = items.item_category;

    event.custom_data.contents = getContentFromItems(items);
}

function sendEvent(capiEvent) {
    const eventRequest = { event: capiEvent };

    const apiEndpoint = 'https://api.yelp.com/v3/conversion/event';

    const requestHeaders = {
        headers: {
            'content-type': 'application/json',
            'Authorization': 'Bearer ' + data.apiAccessToken
        },
        method: 'POST'
    };

    return sendHttpRequest(
        apiEndpoint,
        (statusCode, headers, response) => {
            if (statusCode >= 200 && statusCode < 300) {
                data.gtmOnSuccess();
            } else {
                data.gtmOnFailure();
            }
        },
        requestHeaders,
        JSON.stringify(eventRequest)
    );
}

sendEvent(event);


___SERVER_PERMISSIONS___

[
  {
    "instance": {
      "key": {
        "publicId": "read_event_data",
        "versionId": "1"
      },
      "param": [
        {
          "key": "eventDataAccess",
          "value": {
            "type": 1,
            "string": "any"
          }
        }
      ]
    },
    "clientAnnotations": {
      "isEditedByUser": true
    },
    "isRequired": true
  },
  {
    "instance": {
      "key": {
        "publicId": "send_http",
        "versionId": "1"
      },
      "param": [
        {
          "key": "allowedUrls",
          "value": {
            "type": 1,
            "string": "specific"
          }
        },
        {
          "key": "urls",
          "value": {
            "type": 2,
            "listItem": [
              {
                "type": 1,
                "string": "https://api.yelp.com/*/conversion/event"
              }
            ]
          }
        }
      ]
    },
    "clientAnnotations": {
      "isEditedByUser": true
    },
    "isRequired": true
  }
]


___TESTS___

scenarios:
- name: On Event calls conversion API successfully
  code: |-
    runCode(testConfig);

    // Verify that the tag finished successfully.
    assertApi('sendHttpRequest').wasCalledWith(apiEndpoint, actualSuccessCallback, requestHeaders, JSON.stringify(eventRequest));

    assertApi('gtmOnSuccess').wasCalled();
- name: Minimal Event still fires
  code: |-
    const genericEventName = 'generic_event';
    const mockTime = 1000;

    const minimalEvent = {
      event_name: genericEventName,
      ip_override: testEvent.user_data.ip_address,
      user_agent: testEvent.user_data.user_agent,
    };

    const expectedMinimal = {
      event_time: 1,
      event_name: 'custom_' + genericEventName,
      action_source: 'website',
      user_data: {
        client_ip_address: testEvent.user_data.ip_address,
        client_user_agent: testEvent.user_data.user_agent,
      },
      custom_data: {},
    };

    const minimalEventRequest = { event: expectedMinimal };

    mock('getTimestampMillis', () => {
      return mockTime;
    });

    mock('getAllEventData', () => {
      return minimalEvent;
    });

    // Call runCode to run the template's code.
    runCode(testConfig);

    // Verify that the tag finished successfully.
    assertApi('sendHttpRequest').wasCalledWith(apiEndpoint, actualSuccessCallback, requestHeaders, JSON.stringify(minimalEventRequest));

    assertApi('gtmOnSuccess').wasCalled();
- name: Pre-Hashed Fields
  code: |-
    // Setting the email to the hashed version of itself should
    // just pass it through.
    const hashedEmail = hashFunction(testEvent.user_data.email);
    testEventModel.user_data.email_address = hashedEmail;

    runCode(testConfig);

    // Verify that the tag finished successfully.
    assertApi('sendHttpRequest').wasCalledWith(apiEndpoint, actualSuccessCallback, requestHeaders, JSON.stringify(eventRequest));

    assertApi('gtmOnSuccess').wasCalled();
- name: Sha256 fields
  code: |-
    // Hash the emails first
    const hashedEmail = hashFunction(testEvent.user_data.email);
    const hashedPhone = hashFunction(testEvent.user_data.phone);
    const hashedFirst = hashFunction(testEvent.user_data.first_name);
    const hashedLast = hashFunction(testEvent.user_data.last_name);

    // Then set the sha256 versions of those fields with the hashed email
    testEventModel.user_data.sha256_email_address = hashedEmail;
    testEventModel.user_data.sha256_phone_number = hashedPhone;
    testEventModel.user_data.address.sha256_first_name = hashedFirst;
    testEventModel.user_data.address.sha256_last_name = hashedLast;

    runCode(testConfig);

    // Verify that the tag finished successfully.
    assertApi('sendHttpRequest').wasCalledWith(apiEndpoint, actualSuccessCallback, requestHeaders, JSON.stringify(eventRequest));

    assertApi('gtmOnSuccess').wasCalled();
- name: Check user data for empty string
  code: |-
    // Set the test event to empty
    testEventModel.user_data.email_address = '';
    testEventModel.user_data.phone_number = undefined;

    // Update expected to be not defined
    Object.delete(expectedEvent.user_data, 'em');
    Object.delete(expectedEvent.user_data, 'ph');

    runCode(testConfig);

    // Verify that the tag finished successfully.
    assertApi('sendHttpRequest').wasCalledWith(apiEndpoint, actualSuccessCallback, requestHeaders, JSON.stringify(eventRequest));

    assertApi('gtmOnSuccess').wasCalled();
- name: Malformed json in fields
  code: |-
    const fakeObject = {
      foo: 'bar',
      bar: 'foo'
    };

    const stringFakeObject = JSON.stringify(fakeObject);
    const halfObject = stringFakeObject.slice(0,stringFakeObject.length/2);

    testEventModel.user_agent = halfObject;

    expectedEvent.user_data.client_user_agent = halfObject;

    runCode(testConfig);

    const stringifiedEvent = JSON.stringify(eventRequest);

    // Verify that the tag finished successfully.
    assertApi('sendHttpRequest').wasCalledWith(apiEndpoint, actualSuccessCallback, requestHeaders, stringifiedEvent);

    assertApi('gtmOnSuccess').wasCalled();

    // And parsing it doesn't error
    const output = JSON.parse(stringifiedEvent);
- name: Handle String Value
  code: |-
    // Value is a string
    testEventModel.value = '10';

    runCode(testConfig);

    // Verify that the tag finished successfully.
    assertApi('sendHttpRequest').wasCalledWith(apiEndpoint, actualSuccessCallback, requestHeaders, JSON.stringify(eventRequest));

    assertApi('gtmOnSuccess').wasCalled();
- name: Alternate EM and PH fields
  code: |-
    // Set em and ph
    testEventModel.user_data.em = testEventModel.user_data.email_address;
    testEventModel.user_data.ph = testEventModel.user_data.phone_number;

    // Update expected to be not defined
    Object.delete(testEventModel.user_data, 'email_address');
    Object.delete(testEventModel.user_data, 'phone_number');

    runCode(testConfig);

    // Verify that the tag finished successfully.
    assertApi('sendHttpRequest').wasCalledWith(apiEndpoint, actualSuccessCallback, requestHeaders, JSON.stringify(eventRequest));

    assertApi('gtmOnSuccess').wasCalled();
setup: |-
  // Imports
  const sha256Sync = require('sha256Sync');
  const JSON = require('JSON');
  const Object = require('Object');

  // Helper Functions
  function hashFunction(input, removeSpaces) {
    let formattedInput = input;

    if (removeSpaces) {
      formattedInput = input.replace(' ', '');
    }

    return sha256Sync(formattedInput.trim().toLowerCase(), { outputEncoding: 'hex' });
  }

  function normalizeAndHashGender(inputGender) {
    return hashFunction(inputGender.charAt(0));
  }

  // Event Data

  const testEvent = {
    event_id: 'event_id_1',
    event_time: 123456789,
    event_name: "purchase",
    action_source: "web",
    user_data: {
      email: "[email protected]",
      first_name: "first",
      last_name: "last",
      phone: "11231234567",
      date_of_birth: "20010414",
      gender: "n",
      country: "us",
      state: "ca",
      zip: "94105",
      city: "san francisco",
      external_id: "user_42",
      madid: "madid_user_42",
      ip_address: '192.168.1.0',
      user_agent: 'Browser/1.0 (OS 1.0; x64)'
    },
    custom_data: {
      value: 10,
      currency: 'usd',
      order_id: 'order_id_1',
      content_ids: ['id_1', 'id_2'],
      contents: [
        {
          id: 'id_1',
          item_price: 2,
          quantity: 3
        },
        {
          id: 'id_2',
          item_price: 4,
          quantity: 1
        }
      ]
    }
  };

  const testEventModel = {
    event_id: testEvent.event_id,
    event_time: testEvent.event_time,
    event_name: testEvent.event_name,
    action_source: testEvent.action_source,
    user_data: {
      email_address: testEvent.user_data.email,
      phone_number: testEvent.user_data.phone,
      gender: testEvent.user_data.gender,
      date_of_birth: testEvent.user_data.date_of_birth,
      address: {
        first_name: testEvent.user_data.first_name,
        last_name: testEvent.user_data.last_name,
        country: testEvent.user_data.country,
        region: testEvent.user_data.state,
        postal_code: testEvent.user_data.zip,
        city: testEvent.user_data.city
      },
      external_id: testEvent.user_data.external_id,
      madid: testEvent.user_data.madid
    },
    ip_override: testEvent.user_data.ip_address,
    user_agent: testEvent.user_data.user_agent,
    value: testEvent.custom_data.value,
    currency: testEvent.custom_data.currency,
    transaction_id: testEvent.custom_data.order_id,
    items: [
      {
        item_id: testEvent.custom_data.contents[0].id,
        item_name: testEvent.custom_data.contents[0].id,
        price: testEvent.custom_data.contents[0].item_price,
        quantity: testEvent.custom_data.contents[0].quantity,
      },
      {
        item_id: testEvent.custom_data.contents[1].id,
        item_name: testEvent.custom_data.contents[1].id,
        price: testEvent.custom_data.contents[1].item_price,
        quantity: testEvent.custom_data.contents[1].quantity,
      }
    ]
  };

  const expectedEvent = {
    event_id: testEvent.event_id,
    event_time: testEvent.event_time,
    event_name: testEvent.event_name,
    action_source: testEvent.action_source,
    user_data: {
      em: [hashFunction(testEvent.user_data.email)],
      ph: [hashFunction(testEvent.user_data.phone)],
      ge: normalizeAndHashGender(testEvent.user_data.gender),
      db: hashFunction(testEvent.user_data.date_of_birth),
      fn: hashFunction(testEvent.user_data.first_name),
      ln: hashFunction(testEvent.user_data.last_name),
      country: [hashFunction(testEvent.user_data.country)],
      st: [hashFunction(testEvent.user_data.state)],
      zp: [hashFunction(testEvent.user_data.zip, true)],
      ct: [hashFunction(testEvent.user_data.city, true)],
      external_id: [hashFunction(testEvent.user_data.external_id)],
      madid: testEvent.user_data.madid,
      client_ip_address: testEvent.user_data.ip_address,
      client_user_agent: testEvent.user_data.user_agent,
    },
    custom_data: {
      value: testEvent.custom_data.value,
      currency: testEvent.custom_data.currency,
      order_id: testEvent.custom_data.order_id,
      content_ids: testEvent.custom_data.content_ids,
      contents: testEvent.custom_data.contents,
    }
  };

  // Mocks

  mock('getAllEventData', () => {
    return testEventModel;
  });

  const apiEndpoint = 'https://api.yelp.com/v3/conversion/event';
  const eventRequest = { "event": expectedEvent };
  const testAPIAccessToken = 'testAPIAccessToken';
  const requestHeaders = {
    headers: {
      'content-type': 'application/json',
      'Authorization': 'Bearer ' + testAPIAccessToken
    },
    method: 'POST'
  };

  let actualSuccessCallback, httpBody;
  mock('sendHttpRequest', (postUrl, response, options, body) => {
    actualSuccessCallback = response;
    httpBody = body;
    actualSuccessCallback(200, {}, '');
  });

  const testConfig = {
    apiAccessToken: testAPIAccessToken
  };


___NOTES___

Created on 8/2/2024, 4:12:43 PM