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:
| Step | Internal name | What your script returns |
|---|---|---|
| Authorization URL | authorizationUrl | A URL string to send the user to. |
| Access token request | accessTokenRequest | The token response object (access_token, refresh_token, ...). |
| Refresh token request | refreshTokenRequest | The refreshed token response object. |
| Test API endpoint | testRequest | The HTTP response (used to confirm the token works). |
| Fetch user info | userInfoRequest | The 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:
| Property | Available in | Description |
|---|---|---|
state | authorization, access token | The OAuth state value generated by HighLevel. |
redirect_uri | authorization, access token | The OAuth redirect URL. |
code | access token | The authorization code returned by your provider. |
code_verifier | access token (PKCE) | The PKCE code verifier (also auto-attached to the request body for you). |
<your field keys> | all | Any 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-typeisapplication/x-www-form-urlencoded, thebodyobject 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 -
returna URL string. It must be a validhttpsURL. - 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 usehttps://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
querystringandcrypto(viaz.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.