This article explains how to use Marketing Cloud API to interact with CloudPages.
The Past
In order to understand the different ways of interacting with CloudPages, we need to go back to the past.
In the not-so-distant past, CloudPages could be created using 2 different editors: Classic editor and Content Builder editor.
Classic editor was the first default editor for the CloudPages and had a very minimal design and functionality.
Content Builder editor, on the other hand, used the drag-and-drop functionality of Content Builder application to create modular and customisable design.
Today, the choice between these 2 editors has disappeared and all CloudPages are now created with the Content Builder editor by default.
Why is this important? Because the Content Builder editor uses the Content Builder application (Digital Asset Manager of Marketing Cloud) to save the content of CloudPages.
And the assets of Content Builder can be retrieved, created, modified and deleted using the API of Marketing Cloud.
The Present
CloudPages are saved in the Content Builder, got it! So what’s the catch?
The catch is that CloudPages themselves are not saved in the Content Builder… but their content is! Let me elaborate:
Each CloudPage is a unit with different properties such as Page ID and URL. These units can be published, unpublished or deleted. But their content is separated and stored as an asset in the Content Builder application.
This means that:
- Marketing Cloud API doesn’t interact with CloudPage units, but rather with the content of CloudPages which are stored as Content Builder assets*.
- It is currently impossible to publish, unpublish or delete CloudPages. And it is impossible to retrieve the Page ID and the URL**.
- Therefore, modifying the content of CloudPages doesn’t take effect until the page is republished from the UI of the unit!
- And there is no way of creating an inventory of CloudPages by retrieving all the Page IDs and URLs using Marketing Cloud API.
Here is an example of an API request to retrieve the content of a CloudPage using the asset ID 205 (webpage):
<script runat="server">
Platform.Load("core", "1");
var api = new Script.Util.WSProxy();
var restInstanceUrl = "https://YOUR_SUBDOMAIN.rest.marketingcloudapis.com/",
accessToken = "YOUR_REST_API_TOKEN";
try {
var payload = {
"page": {
"pageSize": 50,
"page": 1
},
"query": {
"property": "assetType.id",
"simpleOperator": "equals",
"value": 205
},
"fields": [
"id",
"customerKey",
"objectID",
"assetType",
"name",
"owner",
"content",
"createdDate",
"createdBy",
"modifiedDate",
"modifiedBy",
"thumbnail",
"category",
"views"
],
"sort": [
{
"property": "modifiedDate",
"direction": "DESC"
}
]
}
var endpoint = restInstanceUrl + "asset/v1/content/assets/query";
var request = new Script.Util.HttpRequest(endpoint);
request.emptyContentHandling = 0;
request.retries = 2;
request.continueOnError = true;
request.setHeader("Authorization", "Bearer " + accessToken);
request.method = "POST";
request.contentType = "application/json";
request.postData = Stringify(payload);
var results = request.send();
var result = Platform.Function.ParseJSON(String(results.content));
Write(Stringify(result));
} catch(error) {
Write(Stringify(error));
}
</script>
And here is the response:
{
"count": 1,
"page": 1,
"pageSize": 50,
"links": {},
"items": [
{
"id": 1234567,
"customerKey": "S0M3-GU1D-K3Y-G03SR1G4T-H3R3",
"objectID": "S0M3-GU1D-K3Y-G03SR1G4T-H3R3",
"assetType": {
"id": 205,
"name": "webpage",
"displayName": "Web Page"
},
"name": "MyCloudPage",
"owner": {
"id": 123,
"email": "example@mail.com",
"name": "John Doe",
"userId": "123"
},
"createdDate": "2024-03-22T01:24:50.567-06:00",
"createdBy": {
"id": 123,
"email": "example@mail.com",
"name": "John Doe",
"userId": "123"
},
"modifiedDate": "2024-03-28T00:11:30.06-06:00",
"modifiedBy": {
"id": 123,
"email": "example@mail.com",
"name": "John Doe",
"userId": "123"
},
"thumbnail": {
"thumbnailUrl": "/v1/assets/1234567/thumbnail"
},
"category": {
"id": 3134,
"name": "Content Builder",
"parentId": 0
},
"views": {
"html": {
"thumbnail": {},
"content": "<!DOCTYPE html><html><head></head><body></body></html>",
"meta": {},
"slots": {
"col1": {
"design": "<p style=\"font-family:arial;color:#ccc;font-size:11px;text-align:center;vertical-align:middle;font-weight:bold;padding:10px;margin:0;border:#ccc dashed 1px;\">Drop blocks or content here</p>",
"modelVersion": 2
}
},
"modelVersion": 2
}
},
"modelVersion": 2
}
]
}
This situation is unfortunate but as of Spring 2024 release it is about to change.
The Future
Future is now! As of Spring 2024 release, CloudPages have received a major update: the introduction of folders and the search bar!
Folders work exactly as expected, no more, no less, but the search bar turned out to be more interesting that it seemed.
As a matter of fact, by inspecting the HTTP request which occurs when the search bar is used, we can discover a familiar sight: a REST API request structure sent to the Content Builder endpoint.
The result of this request is a paginated JSON file with many interesting pieces of data, among which we can sometimes notice CloudPage URLs and Code Resource content.
And the request itself contains many asset type IDs that are nowhere to be found in the official documentation.
Here is an example of the payload for the search “where is Aldo”:
{
"page": {
"pageSize": 50,
"page": 1
},
"query": {
"leftOperand": {
"leftOperand": {
"property": "name",
"simpleOperator": "contains",
"boost": 50,
"value": "where is Aldo"
},
"logicalOperator": "OR",
"rightOperand": {
"leftOperand": {
"property": "content",
"simpleOperator": "contains",
"boost": 5,
"value": "where is Aldo"
},
"logicalOperator": "OR",
"rightOperand": {
"leftOperand": {
"property": "description",
"simpleOperator": "contains",
"boost": 3,
"value": "where is Aldo"
},
"logicalOperator": "OR",
"rightOperand": {
"leftOperand": {
"property": "fileProperties.fileName",
"simpleOperator": "contains",
"boost": 2,
"value": "where is Aldo"
},
"logicalOperator": "OR",
"rightOperand": {
"leftOperand": {
"property": "views.subjectline.content",
"simpleOperator": "contains",
"boost": 50,
"value": "where is Aldo"
},
"logicalOperator": "OR",
"rightOperand": {
"property": "customerKey",
"simpleOperator": "contains",
"value": "where is Aldo"
}
}
}
}
}
},
"logicalOperator": "AND",
"rightOperand": {
"property": "assetType.id",
"simpleOperator": "in",
"values": [240, 241, 242, 243, 244, 245, 247, 248, 249]
}
},
"fields": [
"assetType",
"category",
"createdDate",
"customerKey",
"id",
"modifiedDate",
"name",
"meta",
"status"
]
}
Does it mean there’s finally an API to interact with the CloudPage units? Not quiet, but it’s a start!
The Caveats
As previously stated, it is now possible to query the Content Builder and receive a list of CloudPages, their properties and content.
Considering that Code Resource pages still don’t use the Content Builder editor, it was impossible so far to retrieve their content, which is a big leap forward.
But for the rest, not everything works as it should:
- The content of Landing Pages is still stored in the Content Builder webpage asset and is not returned in the response.
- The URL of the CloudPage appears in the response when the CloudPage is republished. As a stringified object value for the content key (why???)
- There is no Published or Unpublished status provided in the response. But on the other hand, only the published pages include a date value for the publishDate key.
- Categories (folders) are now available in the response… but the Page ID is still nowhere to be found in the API! There is a new ID for the page but it doesn’t match the Page ID from the CloudPage Properties tab.
- Still no API for publishing, unpublishing, deleting or scheduling.
There is a lot of room for improvement, but since it’s the first major update of CloudPages in years, it’s something. And something is always better than nothing.
The Request
Let’s have a look at how the new API works by using SSJS to perform a retrieve request for all CloudPages.
Note that we are retrieving all the assets with the new IDs: 240, 241, 242, 243, 244, 245, 247, 248 and 249 (full list available here).
<script runat="server">
Platform.Load("core", "1");
var api = new Script.Util.WSProxy();
var restInstanceUrl = "https://YOUR_SUBDOMAIN.rest.marketingcloudapis.com/",
accessToken = "YOUR_REST_API_TOKEN";
try {
var payload = {
"page": {
"pageSize": 300,
"page": 1
},
"query": {
"property": "assetType.id",
"simpleOperator": "in",
"values": [240, 241, 242, 243, 244, 245, 247, 248, 249]
},
"fields": [
"id",
"customerKey",
"objectID",
"assetType",
"name",
"owner",
"content",
"createdDate",
"createdBy",
"modifiedDate",
"modifiedBy",
"thumbnail",
"category",
"meta"
],
"sort": [
{
"property": "modifiedDate",
"direction": "DESC"
}
]
}
var endpoint = restInstanceUrl + "asset/v1/content/assets/query";
var request = new Script.Util.HttpRequest(endpoint);
request.emptyContentHandling = 0;
request.retries = 2;
request.continueOnError = true;
request.setHeader("Authorization", "Bearer " + accessToken);
request.method = "POST";
request.contentType = "application/json";
request.postData = Stringify(payload);
var results = request.send();
var result = Platform.Function.ParseJSON(String(results.content));
Write(Stringify(result));
} catch(error) {
Write(Stringify(error));
}
</script>
The Response
Please pay a close attention to the content and meta keys that have quiet surprising values for an API response.
{
"count": 1,
"page": 1,
"pageSize": 50,
"links": {},
"items": [
{
"id": 1234567,
"customerKey": "S0M3-GU1D-K3Y-G03SR1G4T-H3R3",
"objectID": "S0M3-GU1D-K3Y-G03SR1G4T-H3R3",
"assetType": {
"id": 247,
"name": "landingpage",
"displayName": "Landing Page"
},
"name": "MyCloudPage",
"owner": {
"id": 123,
"email": "example@mail.com",
"name": "John Doe",
"userId": "123"
},
"createdDate": "2024-06-15 12:15:00.000",
"createdBy": {
"id": 123,
"email": "example@mail.com",
"name": "John Doe",
"userId": "123"
},
"modifiedDate": "2024-06-15 12:15:00.000",
"modifiedBy": {
"id": 123,
"email": "example@mail.com",
"name": "John Doe",
"userId": "123"
},
"enterpriseId": 100000000,
"memberId": 100000000,
"status": {
"id": 1,
"name": "Draft"
},
"thumbnail": {
"thumbnailUrl": "/v1/assets/56789/thumbnail"
},
"content": "{\"url\":\"https://mydomain.com/mycloudpage\"}",
"category": {
"id": 123456,
"name": "MyCollection",
"parentId": 7890
},
"meta": {
"cloudPages": {
"publishDate": "2024-06-15 12:15:00.000"
}
},
"modelVersion": 2
}
]
}
The Conclusion
There is a long road ahead of us, but I’m pleased to see that the foundations are being built as we speak.
We can only hope that Salesforce is aware of these issues and is working on getting them fixed.
Notes
* CloudPage assets are hidden in the Content Builder and cannot be accessed through the UI of the application.
** As of Spring 2024 release, the API can return the URLs of CloudPages, but only when the old pages are republished and as a weird stringified object value.