Skip to content

Custom API Blocks

This feature is available with the Plugin for Business plan and higher.

If you're on the Plugin for Startup or Plugin Expansion plan, consider upgrading to access this feature. For more details, visit our pricing page or contact support.

A Custom API Block pulls categorized data from your own API and renders it as a styled block inside the template. Product lists, news items or any other grouped content can be placed by the user and restyled without leaving the editor.

Dynamic Api Block Example

Why use Custom API Blocks?

This feature lets you build dynamic email sections that automatically update from your data source without manual editing. For example, you can create product catalogs, event lists, or news sections that pull fresh data every time you use the template.

API endpoint structure

To use a Custom API Block, your API needs to provide two endpoints (feeds and items) and may optionally provide a third one for categories.

1. Feeds endpoint

This is a list of categories or groups. For example, if you're showing products, this could be product categories like "Shoes," "Hats," or "Accessories." The API will send back something like this:

Expected Response for the Feed Endpoint
json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Feed 1"
    },
    {
      "id": 2,
      "name": "Feed 2"
    },
    {
      "id": 3,
      "name": "Feed 3"
    },
    {
      "id": 4,
      "name": "Feed 4"
    }
  ],
  "total_records": 4
}

The editor calls it with per_page (10), current_page, search and id.

2. Items endpoint

For each category, this endpoint returns the detailed items to show. For example, the actual products within a category. The data might look like this:

Expected Response for the Items Endpoint
json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Block 1",
      "image": "https://storage.googleapis.com/topol-io-plugin-97de3577-4897-4379-9699-a51756301fab/plugin-assets/17326/user/warsaw.jpg",
      "href": "https://www.topol.io",
      "my-text-1": "My text 1 in block 1",
      "my-text-2": "My text 2 in block 1",
      "my-text-3": "My text 3 in block 1",
      "feed_id": 1
    },
    {
      "id": 2,
      "name": "Block 2",
      "image": "https://storage.googleapis.com/topol-io-plugin-97de3577-4897-4379-9699-a51756301fab/plugin-assets/17326/user/prague.jpg",
      "href": "https://www.topol.io",
      "my-text-1": "My text 1 in block 2",
      "my-text-2": "My text 2 in block 2",
      "my-text-3": "My text 3 in block 2",
      "feed_id": 1
    },
    {
      "id": 3,
      "name": "Block 3",
      "image": "https://storage.googleapis.com/topol-io-plugin-97de3577-4897-4379-9699-a51756301fab/plugin-assets/17326/user/bridge.jpg",
      "href": "https://www.topol.io",
      "my-text-1": "My text 1 in block 3",
      "my-text-2": "My text 2 in block 3",
      "my-text-3": "My text 3 in block 3",
      "feed_id": 1
    }
  ],
  "total_records": 3
}

The editor calls it with per_page (5), current_page, search, id, category_id, and the selected feed under both feed and feed_id. total_records drives the page count, so it should reflect the full result set rather than the current page.

3. Categories endpoint (optional)

For each feed, this endpoint returns a list of categories that can be used to further narrow the items in the selection modal. If you don't define categoriesURL on the block, the category dropdown is hidden and users only filter by feed.

The endpoint receives the selected feed via the feed_id query parameter, alongside per_page (10), current_page and search, and should return:

json
{
  "success": true,
  "data": [
    { "id": 1, "name": "Category 1", "feed_id": 1 },
    { "id": 2, "name": "Category 2", "feed_id": 1 }
  ],
  "total_records": 2
}

When a category is picked, the items endpoint is called again with the category_id query parameter set so it can return a filtered subset.

How to configure a Custom API Block

The configuration for a Custom API Block defines how the data is mapped and styled within your template. Below is a detailed example of the configuration.

Example of a Custom API Block
js
  apiBlocks: {
      testBlock: {
        itemsURL: "https://example.test/my-block",
        feedsURL:
          "https://example.test/my-block-feeds",
        categoriesURL:
          "https://example.test/my-block-categories", // optional
        name: "Test Block",
        pluralName: "Test Blocks",
        icon: "",
        // optional, see "Static and dynamic mode" below
        dynamicMergetags: [
          { label: "Recommended items", value: "REC_ITEMS" },
        ],
        // optional, see "Mapping API field names" below
        apiStructure: {
          name: "product_name",
          image: "image_url",
        },
        blockStructure: {
          image: {
            defaultValue:
              "https://s3-eu-west-1.amazonaws.com/ecomail-assets/editor/pp1.png",
            label: "Image",
            width: 250,
            align: "center",
          },
          href: {
            defaultValue: "*|MY_HREF|*",
          },
          "my-text-1": {
            defaultValue: "*|MY_TEXT_1|*",
            label: "My Text 1",
            "font-size": "16px",
            color: "#123456",
            "font-family": "Helvetica, Arial, sans-serif",
            "font-weight": "bold",
          },
          "my-text-2": {
            defaultValue: "*|MY_TEXT_2|*",
            label: "My Text 2",
            "font-size": "14px",
            color: "#654321",
            "font-family": "Helvetica, Arial, sans-serif",
            "font-weight": "normal",
          },
          "my-text-3": {
            defaultValue: "*|MY_TEXT_3|*",
            label: "My Text 3",
            "font-size": "12px",
            color: "#000000",
            "font-family": "Helvetica, Arial, sans-serif",
            "font-weight": "normal",
            "font-style": "italic",
          },
          button: {
            defaultValue: "Click me",
            label: "Button",
            align: "center",
            "background-color": "#123456",
            color: "#ffffff",
            "font-size": "16px",
            "font-family": "Helvetica, Arial, sans-serif",
            "font-weight": "bold",
            "font-style": "italic",
            "text-transform": "uppercase",
            "text-decoration": "none",
            "border-radius": "3px",
            padding: "10px 20px 10px 20px",
          },
        },
      },
    },

Required and optional keys

name, pluralName, icon and blockStructure are required. The three URLs, apiStructure and dynamicMergetags are optional.

WARNING

An icon value is required even when empty, as in the example above. A block missing the key fails validation. Each block is validated independently, so an invalid block is dropped on its own (with an error notification) while the remaining blocks still load.

Four further optional flags are available:

  • apiWithoutSearch hides the search inputs and switches items to a "Load more" control.
  • nextKey switches feeds and items to keyset pagination, sending next and published_at instead of current_page.
  • beta adds a beta badge to the block tile.
  • disabled greys the tile out.

Styling attributes

The block configuration includes several customizable styling attributes for different elements within the Custom API Block.

Image

  • defaultValue Default image URL.
  • label Descriptive label for the image.
  • width Image width (in pixels).
  • align Alignment of the image (e.g., center, left, right).
  • alt Alt text.
  • title Title attribute.
  • fluidOnMobile Makes the image full-width on mobile.
  • columns-layout Column arrangement, defaults to one-column.
  • defaultValue Default URL or placeholder for the hyperlink.

Text field

Each text field can be customized with various styling attributes:

  • defaultValue Default text or placeholder.
  • label Descriptive label for the text field.
  • font-size Font size (e.g., 16px).
  • color Text color (e.g., #123456).
  • font-family Font family (e.g., Helvetica, Arial, sans-serif).
  • font-weight Font weight (e.g., bold, normal).
  • font-style Font style (e.g., italic).
  • text-transform Text transformation (e.g., uppercase).
  • text-decoration Text decoration (e.g., underline).
  • padding Padding around the text.
  • align Text alignment.

Button

  • defaultValue Default button text.
  • label Descriptive label for the button.
  • align Button alignment (e.g., center).
  • text-align Alignment of the label inside the button.
  • width Button width.
  • background-color Background color of the button.
  • gradient Gradient background.
  • color Text color.
  • font-size Font size.
  • font-family Font family.
  • font-weight Font weight.
  • font-style Font style.
  • text-transform Text transformation (e.g., uppercase).
  • text-decoration Text decoration (e.g., none).
  • border-radius Border radius (in pixels).
  • padding Padding around the button.
  • inner-padding Padding between the button edge and its label.
  • hover Hover state styling.

The layout key

blockStructure also accepts a layout string that pins the order and visibility of the block's parts, for example "image:show,title:show,button:show". Without it, the editor derives the order from your other blockStructure keys.

Static and dynamic mode

By default, a Custom API Block works in static mode: the user picks specific items from a feed (and optionally a category), and those exact items are saved into the template.

Dynamic mode binds the block to a merge tag instead, and the ESP fills in real items at send time. Enable it by setting dynamicMergetags on the block:

js
apiBlocks: {
  testBlock: {
    // ...
    dynamicMergetags: [
      { label: "Recommended items", value: "REC_ITEMS" },
      { label: "Last viewed", value: "LAST_VIEWED" },
    ],
  },
},

Each entry becomes one option in the block's dynamic-mode dropdown, where label is shown to the user and value is what the renderer uses internally. The same global requirements as for the built-in Product block apply (emailServiceProvider: "sparkpost" and smartMergeTags enabled with syntax).

When dynamicMergetags is set, the editor also synthesizes a Loop Merge Tag for each entry, so it becomes selectable in Loop Blocks too. Its childrenProperties are derived from your blockStructure keys, excluding layout and button, and named after the API fields from apiStructure where one is defined.

Mapping API field names

blockStructure defines the editor's internal field names (name, image, my-text-1). When your API returns different names, such as product_name instead of name, map them with apiStructure:

js
apiBlocks: {
  testBlock: {
    // ...
    apiStructure: {
      name: "product_name",
      image: "image_url",
      "my-text-1": "short_description",
    },
  },
},

The keys are your blockStructure names; the values are the field names in your API response. The editor reads item[apiStructure[key]] and stores it under key, so the rest of the editor can stay agnostic of the upstream JSON shape.

If apiStructure is omitted, the editor expects API field names to match blockStructure keys exactly.