# Code Mode

Source: https://marketplace.gohighlevel.com/docs/2021-04-15/oauth/external-auth/CodeMode

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](https://marketplace.gohighlevel.com/docs/oauth/external-auth/OAuth2) 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.

```js
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](https://marketplace.gohighlevel.com/docs/oauth/external-auth/ConfigureYourFields). |

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

```js
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:

```js
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:

```js
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:

```js
{
  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.

```js
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.

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

#### `z.require(...)`

A small set of built-in helpers is available:

```js
const qs = z.require('querystring')   // stringify / parse / escape / unescape
const crypto = z.require('crypto')    // createHash(algo).update(v).digest(enc), randomBytes(n).toString(enc)
```

```js
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

```js
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

```js
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

```js
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

```js
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

```js
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](https://marketplace.gohighlevel.com/docs/oauth/external-auth/MultiAccountSupport).

## 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.
