GetJWT
Generates a JSON Web Token signed with an inline secret. Runtime-proven on a live Marketing Cloud Engagement CloudPage — including the fact that the algorithm name is matched case-insensitively and the payload is never validated as JSON.
Syntax
GetJWT(secret, algorithm, jsonPayload) → string
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
secret |
string | Yes | Secret used to sign the token; the empty string aborts the page |
algorithm |
string | Yes | HMAC algorithm name — HS256, HS384 or HS512, matched case-insensitively |
jsonPayload |
string | Yes | Payload to encode, copied into the token untouched |
Example
%%[
VAR @secret, @payload, @token
SET @secret = "sfmc-probe-secret-2026"
SET @payload = '{"sub":"probe","n":7}'
SET @token = GetJWT(@secret, "HS256", @payload)
]%%
%%=v(@token)=%%
Renders eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwcm9iZSIsIm4iOjd9.308Bu1Ma_gFb0_8RNujndwB8xJh6n3myT7xZ3h7BNhE.
The usual reason to reach for it is handing a receiving system something it can check for tampering, most often on a link:
%%[
VAR @claims, @token
SET @claims = Concat('{"email":"', AttributeValue("EmailAddress"), '","iat":"', Now(), '"}')
SET @token = GetJWT(@secret, "HS256", @claims)
]%%
<a href="https://example.org/preferences?t=%%=v(@token)=%%">Update your preferences</a>
The secret is written into the page source, so anyone who can read the page can mint tokens. Where Key Management is available, GetJWTByKeyName keeps the secret out of the code.
Return value
string — the token as three Base64url segments joined by dots: the encoded header, the encoded payload, and the signature.
There is no closed set of sentinel values to test for. Every accepted call returns a token, and every rejected one aborts the page instead of returning an error value — so the result can never be checked defensively after the fact.
Behaviour
The token matches an independent implementation byte for byte. For the payload {"sub":"probe","n":7} and the secret above, the page returned exactly the value computed outside Marketing Cloud with a standard HMAC-SHA256, 109 characters long. The same held for HS384 and HS512.
The header is generated from the algorithm argument and nothing else. Decoding the first segment on the page gave {"alg":"HS256","typ":"JWT"} — no key id, no extra claims.
Signing is deterministic. Two calls with identical arguments in the same render produced the identical token, compared on the page rather than by eye.
The encoding is Base64url, not standard Base64
Searching the token found no =, no + and no /, and an _ at position 75. The segments therefore use the URL-safe alphabet and carry no padding, which is what lets a token travel in a query string untouched. Do not run the segments through a standard Base64 decoder without translating - and _ back first.
The algorithm name is matched case-insensitively
Passing hs256 produced exactly the HS256 token, header included — the decoded header still reads "alg":"HS256". No source mentions this. Anything outside the three HMAC names is refused outright: RS256 and the invented HS999 each aborted the page with HTTP 422, as did an empty secret.
The payload is never parsed
Passing the plain string not json at all produced a valid token whose middle segment is simply the Base64url of that string. Nothing validates the payload as JSON, so a malformed payload ships silently and fails only at the receiving end.
Show test script
%%[
VAR @b, @sec, @pl, @t1, @t2, @p1, @rest, @p2
SET @b = RequestParameter("b")
SET @sec = "sfmc-probe-secret-2026"
SET @pl = '{"sub":"probe","n":7}'
/* known-good control: renders on every request, so a run of HTTP 422s
can be told apart from a broken deploy */
OutputLine(Concat("CTRL=[", Base64Encode("Man"), "]"))
/* the token for a fixed payload and secret, its three segments, the
decoded header, the base64url alphabet, and determinism */
IF @b == "safe" THEN
OutputLine(Concat("--- safe start ---"))
SET @t1 = GetJWT(@sec, "HS256", @pl)
OutputLine(Concat("T1=[", @t1, "]"))
OutputLine(Concat("T1LEN=[", Length(@t1), "]"))
SET @t2 = GetJWT(@sec, "HS256", @pl)
IF @t1 == @t2 THEN
OutputLine(Concat("DETERMINISTIC=[yes]"))
ELSE
OutputLine(Concat("DETERMINISTIC=[no]"))
ENDIF
SET @p1 = IndexOf(@t1, ".")
SET @rest = Substring(@t1, Add(@p1, 1), Subtract(Length(@t1), @p1))
SET @p2 = IndexOf(@rest, ".")
OutputLine(Concat("SEG1=[", Substring(@t1, 1, Subtract(@p1, 1)), "]"))
OutputLine(Concat("SEG2=[", Substring(@rest, 1, Subtract(@p2, 1)), "]"))
OutputLine(Concat("SEG3=[", Substring(@rest, Add(@p2, 1), Subtract(Length(@rest), @p2)), "]"))
OutputLine(Concat("HEADER=[", Base64Decode(Substring(@t1, 1, Subtract(@p1, 1))), "]"))
OutputLine(Concat("HAS_EQ=[", IndexOf(@t1, "="), "] HAS_PLUS=[", IndexOf(@t1, "+"), "] HAS_SLASH=[", IndexOf(@t1, "/"), "]"))
OutputLine(Concat("HAS_USCORE=[", IndexOf(@t1, "_"), "]"))
OutputLine(Concat("--- safe done ---"))
ENDIF
/* the two longer HMAC variants */
IF @b == "hs384" THEN
OutputLine(Concat("--- hs384 start ---"))
OutputLine(Concat("T=[", GetJWT(@sec, "HS384", @pl), "]"))
OutputLine(Concat("--- hs384 done ---"))
ENDIF
IF @b == "hs512" THEN
OutputLine(Concat("--- hs512 start ---"))
OutputLine(Concat("T=[", GetJWT(@sec, "HS512", @pl), "]"))
OutputLine(Concat("--- hs512 done ---"))
ENDIF
/* the algorithm name is matched case-insensitively */
IF @b == "lower" THEN
OutputLine(Concat("--- lower start ---"))
OutputLine(Concat("T=[", GetJWT(@sec, "hs256", @pl), "]"))
OutputLine(Concat("--- lower done ---"))
ENDIF
/* the payload is never parsed - a plain string is signed as readily */
IF @b == "nonjson" THEN
OutputLine(Concat("--- nonjson start ---"))
OutputLine(Concat("T=[", GetJWT(@sec, "HS256", "not json at all"), "]"))
OutputLine(Concat("--- nonjson done ---"))
ENDIF
/* each of the three branches below aborts the page - fetch alone */
IF @b == "rs256" THEN
OutputLine(Concat("--- rs256 start ---"))
OutputLine(Concat("T=[", GetJWT(@sec, "RS256", @pl), "]"))
ENDIF
IF @b == "bogus" THEN
OutputLine(Concat("--- bogus start ---"))
OutputLine(Concat("T=[", GetJWT(@sec, "HS999", @pl), "]"))
ENDIF
IF @b == "emptysecret" THEN
OutputLine(Concat("--- emptysecret start ---"))
OutputLine(Concat("T=[", GetJWT("", "HS256", @pl), "]"))
ENDIF
]%%
A bare string literal passed to OutputLine renders an empty line while the page still returns HTTP 200, so the marker silently vanishes and the block looks like a function that produced no output. Always wrap it — OutputLine(Concat("--- safe start ---")) — even for a single argument.
A wrong argument count aborts AMPscript at compile time, so it takes down every branch on the page — including the control block and branches that were never requested. Keep arity checks out of the gated behaviour harness and give each one its own deployment, or a whole run returns uninformative HTTP 422s.
Availability
| Platform | Available |
|---|---|
| Marketing Cloud Engagement | Yes |
| Marketing Cloud Next | No |
See also
- Base64Encode — the padded, non-URL-safe encoding the token segments deliberately avoid
- EncryptSymmetric — when the payload itself must stay unreadable; a token only proves it was not altered
- The algorithm name is case-insensitive
- The payload is not validated as JSON
- Official reference · ampscript.guide