As a follow up to my HowToSFMC article on creating a simple authorization endpoint inside SFMC, I wanted to provide a ‘use-case’ for this endpoint. This article provides educational information and examples and is not intended to be a final product nor are there any guarantees or assumptions made from this information.

A big complaint I hear a lot from the partner community and those who have to work in multiple enterprise accounts:

“I wish there was one place I could go to in order to get data out of all the data extensions without having to push in a new token for each different BU.”

The complaint I hear all the time from the ‘in-house’/client-side is:

“I wish we had a simple place we could direct our 3rd party agencies to get information or other interactions with SFMC without giving them a clientId/secret combination”

To help solve both of those issues, I created the below example endpoint!

Note: This endpoint and process can be adjusted to fit however you want and do whatever you want (needs to be via REST API or using token auth for raw SOAP API if you want it to span across multiple BUs and not just hosted context) including combining operations – such as having an endpoint to copy a data extension to a new BU, or to Create or if already exists then Update a SQL query activity, etc. The possibilities are near endless!

What do I need?

If you utilize the Authentication Method I talked about above, then I would recommend hosting this endpoint inside the same BU as you created that process to allow for easy communication.

  1. In order to have this endpoint work, you will need your own authentication method (see the HowToSFMC article above) in order to validate and secure this endpoint.
  2. REST API Integration information stored inside of a Data Extension (including MID, clientId, clientSecret, subDomain, etc.)
  3. A Business Unit that has the ability to host a Code Resource Cloud Page (JSON Resource)

Authentication Method

This will allow you to secure the endpoint to only allow authorized users access and will also create context for each user to help control capabilities and permissions. Please read my article on HowToSFMC for a detailed example of how to build this.

Once you have this created, you will just need to implement a method to verify this. For my example, I force a token to be passed inside the headers of the POST call. I then do a lookup of that token to the Authentication Data Extension that holds the temporary tokens to validate if it is an authenticated user or not. From there it will pass/fail and either continue on with the process or halt it and write an error JSON object.

Validate token is valid inside of the authorization DE:

//Pull Token from Header
var token = Platform.Request.GetRequestHeader('Token');

//Lookup to the AuthDE for Token context
var authLU = Platform.Function.LookupRows('Auth_HashToken', ['HashAuth'], [token]);

//conditional to verify if token is still active or expired
if (authLU.length > 0) {
  var authCreateDate = authLU[0].CreationDate  //date token was created
  var expireTime = 30;  //Minutes token is active for
  var now = new Date();  //Current date and time
  var expiryDate = new Date(authCreateDate.getTime() + expireTime*60000);  //expiry date of token - now + 30 minutes
  var pass = expiryDate > now ? 1 : 0; //verify if token is expired by compare now to expiry date of token
} else {
  var pass = 1;  // default of 'expired' if no record in authDE
}

REST Integration Information

This Data Extension will be used to house the credentials associated with each MID that can be passed in. Now, again this is not exactly the most secure way of doing this – so you may need to adjust this part to better fit your security policies, but for the sake of an example, I will be implementing it in this fashion.

DE Name: REST_Credentials

Name          |  Data Type  |  Length  |  Primary Key  |  Nullable  |  Default Value  |
MID           |  Number     |          |  Y            |  N         |                 |
clientID      |  Text       |   500    |  N            |  Y         |                 |
clientSecret  |  Text       |   500    |  N            |  Y         |                 |
subDomain     |  Text       |   500    |  N            |  Y         |                 |

You can then interact directly with this Data Extension to get the appropriate REST credentials based on the passed MID without the user needing to have access to or providing them.

    //grab authentication info from REST token DE    
    //this is a simple sample, may require different process/security precautions for you to implement
    var luDE = DataExtension.Init('REST_Credentials');
    var lu = luDE.Rows.Lookup(['MID'], [mid])

    //verifies data is returned and if so, grabs the returned information from the returned row
    if(lu.length > 0) {
        var mid = String(lu[0].MID).replace(/[\n\r\t]/g,""),
            clientId = String(lu[0].clientID).replace(/[\n\r\t]/g,""),
            clientSecret = String(lu[0].clientSecret).replace(/[\n\r\t]/g,""),
            subDomain = String(lu[0].subDomain).replace(/[\n\r\t]/g,""),
            authURL = 'https://' + subDomain + '.auth.marketingcloudapis.com/',
            restBase = 'https://' + subDomain + '.rest.marketingcloudapis.com/',
            version = 2

        // grabs the auth token for the specified account based on data from lookup of MID
        var authToken = 'Bearer ' + generateToken(clientId, clientSecret, mid, authURL, version);
    } else {
      Write('{"ERROR": "Authorization is not available. Please add into the reference data"}')
    }

Code Resource Cloud Page

In order to have this API endpoint available, it needs to be hosted publically somewhere. To allow for this without eating through your Super Messages, I would recommend building this as a Code Resource (JSON) Cloud Page inside of Web Studio.

As a note, you can also use the Classic Content Microsite or Landing Page options if you prefer but keep in mind there may be differences or changes to process to have this function as expected there compared to the Cloud Page process described here.

You would then copy/paste in the following code (with changes for your DEs or your own custom authentication process) into the page to create the endpoint.

<script runat=server>
Platform.Load("Core","1.1.1");
//---------------------------------------------------------//
//---------------------------------------------------------//

// CUSTOM API ENDPOINT TO LOOK UP ROWSETS FROM DEs

// REQUIRES:
//  - MID location of Data Extension
//  - Data Extension External Key
//  - REST Integration information stored inside of a Data Extension in host BU

//NOTE: This will not work on shared DEs outside of looking in the Parent account that they are hosted in.

//---------------------------------------------------------//
//---------------------------------------------------------//

//set Dev environment for debugging and troubleshooting (1: ON | 0: OFF)
var dev = 0;
var debug = 0;


var token = Platform.Request.GetRequestHeader('Token');
var authLU = Platform.Function.LookupRows('Auth_HashToken', ['HashAuth'], [token]);

if (authLU.length > 0) {
  var authStatus = authLU[0].Status;
  var authCreateDate = authLU[0].CreationDate
  var expireTime = 30;
  var now = new Date();
  var expiryDate = new Date(authCreateDate.getTime() + expireTime*60000);
  var pass = expiryDate > now ? 1 : 0;
} else {
  var pass = 1;
}


//------------------------------------------------------//

// Validates the call is coming from an authorized user
// Token creation/assignment is done prior to this call
// authDE is holding the authentication tokens
// this DE will have a data retention timeline on it to limit life of the token
// there are many ways to handle this, feel free to adjust to whatever auth/security you like best

//-------------------------------------------------------//

pass = dev ? 1 : 0;

debug? Write(pass + '\n') : '';

if (pass) {

  //Gathers the data POSTed to the endpoint and turns it into a JSON
  var postStr  = Platform.Request.GetPostData();
  var postJSON = Platform.Function.ParseJSON(postStr);

  debug ? Write('postStr: ' + postStr + '\n') : '';
  debug ? Write('postJSON: ' + Stringify(postJSON)+ '\n') : '';

  if(postJSON) {
    //Gathers the values of the properties passed in payload
    var mid = postJSON.mid,
        deKey = postJSON.externalKey,
        filter = postJSON.filter,       //Optional
        pageSize = postJSON.pageSize,   //Optional (max 2500)
        page = postJSON.page;           //Optional

    if(!mid) { 
      Write('{"ERROR": "Missing MID"}')
      var fail = 1; 
    } else if(!deKey) {
      Write('{"ERROR": "Missing Data Extension Key"}') 
      var fail = 1;
    } else if(mid > 2147483647) {
      Write('{"ERROR": "Incorrect MID provided"}')
      var fail = 1;
    }

    // Error checking to output Error Object if missing
    // a required property from the payload

  } else {
    Write('{"ERROR": "Missing payload"}')
    var fail = 1;
  }


  /****************************************

  Example Payload:

  {
    mid: 123456,
    externalKey: "myDataExtension_Key",
    filter: "ZIP eq 12345 or ZIP eq 22222",
    pageSize: 500,
    page: 1
  }

  ****************************************/


  if(!fail) {

    //transitory array/object vars
    var arr = [],
        obj = {};


    //grab authentication info from REST token DE    
    //this is a simple sample, may require different process/security precautions for you to implement
    var luDE = DataExtension.Init('REST_Credentials');
    var lu = luDE.Rows.Lookup(['MID'], [mid])

    debug ? Write('lu: ' + Stringify(lu) + '\n') : '';
    debug ? Write('lu.length: ' + lu.length + '\n') : '';

    if(lu.length > 0) {
        var mid = String(lu[0].MID).replace(/[\n\r\t]/g,""),
            clientId = String(lu[0].clientID).replace(/[\n\r\t]/g,""),
            clientSecret = String(lu[0].clientSecret).replace(/[\n\r\t]/g,""),
            subDomain = String(lu[0].subDomain).replace(/[\n\r\t]/g,""),
            authURL = 'https://' + subDomain + '.auth.marketingcloudapis.com/',
            restBase = 'https://' + subDomain + '.rest.marketingcloudapis.com/',
            version = 2

        debug ? Write('mid: ' + mid + '\n') : '';
        debug ? Write('clientId: ' + clientId + '\n') : '';
        debug ? Write('clientSecret: ' + clientSecret + '\n') : '';
        debug ? Write('subDomain: ' + subDomain + '\n') : '';
        debug ? Write('authURL: ' + authURL + '\n') : '';
        debug ? Write('restBase: ' + restBase + '\n') : '';

        // grabs the auth token for the specified account
        var authToken = 'Bearer ' + generateToken(clientId, clientSecret, mid, authURL, version);

        // use auth token to make call to REST endpoint to get rowsets
        var data = getData(restBase,authToken,deKey,filter,page,pageSize) ;
        debug ? Write('data: ' + Stringify(data) + '\n') : '';

        var res = {};
        if(data.hasOwnProperty('message')) {
          res.ERROR = data.message;
        } else {
          res.customObjectId  = data.customObjectId;
          res.customObjectKey = data.customObjectKey;
          res.pageSize        = data.pageSize;
          res.page            = data.page;
          res.count           = data.count;

          var items = data.count > 0 ? data.items : '';
          items ? res.items = items : '';
        }
        Write(Stringify(res));
    } else {
      Write('{"ERROR": "Authorization is not available. Please add into the reference data"}')
    }

  }

} else {
  Write('{"ERROR": "Token is invalid."}')
}

function getData(restBase,authToken,deKey,filter,page,pageSize) {

    var fArr = []
    var url = restBase + '/data/v1/customobjectdata/key/' + deKey + '/rowset' 
    filter ? fArr.push('$filter=' + filter) : '';
    page ? fArr.push('$page=' + page) : '';
    !page && pageSize ? fArr.push('$page=1') : '';
    pageSize ? fArr.push('$pageSize=' + pageSize) : '';

    for(e=0;e<fArr.length;e++) {
      url += e == 0 ? '?' + fArr[e] : '&' + fArr[e];
    }

    debug ? Write('url: ' + url + '\n') : '';

    var req = new Script.Util.HttpRequest(url);
    req.emptyContentHandling = 0;
    req.retries = 2;
    req.continueOnError = true;
    req.contentType = "application/json"
    req.setHeader("Authorization", authToken);
    req.method = "GET";

    var resp = req.send();
    var resultStr = String(resp.content);
    var resultJSON = Platform.Function.ParseJSON(String(resp.content));

    return resultJSON;
}

function generateToken(clientId, clientSecret, mid, authURL, version) {

  if (version == 2) {
    var versionEndpoint = '/v2/token'

    var authJSON = {
  "grant_type": "client_credentials",
  "client_id": clientId,
  "client_secret": clientSecret,
  "account_id": mid
  }
  } else {
    var versionEndpoint = '/v1/requestToken'

 //URL changes for V1 endpoints

      var authJSON = {
    "clientId": clientId,
    "clientSecret": clientSecret
  }
  }

  var authUrl = authURL + versionEndpoint;
  var contentType = 'application/json';
  var authPayload = Platform.Function.Stringify(authJSON);

  var accessTokenResult = HTTP.Post(authUrl, contentType, authPayload);

  var statusCode = accessTokenResult["StatusCode"];
  var response = accessTokenResult["Response"][0];

  if(version == 2) {
      var accessToken = Platform.Function.ParseJSON(response).access_token;
  } else {
      var accessToken = Platform.Function.ParseJSON(response).accessToken;
  }

  return accessToken;
}

</script>

Please do note that new pages created in Cloudpages (especially with Code Resources) publishing them can take a few minutes for it to propagate out to the servers to be displayed, so do not worry if you are getting a 500 error initially as this is an expected delay.

Making the API call

For this example, I am going to describe how to make the call via POSTman. You will need to translate this into your own process/tool as there are too many for me to show examples in each. Below is the code sample that is used to make the call:

POST /{{myCodeResourceURL}}
Host: {{myDomain}}}
Token: xxxxxxxx-xxxx-428c-xxxx-12e3axxxxxxxx
Content-Type: application/json

  {
    mid: {{my MID}},
    externalKey: "myDataExtension_Key",
    filter: "ZIP eq 12345 or ZIP eq 22222",
    pageSize: 500,
    page: 1
  }

To break this down,

  • You will want to put The ‘Host’ and ‘POST’ sections together into the URL bar ({{myDomain}}/{{myCodeResourceURL}} or ‘https://pub.myETDomain.com/abc123xyz789’
  • The ‘method’ used is required to be a POST. This is usually shown to the left of where you enter the URL in a dropdown.
  • Inside the Headers section, you will want to add in the ‘Token’ header and value here as well as ensure the ‘Content-Type’ is listed as ‘application/json’.
  • You then will want to select ‘raw’ inside of the Body section and then JSON from the dropdown. You then would insert the JSON (with your changes) into this section.

Now, if you gathered the correct Token and passed it along (to authenticate) you should get a response similar to:

{
    "customObjectId": "xxxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxxxxxx",
    "customObjectKey": "myDataExtension_Key",
    "pageSize": 500,
    "page": 1,
    "count": 3,
    "items": [
        {
            "keys": {
                "subscriberkey": "example1"
            },
            "values": {
                "emailaddress": "[email protected]",
                "firstname": "Gor",
                "lastname": "Tonington",
                "region": "East"
            }
        },
        {
            "keys": {
                "subscriberkey": "example2"
            },
            "values": {
                "emailaddress": "[email protected]",
                "firstname": "G.",
                "lastname": "Gifford",
                "region": "West"
            }
        },
        {
            "keys": {
                "subscriberkey": "example3"
            },
            "values": {
                "emailaddress": "[email protected]",
                "firstname": "G",
                "lastname": "Gifford",
                "region": "North"
            }
        }
    ]
}

or if something is incorrect, it should send a response like:

{
    "ERROR": "custom object data cannot be retrieved for key: myDataExtension_Key"
}

Hopefully this helps get you started on some custom endpoints – whether to make your life easier or to allow for better separation of your vendors from your API ID/Secret, etc.

Tags: , , , , , , , , , , , , , , , , , , , , , , , , , , ,
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments