---
title: "Saved Blocks and Synced Sections"
description: "Save and re-use existing parts of the email template. Automate your content creation process with saved blocks and synced sections."
url: https://docs.topol.io/email-editor/guide/saved-blocks.html
---

# Saved blocks and synced sections

**Both features reuse content across templates, and the difference is whether the copies stay connected to the original.**

-   **Saved Blocks** - Save any section or content block and reuse it across multiple templates. Inserting a saved block creates an independent copy, editable on its own.

-   **Synced Sections** - Save sections that stay synchronized across all templates. Editing a synced section updates every place it is used.


## Enabling saved blocks

Users save a structure with the "Save as Block" button while that structure is selected.

![Save as block](https://docs.topol.io/save-as-block.png)

To enable the feature with no pre-existing blocks, set `savedBlocks` to an empty array.

```ts
savedBlocks: [],
```

To turn it off, set `savedBlocks` to `false`. This is the default.

```ts
savedBlocks: false,
```

Each saved block within the `savedBlocks` array is an object with the following structure:

```ts
{
  id: 1, // ID of the block, required
  name: 'My saved block 001', // Shown as the block's label in the panel
  definition: 'json' // JSON of this saved block
},
```

`id` is required. A block without one fails validation, and a single invalid block rejects the whole array.

## Enabling Synced Sections

To enable synced sections, set `syncedSectionsEnabled` to `true` and configure the `api.SAVED_SECTIONS` endpoint. Synced sections are always API-backed, so they cannot be configured locally the way saved blocks can.

```ts
const TOPOL_OPTIONS = {
  savedBlocks: true,
  syncedSectionsEnabled: true,
  api: {
    SAVED_SECTIONS: "https://your-domain.com/saved-blocks",
  },
};
```

`syncedSectionsEnabled` defaults to `false`. When `true`:

-   The Saved Blocks panel gains a type filter with the values `all`, `saved_section`, and `synced_section`.
-   A "Save as synced section" action appears on the section toolbar for any movable section that isn't already synced.
-   Sections backed by a synced section are rendered with a locked visual treatment in the canvas and can only be edited through the section actions menu, which updates the source on your backend.

Unlike regular saved blocks, synced sections maintain a live connection to every template where they're used. When the source is updated on your backend, the editor pulls the latest definition the next time it loads the template (or when you call [`TopolPlugin.refreshSyncedSections()`](#refresh-synced-sections)).

> **Server-side resolution is required**
>
> A synced section in a saved template JSON is stored as a lightweight reference (just a `syncedId` on the section), not as the section's full content. Those references **must** be resolved on your backend by passing the matching synced sections to the [Convert JSON template to HTML](https://docs.topol.io/email-editor/guide/operations/convertJson2Html.html) endpoint before generating the final HTML. Without that step, synced sections render as empty placeholders. See [Server-Side Resolution](#server-side-resolution) below.

## Advanced Configuration

In manual mode (`savedBlocks` set to an array of local blocks, with no `api.SAVED_SECTIONS` configured), three callbacks are required to make saved blocks persist:

-   `onBlockSave`
-   `onBlockEdit`
-   `onBlockRemove`

All of them need to be implemented in the `callbacks` object. These callbacks do **not** fire when `savedBlocks` is `true` (API-based mode), and `onBlockEdit` / `onBlockRemove` also stop firing as soon as you configure `api.SAVED_SECTIONS`. Configuring that endpoint switches the panel into API mode regardless of whether `savedBlocks` is an array or `true`. Synced sections always go through the [API-Based Configuration](#api-based-configuration) below and never trigger any of these callbacks.

The two gates differ, which matters for one combination. `onBlockSave` stops only when `savedBlocks` is exactly `true`, while the panel switches to API mode on either `savedBlocks: true` or a configured `api.SAVED_SECTIONS`. An array plus `api.SAVED_SECTIONS` therefore still fires `onBlockSave` while edit and remove go to your API. Pick one mode rather than mixing them.

`onBlockSave` receives a single object with a `definition` (the section's MJML JSON) and a `translation` map. `translation` is `undefined` unless the template is multilingual. The other two callbacks each receive the block's `id`.

Example:

```ts
callbacks: {
  onBlockSave({ definition, translation }) {
    const name = window.prompt('Enter block name:')
    if (name !== null) {
      console.log('saving block', definition, translation)
    }
  },

  onBlockRemove(id) {
    if (window.confirm('Are you sure?')) {
      console.log('removing block', id)
    }
  },

  onBlockEdit(id) {
    const name = window.prompt('Block name:', 'My block 001')
    if (name !== null) {
      console.log('saving edited block', id)
    }
  }
}
```

Since it's your responsibility to handle those actions, you also need to update the list of saved blocks once they are updated. To do this, you can use the `TopolPlugin.setSavedBlocks([savedBlocks])` function.

This function takes an array of all saved blocks you want to display in the editor.

```ts
TopolPlugin.setSavedBlocks([
  {
    id: 1, // ID of the block, required
    name: "My saved block 001", // Shown as the block's label in the panel
    definition: "json", // JSON of this saved block
  },
]);
```

## API-Based Configuration

For a more advanced setup, you can use the API-based feature. This allows you to store saved blocks and synced sections on your server with support for folders, search, pagination, and preview images.

To enable API-based saved blocks and synced sections, set `savedBlocks` to `true` and configure the `api.SAVED_SECTIONS` endpoint:

```ts
const TOPOL_OPTIONS = {
  savedBlocks: true,
  syncedSectionsEnabled: true, // Enable synced sections
  api: {
    SAVED_SECTIONS: "https://your-domain.com/saved-blocks",
  },
};
```

Both saved blocks and synced sections use the same `API.SAVED_SECTIONS` endpoint. They are differentiated by the `type` field in the API responses and requests.

### Before implementing the endpoints check [how to work with API endpoints](https://docs.topol.io/email-editor/guide/how-to-work-with-your-api.html)

## List Saved Blocks, Synced Sections and Folders

**Request**

-   URL: `/{API.SAVED_SECTIONS}`
-   Method: `GET`
-   Content-Type: `application-json`

Params:

| key | value | required |
| --- | --- | --- |
| key | authorization api key | true |
| hostname | hostname | true |
| entity\_id | entity\_id corresponds to userId in Options | true |
| per\_page | number of items per page | false |
| current\_page | number of page to return | false |
| sort\_by | "name" or "date" or "type" | false |
| desc | `true`, sent only when sorting descending | false |
| folder\_id | id of folder to return | false |
| search | string to search | false |
| type | "saved\_section" or "synced\_section" to filter by type | false |

This endpoint is called when the editor lists all saved blocks/synced sections, changes sort, page or folder, searches by string, or when you call `TopolPlugin.refreshSyncedSections()`.

**Response**

The `data.data` array mixes folders, saved sections, and synced sections and the `type` field discriminates between them. Synced sections additionally carry a `translation` field used by Multilingual Templates (see [Multi-language Support](#multi-language-support) below).

```json
{
  "success": true,
  "data": {
    "data": [
      {
        "id": 1,
        "name": "Footer",
        "type": "synced_section",
        "created_at": "2026-01-15T14:48:00.000Z",
        "definition": {
          "tagName": "mj-section",
          "attributes": { "padding": "10px 10px" },
          "children": [
            {
              "tagName": "mj-column",
              "attributes": { "width": "100%" },
              "children": [
                {
                  "uid": "abc123",
                  "tagName": "mj-text",
                  "content": "Unsubscribe",
                  "attributes": {}
                }
              ],
              "uid": "col-uid-1"
            }
          ],
          "uid": "P2fM6_fcp"
        },
        "image": "https://url-to-preview-image.jpg",
        "folder_id": 2,
        "translation": {
          "en": { "abc123": { "content": "Unsubscribe" } },
          "de": { "abc123": { "content": "Abmelden" } }
        }
      },
      {
        "id": 2,
        "name": "Hero banner",
        "type": "saved_section",
        "created_at": "2026-01-12T09:21:00.000Z",
        "definition": { "tagName": "mj-section", "children": [], "uid": "..." },
        "image": "https://url-to-preview-image.jpg",
        "folder_id": null
      },
      {
        "id": 3,
        "name": "Newsletters",
        "type": "folder",
        "created_at": "2026-01-10T08:00:00.000Z",
        "folder_id": null
      }
    ],
    "lastPage": 5,
    "parentFolderId": null
  }
}
```

`definition` is the MJML section as a nested JSON object, and `translation` (synced sections only) is a locale-keyed map where each locale maps block UIDs to their translated content. The [Convert JSON template to HTML](https://docs.topol.io/email-editor/guide/operations/convertJson2Html.html) endpoint accepts the same fields but JSON-encoded as strings. See [Server-Side Resolution](#server-side-resolution).

### Understanding `parentFolderId`

The `parentFolderId` field tells the editor which folder is the **parent** of the folder currently being viewed. The editor uses this value for breadcrumb navigation and to traverse back up the folder hierarchy.

**How to compute it:** When the request includes a `folder_id` parameter, look up that folder in your database and return its parent folder's ID. If the folder is at the root level (has no parent), return `null`.

| Scenario | `parentFolderId` value |
| --- | --- |
| Root level (no `folder_id` in request) | `null` |
| Root-level folder (folder has no parent) | `null` |
| Nested folder (folder's parent is folder `5`) | `5` |

> **WARNING**
>
> `parentFolderId` must be the ID of the **parent** of the queried folder, not the queried folder's own ID. Returning the folder's own ID causes infinite API requests and broken breadcrumb navigation.

## Create Saved Block, Synced Section or Folder

-   URL: `/{API.SAVED_SECTIONS}`
-   Method: `POST`

This endpoint is called when a new saved block, synced section, or folder is created.

**Request**

```json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "type": "saved_section",
  "name": "name of new item",
  "definition": "json of MJML section",
  "folder_id": 1
}
```

For synced sections, set `type: "synced_section"`. When the template containing the section uses multilingual templates, the editor also sends a `translation` field: a locale-keyed map of block UID to translated fragment. Persist it alongside the definition so it can be returned to clients later.

```json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "type": "synced_section",
  "name": "Footer",
  "definition": { "tagName": "mj-section", "children": [], "uid": "..." },
  "translation": {
    "en": { "abc123": { "content": "Unsubscribe" } },
    "de": { "abc123": { "content": "Abmelden" } }
  },
  "folder_id": 1
}
```

**Response**

STATUS 200

```json
{
  "success": true,
  "data": {
    "id": 2,
    "name": "name of saved block, synced section, or folder",
    "type": "saved_section",
    "created_at": "ISO 8601 time",
    "definition": "MJML section as nested JSON object",
    "translation": "locale-keyed translation map (synced sections only)",
    "folder_id": 1
  }
}
```

## Get Saved Block or Synced Section Detail

**Request**

-   URL: `/{API.SAVED_SECTIONS}/{id}`
-   Method: `GET`
-   params: `key`, `hostname`, `entity_id`

This endpoint is called when inserting a saved block into a template. For synced sections, it is also called once per synced section during [`TopolPlugin.refreshSyncedSections()`](#refresh-synced-sections) to pull the latest definition of every synced section in the open template.

**Response**

STATUS 200

```json
{
  "success": true,
  "data": {
    "id": 1,
    "name": "name of saved block or synced section",
    "type": "synced_section",
    "created_at": "ISO 8601 time",
    "definition": "MJML section as nested JSON object",
    "translation": "locale-keyed translation map (synced sections only)",
    "folder_id": 2
  }
}
```

## Edit Saved Block, Synced Section or Folder

**Request**

-   URL: `/{API.SAVED_SECTIONS}/{id}`
-   Method: `PATCH`

This endpoint is called when editing the name, content, or folder location of a saved block, synced section, or folder. The editor sends only the fields that changed. To move an item back to the root level (out of all folders), the editor sends `folder_id: null` — treat `null` as "no folder" and clear the item's folder assignment. This is consistent with `GET` responses, where root-level items have `folder_id: null`.

```json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "folder_id": 1,
  "definition": "MJML section as nested JSON object",
  "name": "edited name"
}
```

Moving an item back to the root folder:

```json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "folder_id": null
}
```

For synced sections edited inside a multilingual template, the PATCH also carries a `translation` field, mirroring the `POST` create body: a locale-keyed map of block UID to translated fragment, captured from the same in-template section as the `definition`. Persist it exactly as you do on create so the section's non-primary-language content stays in sync.

```json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "definition": { "tagName": "mj-section", "children": [], "uid": "..." },
  "translation": {
    "en": { "abc123": { "content": "Unsubscribe" } },
    "de": { "abc123": { "content": "Abmelden" } }
  }
}
```

The PATCH body carries no `type` field. Identify the record by the `{id}` in the URL.

> **Translations are re-sent on edit for multilingual synced sections**
>
> When a synced section is edited in a multilingual template, the PATCH request **includes** the `translation` field. Persist it exactly as on create, since the `translation` field is identical between `POST` and `PATCH`. The field is **omitted** for non-multilingual templates and for ordinary saved blocks, so a PATCH without `translation` should leave any stored translation untouched.

**Response**

STATUS 200

## Delete Saved Blocks, Synced Sections or Folders

-   URL: `/{API.SAVED_SECTIONS}/delete`
-   Method: `POST`

This endpoint is called when the user deletes saved blocks, synced sections, or folders. For synced sections, the last loaded version remains in templates where it was used.

**Request**

```json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "blocksToDelete": [1, 2, 3]
}
```

**Response**

STATUS 200

## Refresh Synced Sections

Call `refreshSyncedSections` on the `TopolPlugin` instance to re-pull every synced section that is currently placed in the open template and re-fetch the panel listing from `api.SAVED_SECTIONS`.

```js
TopolPlugin.refreshSyncedSections();
```

**Signature:** `TopolPlugin.refreshSyncedSections(): void`. Takes no arguments and returns nothing.

**What it does:**

-   For every section in the open template that carries a `syncedId`, it issues a `GET /{API.SAVED_SECTIONS}/{id}` request and replaces the section in the canvas with the latest version returned by your API.
-   If the API responds successfully, the new definition is spliced into the MJML tree in place and any stale translations for the old block UIDs are dropped from the template's `langs` array before the fresh content is merged in.
-   If the API returns no result for an `id` (for example, the synced section was deleted server-side, or the response is not of type `synced_section`), the `syncedId` is stripped from the section and it becomes a regular, editable section. Its last-loaded content stays in place.

**When to call it:**

-   After your own UI creates, edits, or deletes a synced section outside the editor.
-   When multiple users edit synced sections concurrently and you want the open editor to pull the latest versions on demand.
-   When you push updates to your synced section store via background jobs and want the editor to reflect them without a full page reload.

If `api.SAVED_SECTIONS` is not configured, this call is a no-op.

## Preview Images

When creating a saved block or synced section, you can store a preview image on your server. The editor sends the block definition, and you can generate a preview image from it. Return the image URL in the `image` field when listing items.

## Server-Side Resolution

When a template is saved, every synced section in the canvas is stored as a placeholder. The section node carries only a `syncedId` referencing the synced section in your database, not its content. Before generating final HTML, your backend must resolve those placeholders into the live definitions stored on your side.

The [Convert JSON template to HTML](https://docs.topol.io/email-editor/guide/operations/convertJson2Html.html) endpoint accepts an optional `syncedSections` array for this purpose. Each item must contain:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `number` | yes | The synced section ID (matches `syncedId` on the section node) |
| `definition` | `string` | yes | JSON-encoded MJML definition of the section |
| `translation` | `string` | no  | JSON-encoded locale-keyed translation map (required for multilingual templates) |

**Request example:**

```json
{
  "definition": "{ ...template JSON... }",
  "options": { "language": "en" },
  "syncedSections": [
    {
      "id": 1,
      "definition": "{\"tagName\":\"mj-section\",\"uid\":\"GbG0yTG8t\",\"children\":[...]}",
      "translation": "{\"en\":{\"abc123\":{\"content\":\"Unsubscribe\"}},\"de\":{\"abc123\":{\"content\":\"Abmelden\"}}}"
    }
  ]
}
```

**How resolution works:**

1.  The endpoint scans the template's body sections for any node carrying a `syncedId`.
2.  For each match, it looks up the matching item in `syncedSections` by `id`.
3.  The section's child columns and blocks are spliced into the template in place of the placeholder. Block UIDs are regenerated to avoid collisions when the same synced section is used twice in one template.
4.  If the template is multilingual, the synced section's `translation` map is merged into the template's `langs[].translations` so that each language mutation renders the correct localised content.
5.  If a `syncedId` has no matching entry in the `syncedSections` array, the placeholder is left alone and the section renders empty, so make sure you fetch and forward every referenced synced section.

> **Always populate syncedSections for any template that may contain them**
>
> Always populate `syncedSections` for any template that may contain them
>
> The HTML output returned to your customers is wrong when this step is skipped. The frontend [`onSave` callback](https://docs.topol.io/email-editor/guide/callbacks.html#onsave-json-html-mutations-syncedsections) supplies the list of synced section IDs used in the template as its fourth `syncedSections` argument. Look those IDs up in your `api.SAVED_SECTIONS` storage and pass the resulting definitions (and translations, if multilingual) to `convertJson2Html` alongside the template. Walking the template JSON yourself is not necessary.

### Keep stored HTML in sync when a synced section changes

Because a synced section can be edited independently of the templates that use it, the HTML you have stored for those templates becomes stale as soon as the source changes. To keep stored HTML up to date:

1.  When your `api.SAVED_SECTIONS` storage receives a POST or PATCH for a synced section, look up every template in your database whose saved `syncedSections` ID list contains that section's ID.
2.  For each of those templates, call [Convert JSON template to HTML](https://docs.topol.io/email-editor/guide/operations/convertJson2Html.html) again, passing the latest version of every referenced synced section in the `syncedSections` request field.
3.  Overwrite the stored HTML for each template with the freshly rendered output.

If you also support concurrent editing (multiple users on the same template at the same time), call [`TopolPlugin.refreshSyncedSections()`](#refresh-synced-sections) in the open editor sessions so the canvas pulls the latest synced section definition without a full reload.

## Multi-language Support

If you're using [multilingual templates](https://docs.topol.io/email-editor/guide/multilingual-templates.html), synced sections can include translations. The `translation` field is a nested JSON object with the following shape: a top-level locale key, then a block UID, then the translated content/attributes for that block in that locale.

```json
{
  "id": 1,
  "name": "Header block",
  "type": "synced_section",
  "definition": {
    "tagName": "mj-section",
    "children": [
      {
        "tagName": "mj-column",
        "children": [
          { "uid": "abc123", "tagName": "mj-text", "content": "..." }
        ]
      }
    ],
    "uid": "P2fM6_fcp"
  },
  "translation": {
    "en": { "abc123": { "content": "Unsubscribe" } },
    "de": { "abc123": { "content": "Abmelden" } },
    "fr": { "abc123": { "content": "Se désabonner" } }
  }
}
```

Each per-block translation may carry `content` (the inner HTML/text of the block) and `attributes` (for example `href` for buttons or `src`/`alt` for images). The block UIDs in this map are the UIDs from the synced section's own `definition` - when the section is rendered into a template, they are remapped to the new generated UIDs in the resulting MJML tree.

When this section is later sent to [Convert JSON template to HTML](https://docs.topol.io/email-editor/guide/operations/convertJson2Html.html) in the `syncedSections` array, both `definition` and `translation` must be re-serialised as JSON-encoded strings - that endpoint accepts them as strings, not nested objects.

Regular (non-synced) saved blocks do not carry a `translation` field, as translations for ordinary saved blocks are merged into the host template's `langs` array at insert time and stored on the template itself.
