Blocks API
Blocks are reusable design fragments your users save in the editor. They come in two flavors:
- Shared project blocks — available to everyone in the project.
- User-saved blocks — saved by one of your end-users and visible only to them (the user id your app passes to the editor at init time).
The Blocks API lets your backend list all of them in one sweep — you don't need to know or enumerate your end-user IDs. Each user-saved block carries the userId it was saved under, so you can:
- Report usage — which of your end-users rely on saved blocks, and how many each has.
- Back up or migrate — export every block's design JSON, including blocks saved by users you can no longer identify individually.
This endpoint returns blocks saved by every end-user in the project. Never call it from a browser, and never proxy its unfiltered response to one of your end-users — if you expose blocks to an end-user, filter with the userId parameter to that user's own blocks. See Authentication for token handling guidelines.
List blocks
GET https://api.unlayer.com/v3/blocks
Authenticate with a bearer token — a project API Key is recommended. With a Personal Access Token, also pass projectId (query param or X-Project-Id header).
curl "https://api.unlayer.com/v3/blocks?displayMode=email" \
-H "Authorization: Bearer unlayer_sk_your_api_key"
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Blocks per page (1–100). |
cursor | string | — | Pagination cursor from the previous response. |
displayMode | string | — | Filter by display mode: email, web, popup, or document. Omit to list blocks across all display modes. |
scope | string | all | shared for project blocks only, user for end-user saved blocks only, all for both. |
userId | string | — | Only blocks saved by this end-user (exact match on the user id your app passes to the editor). |
category | string | — | Filter by category (case-insensitive search). |
includeData | boolean | true | Whether to include each block's design JSON. Pass false for lightweight sweeps such as usage reports. |
projectId | string | — | Required with a Personal Access Token; omit with an API key (the project is embedded in the key). |
Response
{
"data": [
{
"id": "918",
"userId": "u_4f8a12",
"displayMode": "email",
"category": "Headers",
"tags": ["header", "promo"],
"data": { "columns": ["…"] },
"thumbnailUrl": "https://…/thumbnail.png",
"syncId": null,
"isSyncEnabled": false,
"createdAt": "2026-06-14T09:12:33.000Z",
"updatedAt": "2026-07-01T16:40:05.000Z"
},
{
"id": "702",
"userId": null,
"displayMode": "email",
"category": "Footers",
"tags": [],
"data": { "columns": ["…"] },
"thumbnailUrl": null,
"syncId": "s-b41c…",
"isSyncEnabled": true,
"createdAt": "2026-03-02T11:00:00.000Z",
"updatedAt": "2026-03-02T11:00:00.000Z"
}
],
"next_cursor": "aWQ6NzAy",
"has_more": true
}
userIdisnullfor shared project blocks and set to the end-user id for user-saved blocks.syncId/isSyncEnabledidentify synced blocks; designs reference synced blocks bysyncId, so preserve it when migrating.datais the block's design JSON, returned exactly as stored. Pages can be large withincludeData=true— keeplimitmodest when sweeping designs.
Pagination
Follow next_cursor until has_more is false:
curl "https://api.unlayer.com/v3/blocks?limit=100&cursor=aWQ6NzAy" \
-H "Authorization: Bearer unlayer_sk_your_api_key"
The listing is a point-in-time sweep: blocks created while you are paginating may not appear until the next sweep.
Recipe: who uses saved blocks, and how many each?
Sweep with includeData=false (no design payloads — fast and small), then group by userId:
const headers = { Authorization: `Bearer ${process.env.UNLAYER_API_KEY}` };
const counts = {};
let cursor = null;
do {
const url = new URL('https://api.unlayer.com/v3/blocks');
url.searchParams.set('includeData', 'false');
url.searchParams.set('scope', 'user');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await fetch(url, { headers }).then((r) => r.json());
for (const block of page.data) {
counts[block.userId] = (counts[block.userId] ?? 0) + 1;
}
cursor = page.next_cursor;
} while (cursor);
// counts = { "u_4f8a12": 12, "u_9c01d7": 3, … }
Drop scope=user to include shared project blocks in the same sweep — they arrive with userId: null.
For a backup, run the same loop without includeData=false and persist each block's data (and syncId, if set).
Notes
- The API can return more blocks than the editor shows. The editor displays blocks up to your plan's Blocks limit; this endpoint is a faithful dump of everything stored in the project, so counts can differ — for example after a plan downgrade. That is intentional: backups must not silently lose blocks.
- The API does not modify blocks — creating, updating, and deleting blocks still happens through the editor.