View Categories

Product Sync (full)

9 min read

The product Sync API is an API that you need to develop in your shop so that Releva can periodically sync its internal catalogue with your shop catalogue.

Do I need to develop it? #

This is basically a catch-all mechanism which will ensure that any changes to your catalogue are reflected in Releva even if your tracking integration does not work as expected and you miss calling the product update API for some of your catalog changes. Although it’s not required for the functioning of Releva, we highly recommend that you implement it to limit situations where Releva’s version of your catalogue is stale.

How the sync works #

Releva calls your endpoint on a schedule configured for your shop โ€” typically once a day, overnight. It walks your catalogue one page at a time until your endpoint says there are no more pages, writes every product it received, and then removes from its own catalogue every product it did not see during the pass.

Your endpoint must return your COMPLETE catalogue. About two minutes after a successful pass, any product that was not part of it is deleted from Releva โ€” including products you previously sent through the Product Create / Update API. Do not filter the response down to in-stock, visible or recently-changed products: send everything, and use available: false for what cannot be bought right now.

Nothing is deleted when a pass does not complete. If your endpoint starts failing half-way through, Releva marks the run failed, resumes it from the last page it successfully read, and runs the cleanup only once a full pass has succeeded.

The default endpoint is https://your-awesome-shop.com/api/releva/products; a different URL can be configured for your shop, and any query parameters it already carries are preserved.

Request Format #

Releva will perform the following curl equivalent:

curl -H 'authorization: Bearer <secretKey>' -H 'x-rlv-authorization: Bearer <secretKey>' -H 'Content-Type: application/json' -H 'user-agent: Releva/0.0.1' -XGET 'https://your-awesome-shop.com/api/releva/products?page=1&cursor=null'

Note that we send both the standard authorization header and the non-standard x-rlv-authorization header and they contain the same value. This is due to the fact that in a standard PHP and Apache setup, Apache does not forward the authorization headers to PHP unless specifically told to do so. This has proven to be a challenge for some of our customers.

Both query parameters are always present, whichever style of pagination you implement. If you paginate by page number, read page and ignore cursor; the first request of every pass is page=1&cursor=null, and page counts up from there. If you paginate by cursor, read cursor and ignore page (it stays at 1 for the whole pass) โ€” and treat the literal value null as โ€œstart from the beginningโ€.

Verify the secret key Your endpoint exposes your whole catalogue. Compare the bearer token against your shop’s Releva secret key and answer 401 when it does not match.
Respond within 2 minutes Each request times out after two minutes. Aim for 20โ€“50 products per page โ€” smaller pages that answer quickly sync faster overall than large pages that time out.
Be consistent within a pass Return the pages in a stable order. A catalogue that reorders itself between requests can skip or repeat products.

Response Format #

Releva will expect you to provide a JSON response with HTTP status 200 and the following structure. Your response should contain 20-50 products.

{
  "pageCount": 123,
  "products": [
    {
      "id": "12345_en",
      "name": "Hiking Shirt",
      "description": "A short description of the product.",
      "locale": "en",
      "currency": "BGN",
      "categories": [
        "Clothes/Shirts"
      ],
      "url": "https://your-awesome-shop.com/products/hiking-shirt",
      "imageUrl": "https://your-awesome-shop.com/img/hiking-shirt.jpg",
      "publishedAt": "2020-04-15T03:21:50+00:00",
      "sku": "HS-001",
      "stockQuantity": 12,
      "custom": {
        "string": [
          {
            "key": "Season",
            "values": [
              "Summer"
            ]
          },
          {
            "key": "Gender",
            "values": [
              "F"
            ]
          },
          {
            "key": "Size",
            "values": [
              "L"
            ]
          }
        ]
      },
      "available": true,
      "listPrice": 169,
      "discountPrice": 126.75,
      "inventory": [
        {
          "location": [
             {"lat": 39.9352959, "lon": 23.5825895}
          ]
        }
      ]
    }
  ]
}

pageCount is the total number of pages in your catalogue, and it must be present on every page, not only the first one. Releva keeps requesting page+1 until it has fetched pageCount pages. A response that carries neither pageCount nor nextCursor fails the sync, as does a response with no products array.

What if I don’t know the total number of pages or cannot fetch results for a given page? #

We also support cursor-based pagination as an alternative in case your backend does not support traditional pagination. In this case, Releva will perform the following request:

curl -H 'authorization: Bearer <secretKey>' -H 'x-rlv-authorization: Bearer <secretKey>' -H 'Content-Type: application/json' -XGET 'https://your-awesome-shop.com/api/releva/products?page=1&cursor=...'

You should then return a subset of products, and the nextCursor string which Releva will use to fetch the next batch. When there are no more results, please return a nextCursor with the value of null. Remember that the first request of a pass carries the literal value cursor=null โ€” that is your signal to start from the beginning of the catalogue.

{
  "nextCursor": "eyJsYXN0SWQiOjEyMzQ1fQ",
  "products": [
    {
      "id": "12345_en",
      "name": "Hiking Shirt",
      "description": "A short description of the product.",
      "locale": "en",
      "currency": "BGN",
      "categories": [
        "Clothes/Shirts"
      ],
      "url": "https://your-awesome-shop.com/products/hiking-shirt",
      "imageUrl": "https://your-awesome-shop.com/img/hiking-shirt.jpg",
      "publishedAt": "2020-04-15T03:21:50+00:00",
      "available": true,
      "listPrice": 169,
      "discountPrice": 126.75
    }
  ]
}

Do not send both pageCount and nextCursor โ€” when pageCount is present it wins, and your cursor is ignored.

The products Array #

Each element in this array has exactly the same shape as a product sent to the Product Create / Update API โ€” see that page for the full field reference, including multi-variant products (groupId / variants) and multi-market catalogues (marketHandle, markets, canonicalUrl).

Field Type Description
id String The product id. It should match the id that you pass as product.id and cart.products[].id in the frontend integration, as well as in carts[].products[].id in the backend integration. If your shop has multiple languages, the product id should be unique across locales.
name String The product name.
description String (Optional) A short description of the product.
locale ISO 639-1 String (Optional) If you sell in multiple languages or currencies, set this to the current locale. Two-letter language codes only (en, bg) โ€” en-US is not valid. If you omit this field, the shop default will be used.
currency ISO-4217 String (Optional) The currency of this product. If you omit this field, the shop default will be used.
data Object (Optional) Arbitrary non-searchable product data. Use this to pass through product information that you would like to visualize in recommender results.
listPrice Float The regular price of the product.
discountPrice Float (Optional) If the product is on sale, set the discount price here. If not โ€“ do not include this field. It must be lower than listPrice.
stockQuantity Integer (Optional) How many units are still available for sale.
sku String (Optional) The product SKU, used by on-site search and the advertising catalogs.
categories Array[String] Array of category paths where the product is accessible, e.g. ["men/shoes", "men/shoes/hikings", "sports/hiking/shoes"].
available Boolean True if the product is available for sale, false otherwise. Send out-of-stock products with false rather than leaving them out of the response.
url Absolute URL String (Optional) The product URL.
canonicalUrl Absolute URL String (Optional) The product URL without a language or market prefix, used as the base link in advertising feeds.
imageUrl Absolute URL String (Optional) The URL to the product’s image.
custom Object (Optional) The custom fields associated with the product. This is where shop-specific attributes such as brand, colour or material belong โ€” fields invented at the top level of the product object are discarded.
publishedAt ISO 8601 String (Optional) The date and time when this product was first available for sale. The time and the timezone offset are required, e.g. 2020-04-15T03:21:50+00:00.
marketHandle String (Optional) Your identifier for the market this product belongs to, when you sell the same catalogue across several markets.
markets Array[String] (Optional) The ISO-3166-1 alpha-2 country codes this product applies to, upper case, e.g. ["FR", "DE"].
groupId String (Optional) The id of the product this entry belongs to, shared by all its variants, languages and markets. Send it together with variants.
variants Array[Object] (Optional) Per-variant data (variantId, sku, listPrice, discountPrice, available, stockQuantity, imageUrl, barcode, optionValues). See the Product Create / Update API for the full reference.
inventory Array[Object] (Optional) Product inventory information.
inventory[].location Array[Object] (Optional) Inventory locations in decimal longitude, latitude notation.
inventory[].location[].lon Float Inventory location longitude, e.g. 23.5825895.
inventory[].location[].lat Float Inventory location latitude, e.g. 39.9352959.

What Releva does with your response #

Every product is checked against the same rules as the Product Create / Update API, with two differences that matter when you are building the endpoint:

  • Fields Releva does not recognise are silently removed rather than rejected. If you invent a top-level property it will not reach your recommenders or segments โ€” put shop-specific attributes in custom instead.
  • A product that is still invalid is dropped on its own, and the rest of the page is written normally. A product dropped on every pass never appears in Releva at all, so if products are missing, that is the first thing to check with us.

Because a dropped product is not โ€œseenโ€ by the pass, it is also removed by the cleanup afterwards. A systematically malformed field โ€” a price sent as a string, a publishedAt with no timezone offset, a locale such as en-US โ€” can therefore empty large parts of your Releva catalogue while your endpoint keeps answering 200.

When something goes wrong #

Your endpoint errors or times out Releva retries the same page up to 10 times with an increasing delay. If it still fails, the run is marked failed and resumes from the last page it read; no products are deleted.
Your endpoint returns 200 with a broken body A response with no products array, or with neither pageCount nor nextCursor, fails the run. This is the safest way to signal a problem โ€” never answer 200 with an empty catalogue, as a complete-looking empty pass deletes everything.
A sync is already running Only one pass per shop runs at a time; a second trigger is ignored while one is in progress.