View Categories

Custom Website

This guide integrates your shop with Releva using our JavaScript library and a few server-side calls. It is written for shops on a custom stack — if you run Shopify, WooCommerce, Magento, PrestaShop, OpenCart or CS-Cart, install the plugin for your platform instead and skip this entirely.

The work splits into three independent parts. They are usually done by different people, and only the first two are needed for Releva to start producing results:

Part What it is Where it runs
A. Front-end tracking One Releva.push() call, with a few extra fields on certain page types. This is what tells Releva what each visitor is doing. Your storefront, in the browser
B. Catalog & order sync Three or four HTTP calls that keep Releva’s copy of your catalogue current and report paid orders. Your backend
C. Consent & lifecycle Consent signalling, subscribe / unsubscribe, new registrations. Conditional — you implement the ones that apply to you. Both

The minimum viable integration #

If you do nothing else, do these four things. They are what the recommendation and personalization algorithms actually need, and together they are a day of work rather than a project:

  1. The base Releva.push() call on every page (Part A) — who the visitor is and what is in their cart.
  2. Product context on product pages (Part A) — two extra fields on the same call. This is the behavioural signal everything else is built on.
  3. The Product Create / Update API (Part B) — without a catalogue there is nothing to recommend.
  4. The Cart Paid API (Part B) — without orders there is no revenue, no attribution and no purchase history.

Everything after that — listing and search context, banners on more page types, subscriptions, the fallback sync — improves results but is not required to go live. One exception: if you operate where GDPR or CCPA applies, the consent function in Part C is a legal requirement, not an enhancement.

Before you begin: your credentials #

Three values appear in this guide. Two of them live on the same screen; the third is per-page. Getting these straight up front saves the most common support ticket we receive.

Value Where to find it Used by Secret?
<yourAccessToken> Settings → General → Access Token (with a copy button) Part A only — the Releva.push() call in the browser No. It ships in your page source; that is by design.
<secretKey> Settings → General → Secret Key (just below the access token) Part B and the server-side half of Part C, as Authorization: Bearer <secretKey> Yes. Never put it in front-end code.
<yourPageToken> The Pages section — each page you create there has its own token, with a copy button Part A, in page.token No.

A page token identifies a kind of page (home, product, category, search, cart), not an individual product. Create one page in Releva per page type you want personalized content on, and reuse its token across every URL of that type. A push with no token is accepted and still records behaviour — you simply get no banners or recommenders back for that page.

Part A — Front-end tracking #

There is one call to learn. You make Releva.push() on every page with a context object describing what the visitor is looking at, and Releva answers with the personalized content configured for that page. On specific page types you add two or three fields to that same object — you are not writing a new integration per page type.

On this page type Add to the context
Every page cart, profile (and wishlist, if you have one)
Product page page.token, product.id
Category / listing page.token, page.ids, page.categories, optionally page.filter
Search results page.token, page.query, page.ids, optionally page.filter
Checkout success cart.products = the lines of the order just placed, plus profile.email / profile.phoneNumber

A1. The base call, on every page #

Add this towards the end of the body element. Replace every placeholder with real values — and send an empty products array when the cart is empty, rather than omitting the cart.

<script>
document.addEventListener('relevaSdkLoaded', function () {
  Releva.push('<yourAccessToken>', {
    cart: {
      products: [
        {
          id: 'productId1',
          price: 12.5,
          currency: 'USD',
          quantity: 2
        }
      ]
    },
    profile: {
      id: '<yourInternalCustomerId>'
    }
  }, function (results) {
    // Called with the personalized content for this page.
  }, function (error) {
    console.error(error);
  });
});
</script>
<script async type="text/javascript"
        src="https://releva.ai/sdk/v0/js/releva-sdk-js.min.js"
        onload="document.dispatchEvent(new Event('relevaSdkLoaded'));"></script>

The onload attribute and the relevaSdkLoaded listener exist only to make sure Releva.push runs after the library has loaded. Any other way of guaranteeing that order is fine.

Send profile only when you know who the visitor is. An anonymous visitor is tracked perfectly well without it, and Releva merges their history into their profile the moment they log in.

A2. Product pages #

Same call, two more fields:

{
  "page": {
    "token": "<yourProductPageToken>",
    "categories": ["men/shoes/sport"]
  },
  "product": {
    "id": "productId1"
  }
}

A3. Category and listing pages #

Tell Releva which products the visitor is actually looking at, and which filters they applied:

{
  "page": {
    "token": "<yourListingPageToken>",
    "ids": ["productId1", "productId2", "productId3"],
    "categories": ["men/shoes/sport"],
    "filter": {
      "operator": "and",
      "nested": [
        { "key": "price", "operator": "gte", "value": "10", "action": "include" },
        { "key": "price", "operator": "lte", "value": "50", "action": "include" },
        {
          "operator": "or",
          "nested": [
            { "key": "custom.string.size", "operator": "eq", "value": "42", "action": "include" },
            { "key": "custom.string.size", "operator": "eq", "value": "43", "action": "include" }
          ]
        }
      ]
    }
  }
}

A4. Search results #

Add the query the visitor typed and the ids it returned. Sending query also records a search event.

{
  "page": {
    "token": "<yourSearchPageToken>",
    "query": "running shoes",
    "ids": ["productId1", "productId2", "productId3"]
  }
}

To record a search that was driven purely by filters with no typed text, send "query": null together with the filter object. The locale you send should describe both the page and the query; in the rare case where they differ, put the query locale in a custom field.

A5. The checkout success page #

Here cart.products changes meaning: send the lines of the order that was just placed, not what is still in the basket. Include an email address or a phone number even for guest checkouts — without one the order cannot be attached to a person.

{
  "cart": {
    "products": [
      {
        "id": "productId1",
        "price": 12.5,
        "currency": "USD",
        "quantity": 2
      }
    ]
  },
  "profile": {
    "email": "customer@example.com",
    "phoneNumber": "+15857661234"
  }
}

Phone numbers must be in international format — 585-766-1234 in a US shop becomes +15857661234. If you cannot convert a number reliably, leave it out.

This front-end call is a convenience, not the record of the sale. The authoritative one is the Cart Paid API in Part B, which your backend calls with the same order. Both are deduplicated on orderId, so sending both is correct and expected.

How to confirm Part A works #

  1. Load a page of your shop with DevTools open, on the Network tab, filtered to push.
  2. You should see one POST to https://releva.ai/api/v0/push/<yourAccessToken> returning 200.
  3. Open its response. A working call returns a JSON object containing a userId — that is Releva’s id for this visitor. Once you have configured content for the page, the same response carries the recommenders and banners for it.
  4. Check the request payload: the context object is exactly what you sent. If a field you expected is missing here, it never reached us.

A 4xx response names the offending field in its message. The most common causes are a mistyped access token and a price sent as a string ("12.50") where a number is expected.

Custom fields, end to end #

Custom fields are how your own product attributes — size, colour, brand, material — reach Releva, and they are what filtering, segmentation and recommender rules operate on. They are worth ten minutes now because everything downstream depends on their shape.

There are exactly three types (string, numeric, date), and every field is written as a key with an array of values. Here is one field — a shoe size — followed all the way through.

1. You send it with the product (Part B, Product Create / Update):

{
  "id": "productId1",
  "name": "Trail Runner",
  "listPrice": 120,
  "categories": ["men/shoes/sport"],
  "available": true,
  "custom": {
    "string": [
      { "key": "size", "values": ["42", "43", "44"] },
      { "key": "color", "values": ["red"] }
    ],
    "numeric": [
      { "key": "weightGrams", "values": [310] }
    ]
  }
}

2. You send the visitor’s selection with the cart (Part A) — the same shape, describing what this shopper chose:

{
  "id": "productId1",
  "price": 120,
  "currency": "USD",
  "quantity": 1,
  "custom": {
    "string": [
      { "key": "size", "values": ["42"] }
    ]
  }
}

3. You filter on it by joining the type and the key with dots:

{ "key": "custom.string.size", "operator": "eq", "value": "42", "action": "include" }
You wrote You filter on
custom.string entry with "key": "size" custom.string.size
custom.numeric entry with "key": "weightGrams" custom.numeric.weightGrams
custom.date entry with "key": "inPromoUntil" custom.date.inPromoUntil

The rule is custom.<type>.<yourKey>. Standard fields such as price, available and categories are filtered on by their own name, with no prefix.

Two things worth getting right the first time: keep a key in the same type forever (a size sent as string on some products and numeric on others produces two unrelated fields), and use the same spelling everywhere — size and Size are different fields.

Part B — Catalog & order sync #

These run on your server and authenticate with Authorization: Bearer <secretKey>. They are ordinary JSON over HTTPS; each one answers 202 with an empty body when accepted.

Call When you call it Needed?
Product Create / Update Whenever a product changes — price, stock, name, anything. This is how Releva learns your catalogue. Required
Cart Paid When an order is paid for. Required
Periodical Product Sync An endpoint you build, which Releva calls on a schedule to walk your whole catalogue. It repairs anything a missed update left stale. Strongly recommended
Refund When an order is refunded, so revenue and attribution are corrected. If you take refunds
Product Delete When a product is permanently gone. For a product that is merely out of stock, send available: false instead. Optional

Start with Product Create / Update and Cart Paid. The product ids you send here must be the same ids you send in product.id and cart.products[].id in Part A — that is what ties browsing behaviour to the catalogue. If you sell in several languages or currencies, the id has to be unique per locale; the conventions are on the Product Create / Update page.

How to confirm Part B works #

  1. Send one product and check for 202 with a body of {}. A 400 names the field at fault in its message; a 402 means the shop is disabled.
  2. Find that product in the Products section of the admin panel — it appears within about ten seconds — and confirm the price, availability and custom fields look right.
  3. Place a test order and confirm it appears in your revenue reporting. Orders are held for about thirty seconds before processing, so allow a minute.

Part C — Consent, subscriptions & lifecycle #

Implement the parts that apply to you.

Consent (required under GDPR / CCPA) #

If you operate where data protection law applies, pass a hasConsent function as the last argument to Releva.push. It must return true only when the visitor has accepted behavioural tracking for first-party marketing and analytics:

Releva.push('<yourAccessToken>', { /* context */ },
  function (results) { /* ... */ },
  function (error) { /* ... */ },
  {
    hasConsent: function () {
      // Example: a cookie banner that sets `cookielaw=1` on acceptance
      return Releva.getCookie('cookielaw') === '1';
    }
  });

Subscriptions #

  • Call the Unsubscribe API when someone opts out of marketing messages, and the Subscribe API when they opt in. Skip this entirely if you do not use Releva for email or text messages.
  • For a sign-up form on the storefront itself, the front-end subscribe event is simpler than the server-side call.

New registrations #

Call the Profile Register API when a user creates an account, so the welcome journey can start and the profile carries its registration date.

Web push notifications #

Skip this unless you plan to send web push. Download service-worker.min.js, save it at the root of your site, and point the SDK at it:

Releva.push('<yourAccessToken>', { /* context */ },
  function (results) { /* ... */ },
  function (error) { /* ... */ },
  {
    serviceWorkerUrl: '/service-worker.min.js',
    hasConsent: function () {
      return Releva.getCookie('cookielaw') === '1';
    }
  });

The file must be served from the root of your domain. You may rename it, as long as serviceWorkerUrl matches.

A complete example #

If you would rather assemble from one working file than from the sections above, this is every page type in a single snippet. Build the context for the page you are rendering and pass it to one call.

<script>
document.addEventListener('relevaSdkLoaded', function () {
  // 1. Always: who the visitor is and what is in their cart.
  var context = {
    cart: {
      products: window.myShop.cartLines.map(function (line) {
        return {
          id: line.productId,
          price: line.unitPrice,
          currency: window.myShop.currency,
          quantity: line.quantity,
          custom: {
            string: [{ key: 'size', values: [line.size] }]
          }
        };
      })
    }
  };

  if (window.myShop.customer) {
    context.profile = {
      id: window.myShop.customer.id,
      email: window.myShop.customer.email
    };
  }

  // 2. Page-specific fields.
  switch (window.myShop.pageType) {
    case 'product':
      context.page = { token: '<yourProductPageToken>', categories: window.myShop.categories };
      context.product = { id: window.myShop.productId };
      break;

    case 'listing':
      context.page = {
        token: '<yourListingPageToken>',
        ids: window.myShop.visibleProductIds,
        categories: window.myShop.categories
      };
      break;

    case 'search':
      context.page = {
        token: '<yourSearchPageToken>',
        query: window.myShop.searchQuery,
        ids: window.myShop.visibleProductIds
      };
      break;

    case 'checkoutSuccess':
      // cart.products is the ORDER, not the basket - set upstream.
      context.profile = context.profile || {};
      context.profile.email = window.myShop.order.email;
      break;
  }

  Releva.push('<yourAccessToken>', context,
    function (results) {
      // Render recommenders and banners.
    },
    function (error) {
      console.error(error);
    },
    {
      hasConsent: function () {
        return Releva.getCookie('cookielaw') === '1';
      }
    });
});
</script>
<script async type="text/javascript"
        src="https://releva.ai/sdk/v0/js/releva-sdk-js.min.js"
        onload="document.dispatchEvent(new Event('relevaSdkLoaded'));"></script>

Full field reference #

Everything the context object accepts. You will not need most of it on day one.

context.cart — the visitor’s basket, or the order on a checkout success page
Field Type Description
products Array[Object] The products in the cart. Send an empty array when the cart is emptied.
products[].id String The product id, identical to context.product.id for the same product and to the id you send to the Product Create / Update API. Must be unique across locales in a multi-language shop.
products[].price Float The unit price paid, as a number.
products[].currency ISO-4217 String (Optional) Required if you sell in more than one currency.
products[].quantity Integer Units in the cart.
products[].custom Object (Optional) What the shopper chose — size, colour. See Custom fields, end to end.
context.profile — the logged-in visitor
Field Type Description
id String Your own id for this customer. Use the same one everywhere.
custom Object (Optional) Custom profile fields. Replaces the stored set — send the full list.
addCustom Object (Optional) Custom fields to add without overwriting the others.
location Object (Optional) { "lat": 39.9352959, "lon": 23.5825895 } in decimal notation.
context.page — the page being viewed
Field Type Description
token String (Optional) The page’s token from the Pages section. Omit it and you still get tracking, but no personalized content for this page.
locale ISO 639-1 String The page language, e.g. en, bg.
currency ISO-4217 String (Optional) Required if you sell in more than one currency.
ids Array[String] On a listing or search page, the product ids currently shown. Omit elsewhere.
categories Array[String] The categories currently selected.
query String (Optional) The search text. Sending it also records a search event; null records a filter-only search.
custom Object (Optional) Custom-field filters applied to the listing.
filter Object (Optional) The filter applied to the listing — see below.
blocks.tags Array[String] (Optional) Render only product and banner blocks carrying at least one of these tags.
context.product, context.wishlist and context.company
Field Type Description
product.id String The product being viewed. Set on product pages only.
product.custom Object (Optional) The currently selected options, e.g. the chosen size.
wishlist.products[].id String A product in the visitor’s wishlist.
wishlist.products[].custom Object (Optional) The chosen configuration for it.
company.id String (Optional) The company the visitor is acting for. B2B only.
context.page.filter — filter syntax

A filter is either a group (operator plus nested) or a condition. Groups nest to any depth.

Field Type Description
operator String On a group: and (every child must match) or or (any child).
nested Array The child conditions or groups.
key String On a condition: a standard field such as price, or a custom field written custom.string.brand.
operator String eq, lt, gt, lte, gte, or a range pair — gte,lte, gte,lt, gt,lte, gt,lt.
value String The value to compare against. For a range operator, two values separated by a comma.
action String include, exclude, boost (to the top) or bury (to the bottom).
weight Integer (Optional) Orders several boost or bury rules against each other.

Are you done? #

  1. Every page of the shop fires one POST to /api/v0/push/<yourAccessToken> that returns 200.
  2. A product page sends product.id, and the id matches one in your Releva catalogue.
  3. Adding to and removing from the cart changes cart.products on the next push, and emptying it sends an empty array.
  4. A test order appears in your revenue reporting, from the Cart Paid call.
  5. Your catalogue in the admin panel matches your shop — prices, availability, custom fields.
  6. If GDPR or CCPA applies to you, declining the cookie banner makes hasConsent return false.

All six passing means the algorithms have what they need. Everything else is refinement.

All set! #

Congratulations — this concludes the integration of your shop. From here, the Custom Event API lets you report anything specific to your business that the standard calls do not cover, and it is the usual next step once the basics are flowing.