TrophyCustomer's Canvas is honored with a 2020 InterTech Technology Award! Learn more 

Webhooks for State Files

Customer's Canvas supports three types of product templates:

You can only work with the first two types when you create product designs in Adobe software. Your users can select and personalize these designs during the ordering process. If you allow your users to create designs in Customer's Canvas from scratch, make changes to their templates, or go back to edit the designs, then you have to deal with the state files that represent all objects loaded in the editor.

Customer's Canvas creates a state file every time you call Editor.saveProduct or Editor.finishProductDesign. The difference is that the latter method also renders the product and generates URLs that link to hi-res and proof images. By default, these state files are saved in the ..\userdata\<someUserId>\states\ folder for a single user. You can also define another folder on your server by using the UserDataFolder parameter. When you need to organize common storage of state files for a load-balanced environment, you can refer to the Scalability of Customer's Canvas topic and learn how to use symlinks to work with state files in this case.

Now, let's learn how you can enable external storage for state files.

Configuring Webhooks

As an alternative to the default location, Customer's Canvas allows you to organize external storage for state files and implement custom HTTP callbacks triggered by saving and loading the state files in the Design Editor. To enable such webhooks, you need to define ExternalStatePushUrl and ExternalStatePullUrl in AppSettings.config.

    <add key="ExternalStatePushUrl" value="{userId}/{stateId}" />
    <add key="ExternalStatePullUrl" value="{userId}/{stateId}" />
    <add key="ExternalStateSecurityKey" value="UniqueSecurityKey" />
    <add key="UserDataFolder" value="..\userdata" />

When you define these endpoints, the Design Editor sends a state file to ExternalStatePushUrl and does not save it to the local user data folder if it receives a success response. When loading a state file, the editor searches for this file in its local storage and cache first. Then, the editor tries to pull this file from ExternalStatePullUrl. If the state file is not found, an exception occurs.

To pass the state ID and user ID, use the {stateId} and {userId} values, correspondingly. You can specify them as either resource paths or query parameters in your endpoints, for example,{userId}&stateId={stateId}.

When Customer's Canvas makes these calls, it passes the X-ExternalStateStorageApiKey header with the specified security key.

A Sample Implementation

This sample illustrates how you can implement a GET request to pull and a POST request to push state files.

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web;
using System.Web.Hosting;
using System.Web.Http;

namespace SampleControllers
    public class StateController : ApiController
        public HttpResponseMessage Push(string userId, string stateId)
            // Retrieve a file from the request.
            var file = HttpContext.Current.Request.Files[0];

            // Create a file name based on userId and stateId.
            var fileName = HostingEnvironment.MapPath("~/states") + $"/{userId}_{stateId}.st";


            return new HttpResponseMessage(HttpStatusCode.OK);

        public HttpResponseMessage Pull(string userId, string stateId)
            // Obtain a file name based on userId and stateId.
            var fileName = HostingEnvironment.MapPath("~/states") + $"/{userId}_{stateId}.st";

            return StreamToResponse(File.OpenRead(fileName), "application/zip");

        public static HttpResponseMessage StreamToResponse(Stream stream, string mimeType, string attachmentFileName = null)
            if (stream == null)
                return new HttpResponseMessage(HttpStatusCode.NotFound);

            var response = new HttpResponseMessage(HttpStatusCode.OK)
                // Send the state file in the request body.
                Content = new StreamContent(stream)
            response.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType);
            response.Headers.CacheControl = new CacheControlHeaderValue
                Public = true,
                MaxAge = TimeSpan.FromHours(12)

            // Assign a file name to the content if needed.
            if (attachmentFileName != null)
                response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
                    FileName = attachmentFileName

            return response;


See Also