Skip to content

Ecommerce Products

The Topol editor lets your users place products from your store directly into their emails - either specific products they select manually while designing the template, or a dynamic list that fills in at send time, such as an abandoned cart, a "last viewed" rail, or personalized recommendations.

This is handled by the Product block: a built-in block that renders a product card (image, title, description, price, optional discounted price, and a button) and can be repeated as a grid or a list.

How It Works

The Product block can work in two ways:

  • Static – the user picks specific products from a feed, and those exact products are saved into the template.
  • Dynamic – the block is connected to a Merge Tag (e.g. a shopping cart) and the ESP fills in the actual products at send time, looping over the array you pass in the payload.

To make products available in the editor, you'll need to expose two API endpoints, plus an optional third one for categories:

  • /{API.FEEDS} – returns the list of feeds.
  • /{API.PRODUCTS} – returns products for a specific feed.
  • /{API.PRODUCT_CATEGORIES} – optional. Lets users filter products by category inside a feed.

All endpoints should return structured JSON and support pagination and filtering.

WARNING

Before implementing the endpoints, check how to work with API endpoints.

List Feeds

Fetches all available product feeds for the user to select from.

  • URL: /{API.FEEDS}
  • Method: GET
  • Content-Type: application-json

Params:

keyvalue
idFeed ID
searchText search across feed names
per_pageNumber of feeds per page (default: 10)
current_pageWhich page of paginated data to return

Response:

json
{
  "success": true,
  "data": [
    {
      "id": "id of the feed",
      "name": "name of the feed"
    }
  ],
  // Pagination helpers
  "from": "first id of the resource",
  "to": "last id of the resource",
  "total_records": "total records of the resource",
  "per_page": "resource per page",
  "current_page": "current page of the resource",
  "last_page": "last page of the resource"
}

List Products

Returns a paginated list of products for a specific feed. Triggered when a feed is selected in the editor UI.

  • URL: /{API.PRODUCTS}
  • Method: GET
  • Content-Type: application-json

Params:

keyvalue
idProduct ID
searchSearch string to filter products
per_pageNumber of products per page (default: 10)
current_pageCurrent page of paginated products
feedThe ID of the selected feed
category_idOptional category to filter products by

Response:

json
{
  "success": true,
  "data": [
    {
      "id": "product id",
      "name": "name of the product",
      "description": "description of the product",
      "url": "link to the product",
      "img_url": "link to an image of the product",
      "price_with_vat": "price of the product including VAT", // Without currency
      "currency": "currency of the price",
      "price_before": "original price before discount",
      "product_feed_id": "id of the feed product belongs to"
    }
  ],
  // Pagination helpers
  "from": "first id of the resource",
  "to": "last id of the resource",
  "total_records": "total records of the resource",
  "per_page": "resource per page",
  "current_page": "current page of the resource",
  "last_page": "last page of the resource"
}

List Product Categories

Optional. Returns the categories for a specific feed, letting users narrow products by category inside the selection modal. If this endpoint is not configured, the category dropdown is hidden and users filter by feed only.

  • URL: /{API.PRODUCT_CATEGORIES}
  • Method: GET
  • Content-Type: application-json

Params:

keyvalue
feed_idThe ID of the selected feed (categories are scoped to a feed)
searchText search across category names
per_pageNumber of categories per page (default: 10)
current_pageWhich page of paginated data to return

Response:

json
{
  "success": true,
  "data": [
    {
      "id": "category id",
      "name": "category name",
      "feed_id": "id of the feed this category belongs to"
    }
  ],
  // Pagination helpers
  "from": "first id of the resource",
  "to": "last id of the resource",
  "total_records": "total records of the resource",
  "per_page": "resource per page",
  "current_page": "current page of the resource",
  "last_page": "last page of the resource"
}

Layout Options

The Product block supports a few layout variants, so the same data can be presented as a single column, a side-by-side image and text composition, or a grid.

Loop Block
  • Layoutcolumn, image-text, or text-image. Decides whether the image sits above the text, to its left, or to its right.
  • Items per row1 to 4. How many products sit next to each other in a row.
  • Stack columns on mobile – Decides whether products in a row stack vertically on mobile or stay side-by-side.

INFO

In dynamic mode, items per row acts as a maximum rather than a fixed number. The renderer can shrink the row when fewer products come through in the payload, but it needs a small bit of help from you to do that - see Dynamic Mode below.

Static Mode

Static mode is the default. The user opens the Product block, picks a feed (and a category, if you've enabled them), then chooses the products they want from the modal. Those products are saved into the block's items array, so the template stands on its own without any Merge Tags or ESP-side rendering.

Loop Block

TIP

Use static mode when the products are hand-picked per template, for example a weekly highlight, a featured collection, or a one-off promo where the lineup shouldn't change between sends.

Dynamic Mode

In dynamic mode, the user doesn't pick individual products. Instead, they connect the block to a Merge Tag, such as a "Shopping cart" or "Last viewed" list, and the ESP fills in the real products at send time, looping over the array you pass in the payload.

Loop Block

To enable dynamic mode, three options need to be set in TOPOL_OPTIONS:

ts
emailServiceProvider: "sparkpost",
productMergetagTags: [
  { label: "Shopping cart", value: "MT_FOR_ARRAY" },
],
smartMergeTags: {
  enabled: true,
  syntax: {
    start: "{{",
    end: "}}",
  },
},
  • emailServiceProvider: "sparkpost" – dynamic mode relies on SparkPost-compatible loop syntax.
  • productMergetagTags – the Merge Tags users can pick from in the block's dynamic dropdown. Each entry becomes one option, where label is what the user sees and value is what the renderer uses internally.
  • smartMergeTags – must be enabled with double curly bracket syntax. No other syntax is supported in dynamic mode.

One important detail: alongside the Merge Tag for the array (e.g. MT_FOR_ARRAY), your SparkPost payload must also include its length as a separate variable named exactly MT_FOR_ARRAY_count. The renderer uses this value to pick the right column width for the actual number of products in the array.

If _count is missing, the block still renders correctly, but every row will use the maximum items-per-row width and it won't expand to fill the row when fewer products are passed.