Create a CloudPages form with an image/file upload option

The HTML <input> tag specifies an input field where the user can enter data. An input field can vary in many ways, depending on the type attribute. The <input> elements with type="file" let the user choose one or more files from their device storage. The file can then be manipulated using JavaScript in order to process it further.

In this article, we will focus on creating a CloudPages form with an image input field, and uploading that image into Marketing Cloud’s Content Builder. The high-level outline of this process, which can be found in this Stack Exchange post, is: once the file has been uploaded via the form, it needs to be Base64 encoded, and then passed in a REST API call to the /asset/v1/content/assets route in order to create the image in Content Builder.

We will need to create two separate CloudPages: the first one will run client-side JavaScript in order to encode the file into a Base64 string and it will then pass it to the second CloudPage, which will run server-side JavaScript to make an API call to create a new asset in Content Builder.

I will break it down into smaller parts to make it easier to understand the different steps along the way.

Create an input field

Let’s start with creating an <input> field on a CloudPage. The input needs to be of the type="file" in order to allow the end-user to choose a file for upload from their local machine. We will also add an accept attribute, which specifies a filter for what file types the user can pick from the file input dialogue box. For our use case, it will be image/*: [see code snippet]

We also need to include an id attribute in order to be able to reference the input field in JavaScript.

Encode the image using Base64

Base64 is a group of binary-to-text encoding schemes which enable storing and transferring data over media that are designed to deal with ASCII (text) only. In order to encode the image, we will use the FileReader.readAsDataURL() method to securely read the contents of a file stored on the user’s computer and return a result attribute containing a data: URL representing the file’s data. We will use the addEventListener() method to attach a click event to the Upload button, which will trigger the encoding function: [see code snippet]

The format of the returned result is the following:

data:[<mediatype>][;base64],<data>

The mediatype is a MIME type string, such as ‘image/jpeg’ for a JPEG image file and <data> is the Base64 encoded string representing the uploaded file.

Fetch the data to processing page

We now need to pass the data onto our second CloudPage for further processing. The reason why we are using two pages instead of one is that client-side JavaScript is run after the server-side is complete, which makes it impossible to run them in reverse order on the same page.

In order to pass the data between our CloudPages, we will use the fetch() method. But first, let’s prepare the data that we will need for our API call: [see code snippet]

  • base64enc is the encoded string which represents the image
  • fullFileName is the full name of the file uploaded by the user (eg. “astro.png”)
  • fileName is the first part of the file name, before the filename extension (eg. “astro”)
  • assetName is the filename extension (eg. “png”)

Once we have the above, we can POST it in JSON format to the processing page that will contain the server-side script. Our request will look like this: [see code snippet]

where the method is POST, headers specify that the content-type is JSON and in the JSON body we will have three attribute-value pairs, base64enc, fileName and assetName.

The fetch() method argument is the path to the CloudPage containing our server-side script, so, for now, you can create an empty CloudPage just to obtain the link.

The fetch() method returns a promise that resolves to a response to that request, whether it is successful or not: .then(function(res) / .catch(function(err).

The full client-side script

Let’s now put all of the above elements together to create our first CloudPage. The complete code will look like this: [see code snippet]

Now we can move on and prepare the second part of the script.

Retrieve the posted data

In order to retrieve the data posted from our first CloudPage, we will use the getPostData() server-side JavaScript method, which sadly is not documented in the official Server-Side JavaScript Syntax Guide, but you can read about it in this post on Salesforce Stack Exchange. Once we access the body of our request, we can parse the data using the ParseJSON() function: [see code snippet]

Prepare the data for API call

Now we can prepare the data for our API call. Let’s take a look at an example JSON body of a create asset request: [see code snippet]

In the above, the first name represents the name of the file. The second parameter assetType, consists of a name which represents the file extension and an id of the asset type. Asset types are the file or content types supported by Content Builder and the full list can be found here: List of Asset Types.

Let’s prepare the data and match the asset type based on the name of the file extension: [see code snippet]

If you’re only going to upload images, you don’t need to include the whole list of asset types – the ones that start with “2” will be sufficient.

Create an asset using REST API

In order to make an API call using the /asset/v1/content/assets route, we first need to authenticate. You will need the Installed Package for API integration, the Client Id and Cient Secret. If you’re not sure how to authenticate programmatically in Salesforce Marketing Cloud, refer to my article, Salesforce Marketing Cloud API Authentication using Server-Side JavaScript.

As a security measure, it’s best to store your Client Id and Secret as encoded values in a Data Extension to avoid exposing them in your script. My preferred way is to use the EncryptSymmetric and DecryptSymmetric AMPscript functions for encryption, and a simple lookup to get their values from a Data Extension.

Here’s the code snippet for the authentication part:

Once we obtain the accessToken, we will need to include it in our final API call, along with the rest_instance_url: [see code snippet]

The above call will create an image in Content Builder’s main folder and return the following response: [see code snippet]

The full server-side script

Here is the full script for our second CloudPage: [see code snippet]

You can now go back to your first CloudPage and start uploading!

To see this script in action, visit my CloudPage.


Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

11 thoughts on “Create a CloudPages form with an image/file upload option

  1. kevin

    Hi ,
    Thanks for your blog about it.
    could i ask why need to use cloud page to upload image,can you explain the background or the business case
    since we can use content builder to content builde

    Like

    1. Hi Kevin – sure!
      Think of all the use cases, where you create a form for your subscribers to fill in and there is a need to attach some sort of a file along with it.
      It could also be used in case you would want someone to be able to create/upload content, but without creating a user for them in Salesforce Marketing Cloud. Hope this helps!
      Zuzanna

      Liked by 1 person

  2. Hey zuzanne,
    I’ve been seeing a lot of these posts recently about how you can use cloud pages for various things and it scares me a bit to have cloud pages used without any additional authentication since it opens up your marketing cloud instance to security issues – maybe worth to mention this kind of stuff in future since a lot of people will just copy paste:)

    Like

  3. Evan Vega

    Hi Zuzanne –

    I have created a Cloud Page with a form to upload an image using your tutorial. I am able to successfully upload the image to Content Builder and also send a triggered email containing all of the form info using AMPScript, except I cannot for the life of me figure out how to grab the URL of the uploaded image in order to include it in the email. I see that the createAsset response returned by the API contains the created object along with it’s published URL and I learned how to assign it to a variable, but I still can’t figure out how to grab the value so I can upsert it to a DE with the other form data. I am using AMPScript to perform the upsert and it is working fine.

    Published URL:
    `var createAsset = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);
    var asset = Platform.Function.ParseJSON(createAsset);
    var publishedUrl = asset.fileProperties.publishedURL;`

    Upsert:
    `Set @campaignMgr = RequestParameter(‘CampaignMgr’)
    Set @results_DE = “Results_DE”
    UpsertData(@results_DE,1,
    ‘CampaignMgr’, @campaignMgr)`

    Thanks in advance for your help!

    Like

    1. Hi Evan and thank you for your question – yes, you are correct, the url is returned in the response from the API call. You could do something like this in your server-side script to extract the url: parse the response from the call, then create a new variable containing the url, and write it to a Data Extensions. It would be very hard and inefficient to try to do this in AMPscript, so it’s better to go with SSJS:


      var createAsset = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);
      //stringify response
      var respo = createAsset.Response.toString();
      //parse JSON
      var res = Platform.Function.ParseJSON(respo);
      //get the image url
      var imgURL = res.fileProperties.publishedURL;
      //initiate your data extension
      var myDE = DataExtension.Init("{{external key of the data extension}}");
      //add a new row with url to a data extensions
      myDE.Rows.Add({ URLcolumn: imgURL });

      Hope this helps 🙂

      Like

      1. Evan Vega

        Thank you for your quick response, Zuzanna! I have tried and tried and though the image uploads just fine, the publishedURL does not populate in the DE.

        I’m not sure if this is part of the issue, but the only way I can get the AMPScript upsert to work is by having the processing page URL as the action in my form. If it’s only in the fetch script, the page reloads and the image is uploaded to Content Builder, but I do not get the Success alert. If the processing page is the form action, I get directed to it upon submit and the image is uploaded and the AMPScript upsert happens, but the SSJS add does not work. (Once I get it working I’ll do all of the upserting with SSJS, but obviously I’m just not there yet!) 🙂

        I tried printing out the publishedURL, too, and it always comes up as undefined.

        var createAsset = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);

        //stringify response
        var respo = createAsset.Response.toString();

        //parse JSON
        var res = Platform.Function.ParseJSON(respo);

        //get the image url
        var imgURL = res.fileProperties.publishedURL;

        //initiate your data extension
        var myDE = DataExtension.Init(“xxxx-xxx-xxxx-xxxx-xxxx”);

        //add a new row with url to a data extensions
        var fileURL = myDE.Rows.Add({FileURL:imgURL});

        Write(“File URL:” + fileURL);

        Like

  4. Evan Vega

    Ok, I’m an idiot – the DE I was attempting to write to had a primary key and of course, I was. not passing one through to it. All is good now! Thanks again for your help. 🙂

    Like

  5. Hi Zuzanna,

    I’m getting a 400 error message when the content builder API call is made but haven’t been able to figure out yet why that is. This is the code I’m using on the processing page:

    Platform.Load(“Core”,”1″);
    var api = new Script.Util.WSProxy();
    api.setClientId({ “ID”: xx });

    try {
    //fetch posted data
    var jsonData = Platform.Request.GetPostData();
    var obj = Platform.Function.ParseJSON(jsonData);

    //prepare data for API call
    var base64enc = obj.base64enc;
    var fileName = obj.fileName;
    var assetName = obj.assetName;

    //match asset type with uploaded file (https://developer.salesforce.com/docs/atlas.en-us.noversion.mc-apis.meta/mc-apis/base-asset-types.htm)
    var assetTypes = { ai: 16, psd: 17, pdd: 18, eps: 19, gif: 20, jpe: 21, jpeg: 22, jpg: 23, jp2: 24, jpx: 25, pict: 26, pct: 27, png: 28, tif: 29, tiff: 30, tga: 31, bmp: 32, wmf: 33, vsd: 34, pnm: 35, pgm: 36, pbm: 37, ppm: 38, svg: 39, “3fr”: 40, ari: 41, arw: 42, bay: 43, cap: 44, crw: 45, cr2: 46, dcr: 47, dcs: 48, dng: 49, drf: 50, eip: 51, erf: 52, fff: 53, iiq: 54, k25: 55, kdc: 56, mef: 57, mos: 58, mrw: 59, nef: 60, nrw: 61, orf: 62, pef: 63, ptx: 64, pxn: 65, raf: 66, raw: 67, rw2: 68, rwl: 69, rwz: 70, srf: 71, sr2: 72, srw: 73, x3f: 74, “3gp”: 75, “3gpp”: 76, “3g2”: 77, “3gp2”: 78, asf: 79, avi: 80, m2ts: 81, mts: 82, dif: 83, dv: 84, mkv: 85, mpg: 86, f4v: 87, flv: 88, mjpg: 89, mjpeg: 90, mxf: 91, mpeg: 92, mp4: 93, m4v: 94, mp4v: 95, mov: 96, swf: 97, wmv: 98, rm: 99, ogv: 100, indd: 101, indt: 102, incx: 103, wwcx: 104, doc: 105, docx: 106, dot: 107, dotx: 108, mdb: 109, mpp: 110, ics: 111, xls: 112, xlsx: 113, xlk: 114, xlsm: 115, xlt: 116, xltm: 117, csv: 118, tsv: 119, tab: 120, pps: 121, ppsx: 122, ppt: 123, pptx: 124, pot: 125, thmx: 126, pdf: 127, ps: 128, qxd: 129, rtf: 130, sxc: 131, sxi: 132, sxw: 133, odt: 134, ods: 135, ots: 136, odp: 137, otp: 138, epub: 139, dvi: 140, key: 141, keynote: 142, pez: 143, aac: 144, m4a: 145, au: 146, aif: 147, aiff: 148, aifc: 149, mp3: 150, wav: 151, wma: 152, midi: 153, oga: 154, ogg: 155, ra: 156, vox: 157, voc: 158, “7z”: 159, arj: 160, bz2: 161, cab: 162, gz: 163, gzip: 164, iso: 165, lha: 166, sit: 167, tgz: 168, jar: 169, rar: 170, tar: 171, zip: 172, gpg: 173, htm: 174, html: 175, xhtml: 176, xht: 177, css: 178, less: 179, sass: 180, js: 181, json: 182, atom: 183, rss: 184, xml: 185, xsl: 186, xslt: 187, md: 188, markdown: 189, as: 190, fla: 191, eml: 192, text: 193, txt: 194, freeformblock: 195, textblock: 196, htmlblock: 197, textplusimageblock: 198, imageblock: 199, abtestblock: 200, dynamicblock: 201, stylingblock: 202, einsteincontentblock: 203, webpage: 205, webtemplate: 206, templatebasedemail: 207, htmlemail: 208, textonlyemail: 209, socialshareblock: 210, socialfollowblock: 211, buttonblock: 212, layoutblock: 213, defaulttemplate: 214, smartcaptureblock: 215, smartcaptureformfieldblock: 216, smartcapturesubmitoptionsblock: 217, slotpropertiesblock: 218, externalcontentblock: 219, codesnippetblock: 220, rssfeedblock: 221, formstylingblock: 222, referenceblock: 223, imagecarouselblock: 224, customblock: 225, liveimageblock: 226, livesettingblock: 227, contentmap: 228, jsonmessage: 230 };

    var assetTypeID = assetTypes[assetName];

    //authenticate to get access token
    var authEndpoint = ‘xx.marketingcloudapis.com/’ //add authentication endpoint
    var payload = {
    client_id: “xx”, //pass Client ID
    client_secret: “xx”, //pass Client Secret
    grant_type: “client_credentials”
    };
    var url = authEndpoint + ‘/v2/token’;
    var contentType = ‘application/json’;

    var accessTokenRequest = HTTP.Post(url, contentType, Stringify(payload));
    if (accessTokenRequest.StatusCode == 200) {
    var tokenResponse = Platform.Function.ParseJSON(accessTokenRequest.Response[0]);
    var accessToken = tokenResponse.access_token
    var rest_instance_url = tokenResponse.rest_instance_url
    }
    //make api call to create asset
    if (base64enc != null) {
    var headerNames = [“Authorization”];
    var headerValues = [“Bearer ” + accessToken];
    var jsonBody = {
    “name”: fileName
    “assetType”: {
    “name”: assetName,
    “id”: assetTypeID
    },
    “file”: base64enc
    };

    var requestUrl = rest_instance_url + “asset/v1/content/assets”;

    var createAsset = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);
    }

    } catch (error) {
    Write(“error: ” + Stringify(error));
    }

    Like

  6. This is the error message:

    {“message”:”An error occurred when attempting to evaluate a HTTPPost function call. See inner exception for details.”,”description”:”ExactTarget.OMM.FunctionExecutionException: An error occurred when attempting to evaluate a HTTPPost function call. See inner exception for details.
    Error Code: OMM_FUNC_EXEC_ERROR
    – from Jint –>

    — inner exception 1—

    System.Net.WebException: The remote server returned an error: (400) Bad Request. – from System

    “}

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s