Skip to main content
Version: 2021-04-15

Code Mode

Code Mode lets you write JavaScript to fully customize an OAuth request when the form builder isn't flexible enough. Instead of filling in fields for the URL, headers, params, and body, you write a short script that builds the request (or the authorization URL), runs it, and returns the result.

Reach for Code Mode when you need to do things the form can't express, such as:

  • Building a request signature or hash (HMAC, SHA-256).
  • Assembling a non-standard request body or dynamic query string.
  • Parsing or reshaping the provider's response before HighLevel stores it.
  • Conditional logic that depends on the user's input or the provider's response.

You can switch any supported request between form mode and Code Mode at any time using Switch to Code Mode / Switch to Form Mode.

Which requests support Code Mode

Code Mode is available on the following OAuth 2.0 requests:

StepInternal nameWhat your script returns
Authorization URLauthorizationUrlA URL string to send the user to.
Access token requestaccessTokenRequestThe token response object (access_token, refresh_token, ...).
Refresh token requestrefreshTokenRequestThe refreshed token response object.
Test API endpointtestRequestThe HTTP response (used to confirm the token works).
Fetch user infouserInfoRequestThe user/account info response (then field-mapped).

Switching to Code Mode

When you click Switch to Code Mode, HighLevel pre-fills the editor with a starter template for that step. The template mirrors a typical request so you can edit it rather than start from scratch.

The handlebars template variables you use in form mode ({{externalApp.clientId}}, {{bundle.code}}, etc.) are not used in Code Mode. Instead you access the same data through the JavaScript objects described below (process.env, bundle).

The execution environment

Your script runs inside a secure, isolated JavaScript sandbox. The following globals are available.

bundle

Holds the current authentication state and inputs for the step.

bundle.inputData   // inputs for this step (see per-step table below)
bundle.authData // the stored token data (on refresh / user info steps)

bundle.inputData contains, depending on the step:

PropertyAvailable inDescription
stateauthorization, access tokenThe OAuth state value generated by HighLevel.
redirect_uriauthorization, access tokenThe OAuth redirect URL.
codeaccess tokenThe authorization code returned by your provider.
code_verifieraccess token (PKCE)The PKCE code verifier (also auto-attached to the request body for you).
<your field keys>allAny values the user entered for your configured fields.

bundle.authData contains the stored token data, used when refreshing or fetching user info:

bundle.authData.access_token
bundle.authData.refresh_token
// plus any other top-level fields returned by your token response

process.env

A frozen, read-only object with your app credentials:

process.env.CLIENT_ID      // your OAuth Client ID
process.env.CLIENT_SECRET // your OAuth Client Secret (empty when using PKCE only)
process.env.SCOPES // the configured scopes

z - the helper namespace

A frozen object with everything you need to make requests and work with data.

z.request(options)

Makes an outbound HTTP request and returns a promise resolving to the response. Options:

const options = {
url: 'https://api.example.com/oauth/token',
method: 'POST', // GET, POST, PUT, PATCH, DELETE, HEAD
headers: { 'content-type': 'application/x-www-form-urlencoded' },
params: { /* query string params */ },
body: { /* request body */ },
removeMissingValuesFrom: { // drop empty params/body keys
body: false,
params: false
}
}

The resolved response has this shape:

{
status, // HTTP status code (number)
headers, // response headers
content, // raw response body as a string
data, // parsed body (object if JSON, otherwise the raw value)
json // the JSON-parsed body (or the raw value if not JSON)
}

When content-type is application/x-www-form-urlencoded, the body object is automatically form-encoded for you.

z.console

Logging that appears in the tester's Logs tab. Use it to debug your script.

z.console.log('exchanging code', bundle.inputData.code)

A plain console.log(...) also works and writes to the same place.

z.errors.RefreshAuthError

Throw this from the access token or refresh token step to signal that the credentials are no longer valid and a refresh/re-auth is required.

throw new z.errors.RefreshAuthError('Token rejected by provider')

z.require(...)

A small set of built-in helpers is available:

const qs = z.require('querystring')   // stringify / parse / escape / unescape
const crypto = z.require('crypto') // createHash(algo).update(v).digest(enc), randomBytes(n).toString(enc)
const sig = z.require('crypto').createHash('sha256').update(raw).digest('hex')
const query = z.require('querystring').stringify({ a: 1, b: 2 }) // "a=1&b=2"

Other modules are not available and will throw an error.

z.JSON is also exposed (equivalent to the standard JSON object).

What your script must return

  • Authorization URL step - return a URL string. It must be a valid https URL.
  • All other steps - return z.request(options) (or a promise that resolves to the response/parsed object). You can parse or reshape the response in a .then() before returning it.

Examples

Authorization URL

const url = `https://example.com/oauth/authorize?client_id=${process.env.CLIENT_ID}&state=${bundle.inputData.state}&redirect_uri=${encodeURIComponent(bundle.inputData.redirect_uri)}&response_type=code`;

return url;

Access token request

const options = {
url: 'https://api.example.com/oauth/token',
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'accept': 'application/json'
},
params: {},
body: {
'code': bundle.inputData.code,
'client_id': process.env.CLIENT_ID,
'client_secret': process.env.CLIENT_SECRET,
'grant_type': 'authorization_code',
'redirect_uri': bundle.inputData.redirect_uri
},
removeMissingValuesFrom: { 'body': false, 'params': false }
}

return z.request(options)
.then((response) => {
const results = response.json;
// Reshape results here if your provider's response differs.
return results;
});

Refresh token request

const options = {
url: 'https://api.example.com/oauth/token',
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'accept': 'application/json'
},
params: {},
body: {
'refresh_token': bundle.authData.refresh_token,
'grant_type': 'refresh_token'
},
removeMissingValuesFrom: { 'body': false, 'params': false }
}

return z.request(options).then((response) => response.json);

Test request

const options = {
url: 'https://api.example.com/me',
method: 'GET',
headers: {},
params: {},
removeMissingValuesFrom: { 'body': false, 'params': false }
}

return z.request(options).then((response) => response.json);

Fetch user info

const options = {
url: 'https://api.example.com/me',
method: 'GET',
headers: {
'authorization': `Bearer ${bundle.authData.access_token}`,
'accept': 'application/json'
},
params: {},
removeMissingValuesFrom: { 'body': false, 'params': false }
}

return z.request(options).then((response) => response.json);

The returned object is then field-mapped using the ID / Name / Email paths you configure in Multi-Account Support & User Info.

Limits & restrictions

Code Mode runs in a sandbox with guardrails to keep it safe and fast:

  • HTTPS only - all z.request() calls must use https:// URLs.
  • No internal/private networks - requests to localhost, private IP ranges, and internal hostnames are blocked.
  • Execution timeout - scripts that run too long (around 25 seconds) are aborted.
  • Size limits - there are limits on script size and on the size of the response a request may return.
  • Limited modules - only querystring and crypto (via z.require) plus standard JavaScript built-ins are available.
  • Restricted headers - certain internal headers are not allowed and will cause an error.

Secret redaction

To protect your credentials, sensitive values - client secrets, access/refresh tokens, authorization codes, the Authorization header, cookies - are automatically redacted from the logs and request/response details shown in the tester. You'll see [REDACTED] in their place.

Debugging

Use Step 3 - Test your auth to run your script against real values. The tester's Logs tab shows your z.console output (grouped by step), and the HTTP Details tab shows the outbound request and the response. If a script errors or times out, the error message and captured logs are returned so you can fix it.

Save your configuration before testing - the tester uses the most recently saved version.