---
title: "Custom API Blocks"
description: "The Custom API Block allows you to load categorized data from an API endpoint and map it to customizable attributes that you can style and use inside templates."
url: https://docs.topol.io/email-editor/guide/custom-api-blocks.html
---

# 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](https://topol.io/pricing) 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](https://docs.topol.io/api-feed-block.png)

## 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`.

### Hyperlink (href)

-   `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](https://docs.topol.io/email-editor/guide/products.html#dynamic-mode) 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](https://docs.topol.io/email-editor/guide/loop-blocks.html) 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.
