OAuth2 Client Credentials Flow

function dateAdd(timestamp,intervalToAdd,intervalType) {
    Platform.Variable.SetValue("@dateAdd_ts",timestamp);
    Platform.Variable.SetValue("@dateAdd_add",intervalToAdd);
    Platform.Variable.SetValue("@dateAdd_type",intervalType);
    return Platform.Function.TreatAsContent("%%=DateAdd(@dateAdd_ts, @dateAdd_add, @dateAdd_type)=%%");
}
function formatDate(dateString,dateFormat,timeFormat,isoLocale) {
    Platform.Variable.SetValue("@formatDate_string",dateString);
    Platform.Variable.SetValue("@formatDate_date",dateFormat);
    Platform.Variable.SetValue("@formatDate_time",timeFormat);
    Platform.Variable.SetValue("@formatDate_iso",isoLocale);
    return Platform.Function.TreatAsContent("%%=FormatDate(@formatDate_string, @formatDate_date, @formatDate_time, @formatDate_iso)=%%");
}

function getOAuthToken(authUrl, clientId, clientSecret) {
    // Check cached token in DE
    var cached = Platform.Function.LookupRows("TokenCache",
        "service", "oauth_" + clientId);
    if (cached && cached.length > 0) {
        var row = cached[0];
        var expiry = new Date(row.expires);
        if (expiry > new Date()) {
            return row.token;
        }
    }

    // Fetch new token
    var payload = Platform.Function.Stringify({
        grant_type: "client_credentials",
        client_id: clientId,
        client_secret: clientSecret
    });
    // Platform.Function.HTTPPost hands back the HTTP status code, never the body,
    // so the token request has to go through Script.Util.HttpRequest instead.
    var req = new Script.Util.HttpRequest(authUrl);
    req.method = "POST";
    req.contentType = "application/json";
    req.postData = payload;
    var resp = req.send();

    // statusCode is a CLR value — Number() converts it so === works
    var status = Number(resp.statusCode);
    var tokenData = Platform.Function.ParseJSON(String(resp.content) + "");

    if (status !== 200 || !tokenData || !tokenData.access_token) {
        throw new Error("Token fetch failed with status " + status);
    }

    // Cache token (expire a minute early for safety). DateAdd has no seconds unit —
    // only Y, M, D, H and MI — so convert the expires_in seconds to whole minutes.
    var lifetimeMinutes = Math.floor((tokenData.expires_in - 60) / 60);
    var expiresAt = formatDate(
        dateAdd(
            Platform.Function.Now(),
            lifetimeMinutes,
            "MI"
        ),
        "MM/DD/YYYY","HH:mm:ss"
    );
    Platform.Function.UpsertData("TokenCache",
        ["service"], ["oauth_" + clientId],
        ["token", "expires"],
        [tokenData.access_token, expiresAt]
    );

    return tokenData.access_token;
}

Show test script
<script runat="server">
/*
 * Chapter: OAuth2 Client Credentials Flow
 * Proves:
 *   1. HttpRequest constructs; ContentType settable.
 *   NON-ASSERTABLE: live token fetch.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var req = new Script.Util.HttpRequest("https://example.com/v2/token");
req.method = "POST";
req.contentType = "application/json";
assert("method POST", "" + req.method, "POST");
assert("HttpRequest object", typeof req, "clr");
</script>

SFMC REST API — Call Journey Entry Event

var authUrl = "https://SUBDOMAIN.auth.marketingcloudapis.com/v2/token";
var apiBase = "https://SUBDOMAIN.rest.marketingcloudapis.com";
var clientId = Platform.Function.Lookup("SFMCConfig", "value", "key", "clientId");
var clientSecret = Platform.Function.Lookup("SFMCConfig", "value", "key", "clientSecret");

var token = getOAuthToken(authUrl, clientId, clientSecret);

var payload = {
    DefinitionKey: "JourneyDefinitionKey_Here",
    ContactKey: subscriberKey,
    EventDefinitionKey: "APIEvent-abc123",
    Data: {
        EmailAddress: email,
        FirstName: firstName,
        ProductId: productId
    }
};

var req = new Script.Util.HttpRequest(apiBase + "/interaction/v1/events");
req.method = "POST";
req.contentType = "application/json";
req.setHeader("Authorization", "Bearer " + token);
req.postData = Platform.Function.Stringify(payload);

var resp = req.send();
var result = Platform.Function.ParseJSON(String(resp.content) + "");

// statusCode is a CLR value — Number() converts it so === works
var status = Number(resp.statusCode);
if (status === 201) {
    Write(Platform.Function.Stringify({ status: "entered", key: subscriberKey }));
} else {
    throw new Error("Journey entry failed: " + status + " " + Platform.Function.Stringify(result));
}

Show test script
<script runat="server">
/*
 * Chapter: SFMC REST API Journey Entry
 * Proves:
 *   1. Stringify event payload shape.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var payload = { ContactKey: "sk", EventDefinitionKey: "APIEvent-x", Data: { a: 1 } };
var s = Platform.Function.Stringify(payload);
assert("has ContactKey", s.indexOf("ContactKey") >= 0 ? "true" : "false", "true");
</script>

Generic REST Helper

function apiCall(method, url, token, body) {
    var req = new Script.Util.HttpRequest(url);
    req.method = method;
    req.setHeader("Authorization", "Bearer " + token);
    req.setHeader("Accept", "application/json");

    if (body) {
        req.contentType = "application/json";
        req.postData = Platform.Function.Stringify(body);
    }

    var resp = req.send();
    var data = Platform.Function.ParseJSON(String(resp.content) + "");

    // statusCode is a CLR value — Number() converts it to a real JavaScript number
    var status = Number(resp.statusCode);
    return {
        status: status,
        ok: status >= 200 && status < 300,
        data: data
    };
}

// Usage
var result = apiCall("GET", apiBase + "/contacts/v1/contacts/" + contactKey, token, null);
if (!result.ok) {
    throw new Error("API error: " + result.status);
}
var contactData = result.data;

Show test script
<script runat="server">
/*
 * Chapter: Generic REST Helper
 * Proves:
 *   1. Helper builds HttpRequest without send.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
function apiCall(method, url, token, body) {
    var req = new Script.Util.HttpRequest(url);
    req.method = method;
    req.contentType = "application/json";
    if (token) req.setHeader("Authorization", "Bearer " + token);
    // postData is write-only — assignment is fine; never read it (unguarded read → HTTP 422)
    if (body) req.postData = Platform.Function.Stringify(body);
    return req;
}
var r = apiCall("POST", "https://example.com/", "tok", { x: 1 });
assert("apiCall returns clr", typeof r, "clr");
assert("setHeader exists", typeof r.setHeader, "clrmethodinfo");
assert("method POST", "" + r.method, "POST");
</script>

External API with Retry

function callWithRetry(url, method, payload, token, maxRetries) {
    maxRetries = maxRetries || 3;
    var lastError = null;

    for (var attempt = 0; attempt < maxRetries; attempt++) {
        try {
            var req = new Script.Util.HttpRequest(url);
            req.method = method;
            req.setHeader("Authorization", "Bearer " + token);
            if (payload) {
                req.contentType = "application/json";
                req.postData = Platform.Function.Stringify(payload);
            }
            var resp = req.send();

            // statusCode is a CLR value — Number() converts it so === works
            var status = Number(resp.statusCode);
            if (status === 429) {
                // Rate limited — wait and retry
                // (SSJS has no sleep, so we skip back-off and just retry)
                continue;
            }

            return { status: status, body: String(resp.content) };
        } catch(e) {
            lastError = e;
        }
    }
    throw lastError || new Error("Max retries exceeded for " + url);
}
Show test script
<script runat="server">
/*
 * Chapter: External API with Retry
 * Proves:
 *   1. Retry loop counter pattern.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
function shouldRetry(statusCode, attempt, maxRetries) {
    return attempt < maxRetries && (statusCode >= 500 || statusCode === 429);
}
assert("retry 500", shouldRetry(500, 0, 3) ? "true" : "false", "true");
assert("no retry 400", shouldRetry(400, 0, 3) ? "true" : "false", "false");
assert("exhausted", shouldRetry(500, 3, 3) ? "true" : "false", "false");
</script>

See Also