# User Context in Marketplace Apps

Source: https://marketplace.gohighlevel.com/docs/2021-07-28/other/user-context-marketplace-apps

HighLevel provides a secure mechanism for accessing authenticated user information through signed tokens. This guide explains how you can generate and use `Shared Secret` key to access user context in secured manner.

## Setting Up Shared Secret

### Generating a Shared Secret Key

First you'll need to generate a Shared Secret key for your application:

1. Navigate to your application's Advanced Settings
2. Go to the Auth section
3. Under Shared Secret, click the Generate button to create your Shared Secret key

  [image: Shared Secret Key Generation]

## Frontend Implementation Methods

There are two ways in which you can access this data in your frontend, depending on where your code runs:

### 1. Custom JavaScript Implementation

If you're using custom JavaScript injected into HighLevel pages, use the `exposeSessionDetails` method:

```javascript
async function getUserData() {
  try {
    // APP_ID is your application's unique identifier
    const encryptedUserData = await window.exposeSessionDetails(APP_ID);

    // Send this encrypted data to your backend for decryption
    const response = await fetch("your-backend-endpoint", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ encryptedData: encryptedUserData }),
    });

    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error("Failed to fetch session details:", error);
    throw error;
  }
}
```

### 2. Custom Pages Implementation

If you're trying to get user context in custom page, use the `postMessage` method to communicate with the parent window:

```javascript
async function getUserData() {
  try {
    const encryptedUserData = await new Promise((resolve) => {
      // Request user data from parent window
      window.parent.postMessage({ message: "REQUEST_USER_DATA" }, "*");

      // Listen for the response
      const messageHandler = ({ data }) => {
        if (data.message === "REQUEST_USER_DATA_RESPONSE") {
          window.removeEventListener("message", messageHandler);
          resolve(data.payload);
        }
      };

      window.addEventListener("message", messageHandler);
    });

    // Send encrypted data to your backend for decryption
    const response = await fetch("your-backend-endpoint", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ encryptedData: encryptedUserData }),
    });

    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error("Failed to fetch user data:", error);
    throw error;
  }
}
```

## Backend Implementation

Regardless of which frontend method you use, the backend decryption process remains the same:

```javascript
const CryptoJS = require("crypto-js");

function decryptUserData(encryptedUserData, sharedSecretKey) {
  try {
    const decrypted = CryptoJS.AES.decrypt(
      encryptedUserData,
      sharedSecretKey
    ).toString(CryptoJS.enc.Utf8);
    return JSON.parse(decrypted);
  } catch (error) {
    throw new Error("Failed to decrypt user data");
  }
}

// Example Express endpoint
app.post("/decrypt-user-data", (req, res) => {
  try {
    const { encryptedData } = req.body;
    const userData = decryptUserData(
      encryptedData,
      process.env.GHL_APP_SHARED_SECRET
    );
    res.json(userData);
  } catch (error) {
    res.status(400).json({ error: "Failed to decrypt user data" });
  }
});
```

## Decrypted Data Structure

After decryption, the data will be returned as a JSON object containing user information. The structure varies based on whether the user is accessing from an Agency or Location context.

### Agency Context

When accessing from an Agency context, the decrypted data will have the following structure:

```json
{
  "userId": "MKQJ7wOVVmNOMvrnKKKK", // Unique identifier for the user
  "companyId": "GNb7aIv4rQFVb9iwNl5K", // Unique identifier for the company/agency
  "role": "admin", // User's role in the system
  "type": "agency", // Indicates this is an agency user
  "userName": "John Doe", // Full name of the user
  "email": "johndoe@gmail.com", // User's email address
  "isAgencyOwner": true, // If the loggedIn user is agency owner
  "region": "us", // HighLevel region that served this session
  "versionId": "695505b431a9710730ee67d7",
  "appStatus": "live",
  "whitelabelDetails": {
      "domain": "example.com",
      "logoUrl": "example.com"
    }
}
```

### Location Context

When accessing from a Location context, the decrypted data will include an additional `activeLocation` field:

```json
{
  "userId": "MKQJ7wOVVmNOMvrnKKKK", // Unique identifier for the user
  "companyId": "GNb7aIv4rQFVb9iwNl5K", // Unique identifier for the company/agency
  "role": "admin", // User's role in the system
  "type": "agency", // Indicates this is an agency user
  "activeLocation": "yLKVZpNppIdYpah4RjNE", // Unique identifier for the active location
  "userName": "John Doe", // Full name of the user
  "email": "johndoe@gmail.com", // User's email address
  "isAgencyOwner": false, // If the loggedIn user is agency owner
  "region": "us", // HighLevel region that served this session
  "versionId": "695505b431a9710730ee67d7",
  "appStatus": "live",
  "whitelabelDetails": {
      "domain": "example.com",
      "logoUrl": "example.com"
    }
}
```

### Field Descriptions

| Field          | Type    | Description                                                       |
| -------------- | ------- | ----------------------------------------------------------------- |
| userId         | string  | Unique identifier for the user                                    |
| companyId      | string  | Unique identifier for the company/agency                          |
| role           | string  | User's role in the system (e.g., 'admin', 'user')                 |
| type           | string  | Context type ('agency' or 'location')                             |
| activeLocation | string  | (Location context only) Unique identifier for the active location |
| userName       | string  | Full name of the user                                             |
| email          | string  | User's email address                                              |
| isAgencyOwner  | boolean | If the loggedIn user is agency owner                              |
| region         | string  | HighLevel region that served this session (`us`, `au`, ...)       |

#### About `region`

`region` tells you which HighLevel deployment served this session, so your app can route its own
storage and processing to match. Values are lowercase region codes such as `us` and `au`. Treat the
list as open — more regions are added over time.

It is **not** a statement about where the account's HighLevel data is stored. Use it to decide where
*your* application handles the data for this session, not as a data-residency guarantee.

:::tip Reading `region` without decrypting

`region` is also delivered outside the encrypted blob, so you can pick a region **before** you send
the session anywhere — including before the decryption call itself. See
[Getting the region up front](#getting-the-region-up-front).

:::

:::note Treat this payload as additive

New fields are added to the decrypted object over time, inside the same encrypted blob. Read the
keys your app needs and ignore any you do not recognise. Do not validate the decrypted object
against a strict schema that rejects unknown keys, or a future field will break your app.

:::

### Reference Implementation

For a complete example implementation, you can refer to our marketplace app template repository:

[HighLevel Marketplace App Template](https://github.com/GoHighLevel/ghl-marketplace-app-template/tree/main)

The relevant implementation can be found in the `/decrypt-sso` endpoint of the template.

### Security Considerations

- Never expose your Shared Secret key in client-side code
- Always perform decryption on your backend
- Store your Shared Secret key securely using environment variables
- Use HTTPS for all communications between your frontend and backend
- Regularly rotate your Shared Secret keys for enhanced security

## Getting the region up front

Because `region` lives inside the encrypted payload, reading it from there means you have already
sent the session somewhere to be decrypted. If your backend runs in more than one region, that first
call has to go somewhere before you know where it should go.

To avoid that, `region` is also provided **alongside** the encrypted blob, unencrypted, so you can
route the very first request — including the decryption — correctly.

### Custom Pages

The `REQUEST_USER_DATA_RESPONSE` message carries `region` next to `payload`:

```javascript
window.addEventListener("message", ({ data }) => {
  if (data.message !== "REQUEST_USER_DATA_RESPONSE") return;

  const encryptedUserData = data.payload; // unchanged
  const region = data.region; // "us", "au", ...

  // Send the blob to the backend for this region
  fetch(backendFor(region), {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ encryptedData: encryptedUserData }),
  });
});
```

`payload` is unchanged — `region` is a separate key beside it, so existing handlers keep working
without modification.

### Custom JavaScript

Use `exposeSessionDetailsWithRegion`, which resolves to both values:

```javascript
const { data, region } = await window.exposeSessionDetailsWithRegion(APP_ID);

await fetch(backendFor(region), {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ encryptedData: data }),
});
```

`exposeSessionDetails` is unchanged and still resolves to the encrypted string on its own, so
existing integrations need no updates.

:::caution Use the encrypted copy for anything that must be trusted

The `region` delivered beside the payload is not part of the encrypted, signed blob, so treat it as
a routing hint. When a decision needs a value your app can verify, read `region` from the decrypted
payload instead — that copy is sealed with your Shared Secret.

:::
