# Create an app card
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card
Learn how to create an app card on the latest version of the developer platform.
You can create a React-based app card for projects on the latest versions (`2025.2` and `2026.03`) of the developer platform. App cards work similarly to existing cards you may have [built for legacy apps](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference), with some minor modifications to your configuration files.
This guide walks you through how to create a boilerplate app card in an existing app, along with how to upload and preview your card in HubSpot. You'll then upload all these files to your project, where you can preview the card in a developer test account where you've installed your app.
## Prerequisites
* If you haven't done so yet, [create an app](/docs/getting-started/quickstart).
* It's recommended to [create a configurable test account](/docs/developer-tooling/local-development/configurable-test-accounts) so that you can build and test in an isolated environment.
* Ensure you've installed the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli).
## Create an app card
To add a new app card component to your project, use the terminal to navigate into your local project directory, then run the following command:
```shell theme={null}
hs project add
```
Then, when prompted to select a component to add, select **Card**.

If your project doesn't contain any app cards yet, a `cards/` directory will be created for you in the `app/` directory. The `cards/` directory will contain:
* A JSON card configuration file (`*-hsmeta.json`)
* A React component file (`.jsx`)
* A `package.json` file
If your project already includes app cards, the above files will be added to the existing `cards/` directory (excluding `package.json` if it already exists).
```shell theme={null}
myProject
└── src/
└── app/
└── cards/
├── NewCard-hsmeta.json
├── NewCard.jsx
└── package.json
```
Learn more about these files in the [app cards reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference).
To upload the new card to your HubSpot account:
* Run `hs project install-deps` to install the dependencies needed for the app card. This will update the `cards/` directory with the necessary Node modules and a `package-lock.json` file, which will speed up the build of the uploaded app card extension and ensure that any dependencies in your local development environment and production match.
* Then, run `hs project upload` to upload the project to your default account.
* To specify a different account (such as a separate test account), include the `--account` flag and specify the HubID of the account. For example `hs project upload --account 123456`.
* If the project hasn't been uploaded before, you'll be prompted to confirm that you want to create the project in your account. Otherwise, the terminal will display the build and deploy status, then confirm once the project has been successfully uploaded.
## View the card in HubSpot
After uploading the project, you can view it in HubSpot by running `hs project open`. A browser tab will open to the project details page, where you can view your project, app, and its new card component.
If you haven't yet installed the app in the account, you'll need to do so before you can view the card. To install the app:
* Click the **name** of the app in the project summary on the left, or under *Project components*.
* Click the **Distribution** tab.
* Click **Install now**.
The boilerplate app card created by `hs project add` is configured to appear in the middle column of contact records. To display the card, you'll first need to add it to the contact record view:
* In your HubSpot account, navigate to **CRM** > **Contacts**.
* Click the **name** of a contact.
* At the top of the middle column of the contact record, click **Customize**.
* Click **Default view**.
* Select the **tab** that you want to add the card to. You can then hover over the location where you want to place the card and click the **plus button**. This can be adjusted at any time after initial setup.
* In the right sidebar, click the **Card library** tab. Then, click the **Card types** dropdown menu and select **App** to filter for app cards.
* Click **Add card** under the app card you created, then click the **close** button in the top right of the sidebar.
* In the top right, click **Save and exit**.
You'll then be redirected back to the contact record where your card will now appear. Keep the contact record page open in your browser for the next step, when you'll start the local development server.
## Start local development
With your app card added to all contact records, you can now continue to build the app card. The easiest way to quickly iterate is to start the local development server with the `hs project dev` command:
* In the terminal, run `hs project dev`.
* Follow the terminal prompts to select the **account** you want to use for local development.
* The terminal will start the local development server, then confirm once it's running.
* With the server running, navigate back to the browser tab with the contact record and reload the page.
The app card will display with a `Developing locally` tag, indicating that the local development server is ready.
The local development server will automatically detect any changes saved to the app card's front-end React files (i.e., any `.jsx` or `.tsx` files). If you need to make changes to other file types, such as a `.json` configuration file, you'll need to reupload the project and restart the local development server.
## Next steps
Check out the following resources to build out the card's appearance and functionality.
* Consult the [UI extension component reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) to add more UI components to the card.
* See all app card configuration options and more in the [app card reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference).
* View all available utilities and methods available for UI extensions in the [UI extension SDK reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk).
* Learn how to [fetch data for your app card](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data) with `hubspot.fetch()`.
* To list an app with app cards on the HubSpot marketplace, follow the [app card review process steps below](#submitting-app-cards-to-the-hubspot-marketplace).
### Migrating a previously created app card
If you need to migrate an existing project with app cards to the developer platform, check out the following guides:
* [Migration overview](/docs/apps/developer-platform/build-apps/migrate-an-app/overview)
* [Migrate an app to 2026.03](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-to-the-latest-platform-version)
If your existing project had a card with custom objects connected, confirm that you have the `crm.objects.custom.read` scope on your app before migration. For projects built before version `2025.2`, custom object cards could be built with only the `crm.schemas.custom.read` scope required. In the latest versions (`2025.2` and `2026.03`) of the developer platform, `crm.objects.custom.read` is required. If you don't include this scope, the build during the migration will fail with the following error:
**\[ERROR] Build failed or timed out. Inspect the failure to update your build and retry the migration.**
Your customer-facing app will be unaffected, despite the status in the project details page. To fix the issue, add the `crm.objects.custom.read` scope to your legacy app's scopes, re-build the app on version `2023.2`, then re-try the migration.
### Submitting app cards to the HubSpot Marketplace
To submit your app cards for HubSpot Marketplace approval, fill out [this form](https://96it.share.hsforms.com/2a_JwN0iMSDuqoDikVh255Q) with the required information below:
* **Production app ID:** the ID of the app you intend to update or list on the HubSpot Marketplace.
* **Build ID:** the build ID of the app installed in the test account.
* **Testing video URL:** a short video with detailed instructions of how users will interact with the card. For guidance on recording a sufficient video, refer to the [app certification demo video requirements](/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification#requirements-for-the-app-demo-video).
* **App status:** indicate if this is a new app or an app already on the marketplace.
After receiving the above information, the HubSpot Ecosystem Quality team will review your app cards and share initial feedback within 10 business days. Please action this feedback as soon as you receive it, as multiple reviews may be necessary for final approval.
Once the review is successfully completed and your app card is approved, you will receive confirmation with the CTA to resubmit your draft listing. Please do not resubmit your listing before receiving confirmation of approval from [app-card-review@hubspot.com](mailto:app-card-review@hubspot.com). Any resubmissions of your listing before the app card review is completed will be rejected.
# App cards overview
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview
An overview of building cards to customize the HubSpot UI.
Add a layer of UI customization to your app by including app cards that can display data, allow users to perform actions, and more. You can build app cards that display on CRM records, preview panels, the help desk workspace, and the sales workspace.
Once the app is installed in an account, Super Admins and users with *Customize record page layout* permissions can [add any included app cards](https://knowledge.hubspot.com/integrations/install-and-manage-app-cards) to their configured locations in HubSpot.

## Sensitive Data scopes
Apps that use [Sensitive Data scopes](/docs/api-reference/latest/crm/properties/sensitive-data) can include app cards, but with the following restrictions:
* Apps with Sensitive Data scopes **cannot** use [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data), preventing Sensitive Data from being sent to external services.
* Apps with Sensitive Data scopes **cannot** use serverless functions.
If your app requires both Sensitive Data scopes and `hubspot.fetch()`, you'll need to use separate apps for each capability.
**Please note:** these restrictions are safeguards provided by HubSpot, but they are not a substitute for your own Sensitive Data handling practices. As a developer, you are responsible for ensuring that your app handles Sensitive Data in compliance with applicable laws, regulations, and HubSpot's [Sensitive Data terms](https://legal.hubspot.com/sensitive-data-terms).
To get started building app cards:
* Follow the [quickstart guide](/docs/getting-started/quickstart) to start with a new app, or check out the [app creation guide](/docs/apps/developer-platform/build-apps/create-an-app) to customize the features and configuration of a new app.
* Migrate an existing legacy [private](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-an-existing-private-app) or [public](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-an-existing-public-app) app to the developer platform.
* Learn how to [add an app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) to an existing developer platform app.
* Review the [app card reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) for a full breakdown of app card options and features.
If you plan on distributing your app on the HubSpot Marketplace, any app cards you've built are subject to a technical review from the HubSpot Ecosystem Quality team, and must adhere to the requirements listed [here](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements#app-card-requirements).
# App cards reference
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference
Reference information for building app cards on the latest version of the developer platform.
Below, find reference information for building app cards
## Project structure
To add an app card to an app, create a `cards` directory within `src/app`. The `cards` directory should contain:
* A JSON configuration file for each card that defines the card's schema (`*-hsmeta.json`).
* A React file for each card that renders the card's front-end (`.jsx` or `.tsx`).
* A `package.json` file to handle any needed dependencies. This file is shared between all app cards.
```shell theme={null}
project-folder/
└── src/
└── app/
├── app-hsmeta.json
└── cards/
└── my-app-card-hsmeta.json
└── my-app-card.jsx
└── package.json
```
## App card configuration
In the `*-hsmeta.json` configuration file for your app card, include the properties below.
```json theme={null}
{
"uid": "example-card",
"type": "card",
"config": {
"name": "Hello Example App",
"description": "A description of the card's purpose.",
"entrypoint": "/app/cards/ExampleCard.jsx",
"objectTypes": ["contacts"]
}
}
```
Fields marked with \* are required.
| Field | Type | Description |
| ---------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uid`\* | String | The card's unique identifier. This can be any string, but should meaningfully identify the card. HubSpot will identify the card by this ID so that you can change the card's title without removing historical or stateful data, such as the position on the CRM record. |
| `type` | String | The type of component, which should be `card` in this case. |
| `config` | Object | An object containing configuration details. |
| `name`\* | String | The card's title, as displayed in HubSpot's UI. |
| `description` | String | A description of the card. |
| `previewImage` | Object | An object containing the `file` and `altText` fields. The `file` field is the relative path to the preview image. Valid file extensions are png, jpeg, jpg, or gif. The maximum file size is 5.0 MB. The `altText` field is a short description of the image. |
| `entrypoint`\* | String | The file path of the card's front-end React code. |
| `location` | `crm.record.tab` \| `crm.record.sidebar` \| `crm.preview` \|`helpdesk.sidebar` | The surface this card is restricted to. If omitted, the card is eligible for every surface that's compatible with its card type and `objectTypes`. See [*Card locations*](#card-locations) for accepted values. |
| `objectTypes`\* | Array | The types of CRM records that the card will appear on. See the [*Supported objects*](#supported-objects) section below for more details. |
### Supported objects
In the `objectTypes` array of the card's `*-hsmeta.json` configuration file, specify the types of CRM records that the card will appear on. Below are the currently supported CRM objects, their `objectType` value, and the minimum [scope](/docs/apps/legacy-apps/public-apps/overview#scope-types) to add to your app.
For HubSpot's standard objects, `objectType` values are not case sensitive, and both the singular and plural are supported. For example, `"CONTACT"` and `"contacts"` are both valid.
| CRM object | objectType value | Related scope |
| ---------------- | ------------------------------- | -------------------------------------------------------------------------------------------- |
| Contacts | `CONTACT` | `crm.objects.contacts.read` |
| Companies | `COMPANY` | `crm.objects.companies.read` |
| Deals | `DEALS` | `crm.objects.deals.read` |
| Tickets | `TICKETS` | `tickets` |
| Orders | `ORDERS` | `crm.objects.orders.read` |
| Carts | `CARTS` | `crm.objects.carts.read` |
| Invoices | `INVOICES` | `crm.objects.invoices.read` |
| Leads | `LEADS` | `crm.objects.leads.read` |
| Marketing events | `MARKETING_EVENTS` | `crm.objects.marketing_events.read` |
| Partner accounts | `PARTNER_ACCOUNTS` | `crm.objects.partner-accounts.read` |
| Partner clients | `PARTNER_CLIENTS` | `crm.objects.partner-clients.read` |
| Partner services | `PARTNER_SERVICES` | `crm.objects.partner-services.read` |
| Payments | `COMMERCE_PAYMENTS` | `crm.objects.commercepayments.read` |
| Products | `PRODUCTS` | `crm.objects.products.read` |
| Quotes | `QUOTES` | `crm.objects.quotes.read` |
| Subscriptions | `SUBSCRIPTIONS` | `crm.objects.subscriptions.read` |
| Custom objects | `p_objectName` (case sensitive) | `crm.objects.custom.read` |
| App objects | `app_object_uid` | See [app objects scopes](/docs/apps/developer-platform/add-features/app-objects/reference#scopes) |
The custom object wildcard `p_*` can be used in lieu of `p_objectName` to target all custom objects in the HubSpot account.
In addition, the following CRM objects are supported if they've been [activated in the data model builder](https://knowledge.hubspot.com/data-management/use-the-data-model-builder):
| CRM object | objectType value | Related scopes |
| ------------ | ---------------- | ------------------------------- |
| Appointments | `APPOINTMENTS` | `crm.objects.appointments.read` |
| Courses | `COURSES` | `crm.objects.courses.read` |
| Listings | `LISTINGS` | `crm.objects.listings.read` |
| Services | `SERVICES` | `crm.objects.services.read` |
| Projects | `PROJECTS` | `crm.objects.projects.read` |
### Card locations
By default, when `location` is omitted, the card is eligible to appear on every surface that's compatible with its card type and `objectTypes`. Account admins decide where it actually appears via the [page editor](https://knowledge.hubspot.com/object-settings/customize-records). To restrict your card to a single surface, set `location` to one of the supported values:
```json theme={null}
{
"uid": "example-card",
"type": "card",
"config": {
"name": "Hello Example App",
"description": "A description of the card's purpose.",
"location": "crm.record.tab",
"entrypoint": "/app/cards/ExampleCard.jsx",
"objectTypes": ["contacts"]
}
}
```
**Please note:** omitting `location` does not override the `objectTypes` constraint. For example, a card with `"objectTypes": ["TICKETS"]` is still scoped to ticket-bearing surfaces, regardless of whether `location` is set.
Below are the currently supported locations.
* `crm.record.tab`: places the extension in the middle column of CRM record pages, either in one of HubSpot's default tabs or in a custom tab. When `objectType` is set to `COMPANIES`, the card will also be available in the [sales workspace target accounts preview panel](https://knowledge.hubspot.com/prospecting/manage-companies-in-the-sales-workspace).
If you've customized the middle column previously, you'll need to [customize the middle column view](https://knowledge.hubspot.com/object-settings/customize-records) to make any newly created extensions visible.

* `crm.record.sidebar`: displays the extension in the right sidebar of CRM record pages. Extensions in the sidebar cannot use [CRM data components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview). When `objectType` is set to `DEALS`, the card will also be available in the [sales workspace deals sidebar](https://knowledge.hubspot.com/prospecting/create-and-manage-deals-in-the-sales-workspace).

* `crm.preview`: displays the app card in the preview panel that you can access throughout the CRM. When using this location, the extension will be available when previewing the `objectTypes` specified in the [JSON config file](#example-card-json). This includes previewing records from within CRM record pages, index pages, board views, and the lists tool. Learn more about [customizing previews](https://knowledge.hubspot.com/object-settings/customize-record-previews).

* `helpdesk.sidebar`: displays the card in the ticket sidebars within help desk. This includes both the ticket preview panel on the help desk home page and the right sidebar of the ticket view in help desk. To add a card to this location, you'll need to [configure your help desk settings](https://knowledge.hubspot.com/help-desk/customize-the-right-sidebar-of-help-desk) to include the card.
When creating an extension for this location, you'll also need to ensure that the [app's JSON configuration file](/docs/apps/developer-platform/build-apps/app-configuration) includes `tickets` in the `scopes` array, and that the [card's JSON configuration file](#app-card-schema) includes `tickets` in the `objectTypes` field.
* Help desk home:

* Help desk ticket view:

## Building the React front-end
The UI of an app card is created by a React component file, either `.jsx` or `.tsx`. This file lives in the `cards/` directory alongside the [card configuration JSON file](#app-card-schema) (`*-hsmeta.json`). In the card configuration file, you'll specify the path of the React file in the `entrypoint` field.
Below is an example of a simple app card, which includes `Text` and `Button` [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) to render the card content, along with a `Flex` component for managing layout.
```jsx theme={null}
import React from "react";
import { Text, Button, Flex, hubspot } from "@hubspot/ui-extensions";
// Define the extension to be run within the Hubspot CRM
hubspot.extend(() => );
// Define the Extension component
const Extension = () => {
return (
This is a simple getting started UI extension with static text.
);
};
```
The following reference documentation is provided for building out card appearance and functionality:
* [UI extension components reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview)
* [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk)
## Managing dependencies
You can include dependencies for your app card in a `package.json` file within the `cards/` directory. By default, when adding an app card through the `hs project add` command, a `package.json` file will be created for you with the following dependencies:
* `@hubspot/ui-extensions`
* `react`
* `typescript`
To install dependencies for project components with a `package.json` file, you can run the `hs project install-deps` command in your project directory.
```json theme={null}
{
"name": "hubspot-example-extension",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@hubspot/ui-extensions": "latest",
"react": "^18.2.0"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
```
# Create an app home page
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-home-page
Learn how to create a home page for your app.
[App pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview) are a newer extension point and provide additional functionality compared to the app home page feature described in this article. While app home pages are still supported, it's recommended you try out app pages to leverage new components and features such as page routing and passing parameters between pages.
App home pages let you create a custom landing experience for users when they navigate to your app in HubSpot. Built with React and powered by the [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk), home pages are built with the same toolkit available for [app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) and [settings pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page), including HubSpot's UI components and data fetching utilities.
Below, learn how to create an app home page on the latest versions of the developer platform (`2025.2` and `2026.03`).
## Prerequisites
* If you haven't done so yet, [create a new app](/docs/getting-started/quickstart).
* Ensure you've installed the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli).
## Create an app home page
To add a home page component to an existing app, use the terminal to navigate into your local project directory, then run the following command:
```shell theme={null}
hs project add
```
Then, when prompted to select a component to add, select **Home**.

A new `pages/` directory will be created in the project's `src/app/` directory. The `pages/` directory will contain:
* A JSON configuration file (`*-hsmeta.json`)
* A React component file (`.tsx`)
* A `package.json` file
```shell theme={null}
myProject
└── src/
└── app/
└── pages/
├── home-hsmeta.json
├── Home.tsx
└── package.json
```
```json theme={null}
{
"uid": "app-home",
"type": "page",
"config": {
"entrypoint": "/app/pages/Home.tsx",
"location": "home"
}
}
```
```tsx theme={null}
import React from "react";
import { EmptyState, Text, hubspot } from "@hubspot/ui-extensions";
import {
HeaderActions,
PrimaryHeaderActionButton,
SecondaryHeaderActionButton,
} from "@hubspot/ui-extensions/pages/home";
hubspot.extend<"home">(({ context }) => {
return ;
});
const NewHomesPage = ({ context }) => {
return (
<>
console.log("P1")}>Primary 1 console.log("S1")}>Secondary 1 console.log("S2")}>Secondary 2Build your application home page here!
>
);
};
```
```json theme={null}
{
"name": "hubspot-example-extension",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@hubspot/ui-extensions": "latest",
"react": "^18.2.0"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
```
To upload your home page to HubSpot:
* Run `hs project install-deps` from within your local project directory to install necessary dependencies. This will create a `package-lock.json` file, which will speed up the project build time and ensure that any dependencies in your local development environment and production match.
* Then, run `hs project upload`.
* After the project finishes deploying, open the project in HubSpot by running `hs project open`. Alternatively, in HubSpot you can navigate to **Development** > **Projects**, then click the **name** of your project.
* Your home component should now be listed on the details page.
## View the app home page in HubSpot
To verify the home page is working correctly:
* In the HubSpot account where you've installed the app, click the **Marketplace** icon.
* In the *Your recently visited apps* section, click the **name of the app** to open the app's overview page.
If your app doesn't appear in the dropdown menu, or if you have more than three apps installed, you can access your app home page directly at the following URL:
`https://app.hubspot.com/app/{HubID}/{appId}`
You can now continue to build out your app's home page as needed. Similar to building app cards, you'll use the UI extensions SDK to add functionality and visual elements to your home page. Note that all the existing limitations around building UI extensions apply to building a settings page.
* Use `hubspot.fetch` to leverage your backend to save and retrieve settings. Learn more about using this approach in the legacy documentation.
* Check out the reference documentation on [standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for how to use React components when building your extension, or use the component in the [Figma Design Kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit).
* Use the `hs project dev` command to iteratively build out your settings page and preview your changes locally.
The local development server will only pick up changes saved to the front-end React file. If you update the `*-hsmeta.json` or `package.json` files, you'll need to stop the server, upload your changes, then start the server again.
## Home page components
In addition to the full set of [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview), there are three UI components specific to app home pages that you can use together to add action buttons to the header area:
* ``
* ``
* ``
To use these components, you'll need to be on version `0.10.0` or later of the `ui-extensions` NPM package. You can check your current version by running `npm list` or `npm list -g`, and install the latest version by running `npm i @hubspot/ui-extensions`.
These components imported from `@hubspot/ui-extensions/pages/home`.
```tsx theme={null}
import {
HeaderActions,
PrimaryHeaderActionButton,
SecondaryHeaderActionButton,
} from "@hubspot/ui-extensions/pages/home";
console.log("P1")}>Primary console.log("S1")}>Secondary 1 console.log("S2")}>Secondary 2;
```
Below, learn more about each component.
### HeaderActions
The `HeaderActions` component is the main wrapper component for the primary and secondary header action button components. It's recommended to only include one instance of `HeaderActions` in your app, and to avoid adding/removing it dynamically which can result in unexpected behavior.
Only `PrimaryHeaderActionButton` and `SecondaryHeaderActionButton` are supported as children.
```jsx theme={null}
import { HeaderActions, PrimaryHeaderActionButton } from "@hubspot/ui-extensions/pages/home";
Primary button;
```
### PrimaryHeaderActionButton
The `PrimaryHeaderActionButton` component renders an orange button, and should be used for the primary action on the home page. `HeaderActions` can contain only one instance of `PrimaryHeaderActionButton`. For additional buttons, use `SecondaryHeaderActionButton`.
```jsx theme={null}
import {
HeaderActions,
PrimaryHeaderActionButton
} from '@hubspot/ui-extensions/pages/home';
// Basic primary button
saveData()}>
Save changes
// Primary button as link
View details
```
| Prop | Type | Description |
| --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `children` (required) | ReactNode | The button text or content. |
| `onClick` | `() => void` | A function that will be invoked when the button is clicked. It receives no arguments and it’s return value is ignored. |
| `href` | String \| Object | Include this prop to open a URL on click. Contains the following fields:
`url` (string): the URL that will open on click.
`external` (boolean): set to `true` to open the URL in a new tab and display an external link icon. By default:
Links to HubSpot app pages will open in the same tab and will not include an icon.
Links to non-HubSpot app pages will open in a new tab and include the icon.
When a button includes both `href` and an `onClick` action, both will be executed on button click. |
| `disabled` | Boolean | When set to `true`, the button will render in a greyed-out state and cannot be clicked. |
| `overlay` | Object | Include a [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) or [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays). |
### SecondaryHeaderActionButton
The `SecondaryHeaderActionButton` component renders additional buttons that appear in an *Actions* dropdown menu next to the primary button. You can include multiple secondary buttons, and each will appear as another item in the *Actions* dropdown menu.
```jsx theme={null}
import {
HeaderActions,
PrimaryHeaderActionButton,
SecondaryHeaderActionButton,
} from "@hubspot/ui-extensions/pages/home";
hubspot.extend <
"home" >
(() => {
return ;
});
function AdvancedHeaderActions() {
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
setIsSaving(true);
try {
console.log("Saving...");
} finally {
setIsSaving(false);
}
};
return (
{isSaving ? "Saving..." : "Save Changes"}
console.log("Export Data")}>Export Data console.log("Share")}>Share
Help docs
);
}
```
# Create app pages
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages
Learn how to create custom pages for your app with routing support.
App pages let you create custom page experiences for your app in HubSpot, including a home page and additional pages. Built with React and powered by the [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk), app pages are built with the same toolkit available for [app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) and [settings pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page), including HubSpot's UI components and data fetching utilities.
With app pages, you can create multiple pages within your app and navigate between them. Users can access your app's home page directly from the HubSpot Marketplace menu, and you can link to additional pages within your app.
Below, learn how to create app pages on the latest versions of the developer platform (`2025.2` or `2026.03`).
## Prerequisites
* Ensure you've installed the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli).
* If you haven't done so yet, [create a new app](/docs/getting-started/quickstart).
## Create app pages
### Add an app pages component to your project
To add an app pages component to an existing app, use the terminal to navigate into your local project directory, then run the following command:
```shell theme={null}
hs project add
```
Then, when prompted to select a component to add, select **Pages**.
A new `pages/` directory will be created in the project's `src/app/` directory. The `pages/` directory will contain:
* A JSON configuration file (`pages-hsmeta.json`)
* A React component file (`Pages.tsx`)
* A `package.json` file
```shell theme={null}
myProject
└── src/
└── app/
└── pages/
├── pages-hsmeta.json
├── Pages.tsx
└── package.json
```
```json theme={null}
{
"uid": "my-app-pages",
"type": "page",
"config": {
"entrypoint": "/app/pages/Pages.tsx"
}
}
```
```tsx theme={null}
import React from "react";
import { Text, hubspot } from "@hubspot/ui-extensions";
import { createPageRouter, PageHeader, PageRoutes, PageLink, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
const AppHomePage = () => {
return (
<>
HomeHomeBuild your application home page here!
>
);
};
const PageLayout = ({ children }) => {
return (
<>
HomeDocumentation
{children}
>
);
};
const PageRouter = createPageRouter(
);
hubspot.extend<"pages">(() => );
```
```json theme={null}
{
"name": "hubspot-example-extension",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@hubspot/ui-extensions": "latest",
"react": "^18.2.0"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
```
### Upload to HubSpot
To upload your app pages to HubSpot:
* Run `hs project install-deps` from within your local project directory to install necessary dependencies. This will create a `package-lock.json` file, which will speed up the project build time and ensure that any dependencies in your local development environment and production match.
* Then, run `hs project upload`.
* After the project finishes deploying, open the project in HubSpot by running `hs project open`. Alternatively, in HubSpot you can navigate to **Development** > **Projects**, then click the **name** of your project.
* Your pages component should now be listed on the details page.
## View app pages in HubSpot
To verify your app pages are working correctly:
* In the HubSpot account where you've installed the app, click the **Marketplace** icon.
* In the *Your recently visited apps* section, click the **name of the app** to open the app's home page.
If your app doesn't appear in the dropdown menu, or if you have more than three apps installed, you can access your app's home page directly at the following URL:
`https://app.hubspot.com/app/{HubID}/{appId}`
You can now continue to build out your app pages as needed. Similar to building app cards, you'll use the UI extensions SDK to add functionality and visual elements to your pages. Note that all the existing limitations around building UI extensions apply to building app pages.
* Use `hubspot.fetch` to communicate with your backend to save and retrieve data.
* Check out the reference documentation on [standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for how to use React components when building your extension, or use the component in the [Figma Design Kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit).
* Use the `hs project dev` command to iteratively build out your app pages and preview your changes locally.
The local development server will only pick up changes saved to the front-end React file. If you update the `*-hsmeta.json` or `package.json` files, you'll need to stop the server, upload your changes, then start the server again.
## Next steps
Now that you've created your app pages, check out the following articles for guidance as you continue to develop your app page:
* [Page routing](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing)
* [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking)
* [App pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference)
* [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header)
* [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes)
# App pages overview
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview
An overview of building pages to customize the HubSpot UI.
App pages are a newer extension point and provide additional functionality compared to the older [app home page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-home-page) feature. While app home pages are still supported, it's recommended you try out app pages to leverage new components and features such as page routing and passing parameters between pages.
Create custom page experiences for your app in HubSpot with app pages. Built with React and powered by the [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk), app pages let you build a main page for your app, providing users with a dedicated space to interact with your app's features. You can also define additional pages for documentation, analytics, support, or any other functionality your app provides.
## Key features
* **Main page**: create a custom landing experience for users when they navigate to your app.
* **Multiple pages**: build multiple pages within your app that users can navigate between.
* **Layout components**: wrap groups of routes with shared UI such as navigation bars or sidebars.
* **Page header actions**: add primary and secondary action buttons to the page header.
* **Deep linking**: link directly to specific pages in your app.
* **Wildcard routes**: match hierarchical paths for file browsers or documentation structures.
* **Built with React**: use the same UI components and data fetching utilities available for app cards and settings pages.
## Start building out app pages
* Follow the [quickstart guide](/docs/getting-started/quickstart) to start with a new app, or check out the [app creation guide](/docs/apps/developer-platform/build-apps/create-an-app) to customize the features and configuration of a new app.
* Learn how to [create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages).
* Define [routes for your pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing).
* Link between [pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking).
* Consult the [app pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference).
## Component reference documentation
* [UI components overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview).
* [PageHeader](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header).
* [PageLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link).
* [PageBreadcrumbs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-breadcrumbs).
* [PageTitle](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-title).
* [PageRoutes](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes).
* [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link).
If you plan on distributing your app on the HubSpot App Marketplace, any app pages you've built are subject to a technical review from the HubSpot Ecosystem Quality team.
# Page linking and navigation
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking
Learn how to navigate between pages and pass data in app pages.
Once you've defined routes in your app, you'll need to navigate between them. This guide covers the different methods for linking to pages and passing parameters when navigating.
For information on how to access parameters in your page components, see the [accessing route information](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing#accessing-route-information) section in the routing guide.
## Navigation methods
There are three primary ways to navigate between pages in your app.
### Using the PageLink component
The `PageLink` component is the primary way to create clickable links to other pages within your app.
```tsx theme={null}
import { PageLink } from "@hubspot/ui-extensions/pages";
// Link to home page
Home
// Link to a named page
Documentation
// Link with query parameters
Getting Started
```
The `PageLink` component supports the following props:
* `to` (string): The path to navigate to (e.g., `"/"`, `"/docs"`)
* `params` (object): Query parameters to include in the URL
For more details on the PageLink component, see the [PageLink component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link).
The `Link` component from `@hubspot/ui-extensions` should only be used for external URLs. Use `PageLink` for navigating between pages within your app.
### Using PageHeader actions
Page header actions support linking to app pages using `PageLink` components:
```tsx theme={null}
import { Link } from "@hubspot/ui-extensions";
import { PageHeader } from "@hubspot/ui-extensions/pages";
HomeDocumentation
Analytics Dashboard
External Link
```
`PageHeader.PrimaryAction` and `PageHeader.SecondaryActions` can only contain `PageHeader.PageLink` (for internal pages) or `PageHeader.Link` (for external URLs) components.
For more details on PageHeader components, see the [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header).
### Programmatic navigation
You can navigate programmatically using the `navigateToPage` action from the extension context:
```tsx theme={null}
import { Button, hubspot, useExtensionActions } from "@hubspot/ui-extensions";
const MyComponent = () => {
const { navigateToPage } = useExtensionActions();
const handleClick = () => {
navigateToPage({
to: '/docs',
params: { section: 'api' }
});
};
return ;
};
```
The `navigateToPage` function accepts an options object with:
* `to` (string, required): The path to navigate to
* `params` (object, optional): Query parameters to include
## Linking with path parameters
When your routes include path parameters (e.g., `/view-contact/:contactId`), you can pass parameter values through the `params` object. The routing system will automatically substitute matching parameter names into the URL path.
### Basic path parameter linking
```tsx theme={null}
import { PageLink } from "@hubspot/ui-extensions/pages";
// Route definition:
// Link to a specific contact
View Contact
```
**Generated URL:** `https://app.hubspot.com/app/{HubID}/{appId}/view-contact/123`
The `contactId` parameter matches the `:contactId` path parameter, so it gets substituted into the path.
### Mixing path and query parameters
When you provide multiple parameters, the routing system intelligently separates them:
```tsx theme={null}
Contact Activity
```
**Generated URL:** `https://app.hubspot.com/app/{HubID}/{appId}/view-contact/123?tab=activity`
### Multiple path parameters
You can link to routes with multiple path parameters by including all matching parameters:
```tsx theme={null}
// Route definition:
//
View Note
```
**Generated URL:** `https://app.hubspot.com/app/{HubID}/{appId}/deals/456/notes/789?highlightSection=comments`
### How parameter substitution works
The routing system follows these rules when generating URLs:
1. **Matching parameters go into the path**: If a parameter name matches a path parameter placeholder (e.g., `contactId` matches `:contactId`), it replaces that placeholder in the URL.
2. **Non-matching parameters become query parameters**: Any parameters that don't match path parameter names are added to the query string.
3. **Parameter order doesn't matter**: The system will correctly match parameters regardless of the order they appear in the `params` object.
### Programmatic navigation with path parameters
The same substitution rules apply when using `navigateToPage`:
```tsx theme={null}
const { navigateToPage } = useExtensionActions();
const viewContact = (contactId, initialTab) => {
navigateToPage({
to: '/view-contact/:contactId',
params: {
contactId: contactId, // Goes into path
tab: initialTab // Goes into query string
}
});
};
// Usage
viewContact("12345", "overview");
// Navigates to: /view-contact/12345?tab=overview
```
For details on how to access parameters in your page components using `usePageRoute`, including parameter limitations and best practices, see the [Accessing route information](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing#accessing-route-information) section in the routing guide.
## See also
* [Page routing](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing)
* [PageLink component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link)
* [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes)
* [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header)
* [Link component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link)
* [App pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference)
# Page routing
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing
Learn how to define routes and structure pages in app pages.
App pages support routing, allowing you to create multiple pages within your app that users can navigate between. This guide covers how to define routes, including route types, path parameters, nested routes, and how paths are normalized.
## Route types
The `PageRoutes` component supports three types of routes to handle different navigation scenarios.
### Index routes
The index route is your app's home page, rendered when users navigate to the root URL of your app. Every app pages implementation should include an index route.
```tsx theme={null}
import { createPageRouter, PageRoutes } from "@hubspot/ui-extensions/pages";
const PageRouter = createPageRouter(
);
```
**URL:** `https://app.hubspot.com/app/{HubID}/{appId}`
The index route is always accessed with an empty path or `/`. You don't need to specify a path prop for index routes.
### Named routes
Named routes define specific pages within your app. Each route has a unique path that users can navigate to.
```tsx theme={null}
```
Route paths are normalized automatically, so both `"docs"` and `"/docs"` will work. See [path normalization](#path-normalization) for details.
**URLs:**
* `https://app.hubspot.com/app/{HubID}/{appId}/docs`
* `https://app.hubspot.com/app/{HubID}/{appId}/support`
* `https://app.hubspot.com/app/{HubID}/{appId}/analytics`
### AnyRoute component
The `AnyRoute` component defines a fallback route that renders when no other routes match. This is useful for displaying custom 404 pages or redirecting users.
```tsx theme={null}
```
The `AnyRoute` will render for any path that doesn't match defined routes, such as `/invalid-path` or `/does-not-exist`.
It's recommended to always include an `AnyRoute` to provide a better user experience when users navigate to undefined paths.
### Wildcard routes (splat routes)
Wildcard routes, also known as "splat" routes, allow you to match any characters following a specific path prefix. If a route path ends with `/*`, it will match any characters following the `/`, including other `/` characters.
```tsx theme={null}
```
**Example matches**
The table below provides examples of route patterns and sample URLs that will and won't match:
| Route Pattern | Matches | Does Not Match |
| ------------- | --------------------------------------------------------------------------------------- | ------------------------------------------ |
| `files/*` | `/files/documents/report.pdf` `/files/images/photo.jpg` `/files/a/b/c/d.txt` | `/file/doc.pdf` `/documents/file.txt` |
| `docs/*` | `/docs/getting-started` `/docs/api/endpoints` `/docs/guides/advanced/routing` | `/doc/guide` `/documentation/api` |
#### Accessing the wildcard value
The matched portion of the URL is available as `params["*"]`:
```tsx theme={null}
import { Heading, Text } from "@hubspot/ui-extensions";
import { usePageRoute, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
const FileBrowserPage = () => {
const { params } = usePageRoute();
const filePath = params["*"];
// For URL: /files/documents/report.pdf
// filePath = "documents/report.pdf"
return (
<>
HomeFiles{filePath}File BrowserCurrent path: {filePath}
>
);
};
```
You can destructure the `*` parameter by assigning it a more descriptive name:
```tsx theme={null}
const { params: { "*": filePath } } = usePageRoute();
// or
const { params: { "*": splat } } = usePageRoute();
```
#### Wildcard routes vs AnyRoute
While both match multiple paths, they serve different purposes:
**AnyRoute component:**
* Catches any unmatched path at that nesting level
* Typically used for 404 pages or fallback routes
* Does not provide the matched path as a parameter
**Wildcard routes (`path="prefix/*"`):**
* Match a specific path pattern (e.g., `/docs/*` only matches paths starting with `/docs/`)
* The matched portion is accessible as a parameter
* Useful for file browsers, documentation sites, or any hierarchical content
```tsx theme={null}
{/* Wildcard route matches /docs/anything */}
{/* AnyRoute catches everything else that doesn't match */}
```
Use wildcard routes when you need to handle hierarchical paths (like file systems or documentation structures) and need access to the full path. Use AnyRoute for general 404 handling.
## Nested routes
You can nest `PageRoutes` components within the route tree to create hierarchical page structures. Nest routes by adding a `` with a `path` prop as a child of the parent ``:
```tsx expandable theme={null}
import React from "react";
import { Text, hubspot } from "@hubspot/ui-extensions";
import { createPageRouter, PageRoutes, PageLink, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
const HomePage = () => {
return (
<>
HomeHomeContent for the Home page...Visit Support
>
);
};
const SupportIndexPage = () => {
return (
<>
HomeSupportSupportBrowse our support resources...Contact UsFAQ
>
);
};
const ContactUsPage = () => {
return (
<>
HomeSupportContact UsContact UsGet in touch with our support team...
>
);
};
const FAQPage = () => {
return (
<>
HomeSupportFAQFAQFind answers to common questions...
>
);
};
const PageRouter = createPageRouter(
);
hubspot.extend<"pages">(() => );
```
This creates the following URL structure:
* `/` - Home page
* `/support` - Support index page
* `/support/contact-us` - Contact us page
* `/support/faq` - FAQ page
### Nested route behavior
When using nested routes:
* A `` with a `path` prop defines a group of nested routes under that path
* The nested `IndexRoute` renders when the path matches exactly the parent path (`/support`)
* Named routes within the nested `PageRoutes` extend the parent path (`/support/contact-us`)
* Each nested level can have its own `AnyRoute` for unmatched paths within that section
### Nesting with path parameters
You can combine nested routes with path parameters to create hierarchical structures:
```tsx theme={null}
const PageRouter = createPageRouter(
);
```
This creates routes like:
* `/deals/123` - Deal overview
* `/deals/123/notes` - Deal notes list
* `/deals/123/notes/456` - Specific note details
## Layout components
Layout components let you wrap groups of routes with shared UI, such as navigation bars, sidebars, or other persistent elements. A layout component receives `children` as a prop and renders the matched route's content within it.
### Basic layout
Pass a layout component to `` using the `layoutComponent` prop:
```tsx theme={null}
import { ReactNode } from "react";
import { Text, Flex } from "@hubspot/ui-extensions";
import { createPageRouter, PageRoutes, PageLink, usePageRoute } from "@hubspot/ui-extensions/pages";
function AppLayout({ children }: { children: ReactNode }) {
const { path } = usePageRoute();
return (
HomeDocsSupport
{children}
);
}
const PageRouter = createPageRouter(
);
```
The `AppLayout` component will wrap every page rendered by these routes. The matched page component is rendered in place of `{children}`.
### Nested layouts
You can apply different layout components at different nesting levels. Nested `` can define their own `layoutComponent`, which will render inside the parent layout:
```tsx theme={null}
function CustomersLayout({ children }: { children: ReactNode }) {
return (
Customers
{children}
);
}
const PageRouter = createPageRouter(
);
```
When a user navigates to `/customers`, the rendered output will be `AppLayout` > `CustomersLayout` > `ListCustomersPage`. Each layout wraps the content below it in the route tree.
## Path parameters
Path parameters allow you to create dynamic routes that can capture values from the URL path. This is useful for pages that display specific items, such as viewing a particular contact, deal, or custom object record.
### Defining routes with path parameters
To define a route with path parameters, use a colon (`:`) followed by the parameter name in the route path:
```tsx theme={null}
```
You can include multiple path parameters in a single route, as shown in the `documents/:folderId/:documentId` example.
### Understanding path parameters
Path parameters are placeholders in your route definitions that capture values from the URL. When a user navigates to a URL that matches the route pattern, the parameter values are extracted and made available to your component.
**Example route pattern:** `/view-contact/:contactId`
This pattern will match URLs like:
* `/view-contact/123` (contactId = "123")
* `/view-contact/abc-def` (contactId = "abc-def")
* `/view-contact/contact-456` (contactId = "contact-456")
But will not match:
* `/view-contact` (missing contactId)
* `/view-contact/123/edit` (additional path segment)
### Multiple path parameters
When using multiple path parameters, each segment of the URL path can capture a different value:
```tsx theme={null}
// Route definition
```
This route pattern creates a hierarchical structure where:
* `/deals/456/notes/789` matches with `dealId="456"` and `noteId="789"`
* The path segments between parameters (`deals` and `notes`) must match exactly
* Both parameters are required for the route to match
### Best practices for path parameters
* **Use descriptive parameter names**: Choose names that clearly indicate what the parameter represents (e.g., `:contactId`, `:dealId`, not `:id`)
* **Keep parameter counts reasonable**: While you can have multiple path parameters, too many can make URLs hard to read
* **Validate parameter values**: Always validate path parameter values before using them (e.g., check if an ID exists)
* **Use path parameters for resource IDs**: Path parameters work well for resource identifiers that define what page you're viewing
* **Use query parameters for optional state**: Use query parameters for filtering, sorting, or view state that doesn't change what resource you're viewing
* **Use wildcards for hierarchical paths**: When you need to match multiple nested segments and preserve the full path (e.g., file systems, documentation), use wildcard routes instead of multiple path parameters
## Accessing route information
Use the `usePageRoute` hook to access the current page path, route ID, and all parameters (both path parameters and query parameters) in your page components. The hook returns an object with `path`, `routeId`, and `params`.
### Basic usage
```tsx theme={null}
import { Text } from "@hubspot/ui-extensions";
import { usePageRoute, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
const DocsPage = () => {
const { path, params } = usePageRoute();
const { section, topic } = params;
return (
<>
HomeDocumentationDocumentationCurrent path: {path}
{section && Section: {section}}
{topic && Topic: {topic}}
>
);
};
```
When a user visits `https://app.hubspot.com/app/{HubID}/{appId}/docs?section=api&topic=authentication`, the hook will return:
* `path`: `"/docs"`
* `params.section`: `"api"`
* `params.topic`: `"authentication"`
### Accessing path parameters
Path parameters are included in `params` alongside query parameters - the `usePageRoute` hook returns all parameters regardless of their source:
```tsx theme={null}
// Route definition:
const ContactDetailsPage = () => {
const { params } = usePageRoute();
const { contactId, tab } = params;
// contactId comes from the path: /view-contact/123
// tab comes from query string: ?tab=activity
return (
<>
HomeContactsContact {contactId}Contact {contactId}Contact ID: {contactId}
{tab && Active tab: {tab}}
>
);
};
```
When a user visits `/view-contact/123?tab=activity`:
* `contactId`: `"123"` (from path)
* `tab`: `"activity"` (from query string)
### Accessing wildcard parameters
Wildcard values are accessed through the `params["*"]` property:
```tsx theme={null}
import { usePageRoute } from "@hubspot/ui-extensions/pages";
const FileBrowserPage = () => {
const { params } = usePageRoute();
const wildcardValue = params["*"];
// For URL: /files/documents/2024/report.pdf
// wildcardValue = "documents/2024/report.pdf"
return (
<>
File BrowserPath: {wildcardValue}
>
);
};
```
You can destructure the `*` parameter by assigning it a more descriptive name:
```tsx theme={null}
const { params: { "*": filePath } } = usePageRoute();
// or
const { params: { "*": splat } } = usePageRoute();
```
When a user visits `/files/documents/2024/report.pdf` with a route defined as `path="/files/*"`:
* `params["*"]`: `"documents/2024/report.pdf"`
Like all page parameters, wildcard values are strings. Remember to validate and sanitize the wildcard value before using it, especially if using it to access files or resources.
## Route IDs
Each route can be assigned a stable `id` prop that uniquely identifies it. When a route matches, its `id` is available as `routeId` from the `usePageRoute` hook. This is particularly useful in layout components where you need to determine which route is currently active without relying on path matching.
Route IDs must be unique within your collection of routes. Having multiple routes with the same `id` will result in an error.
### Defining route IDs
Add an `id` prop to any route component:
```tsx theme={null}
const PageRouter = createPageRouter(
);
```
### Accessing the route ID
Use the `routeId` property from the `usePageRoute` hook:
```tsx theme={null}
import { usePageRoute } from "@hubspot/ui-extensions/pages";
const MyComponent = () => {
const { routeId } = usePageRoute();
// routeId is "home", "support", "not-found", etc.
};
```
### Using route IDs in layout components
Route IDs are especially useful in layout components where you need to render different breadcrumbs, titles, or other shared UI based on the active route. Unlike path-based matching, route IDs remain stable even if route paths change, and they work reliably with dynamic path parameters.
```tsx theme={null}
import { ReactNode } from "react";
import { Text, Flex } from "@hubspot/ui-extensions";
import { createPageRouter, PageRoutes, PageLink, PageBreadcrumbs, PageTitle, usePageRoute } from "@hubspot/ui-extensions/pages";
const BREADCRUMBS: Record = {
home: Home,
support: (
<>
HomeSupport
>
),
"not-found": (
<>
HomeNot Found
>
),
};
const TITLES: Record = {
home: "Home",
support: "Support",
"not-found": "Page Not Found",
};
function AppLayout({ children }: { children: ReactNode }) {
const { routeId } = usePageRoute();
return (
{BREADCRUMBS[routeId]}{TITLES[routeId]}
{children}
);
}
const PageRouter = createPageRouter(
);
```
Route IDs provide a more robust way to identify the active route compared to path matching. They don't break when paths are renamed and work naturally with routes that contain dynamic path parameters.
## Parameter limitations
All page parameters are treated as strings. If you pass numbers, booleans, or other types, they will be converted to strings.
Understanding parameter limitations helps you avoid common issues when working with app pages.
### All values are strings
Parameters are always received as strings, regardless of the type you pass.
```tsx theme={null}
// Passing parameters
navigateToPage({
to: '/analytics',
params: {
count: 42, // Will be "42"
enabled: true, // Will be "true"
userId: "12345" // Will be "12345"
}
});
// Receiving parameters
const { params } = usePageRoute();
const { count, enabled, userId } = params;
// count is "42" (string, not number)
// enabled is "true" (string, not boolean)
// userId is "12345" (string)
// You'll need to convert them if needed
const countNumber = parseInt(count, 10);
const enabledBoolean = enabled === "true";
```
Always validate and convert parameter values before using them. Check for `null`, `undefined`, or invalid formats to handle cases where parameters are missing or malformed.
### No complex objects
You cannot pass objects, arrays, or other complex data structures as parameters. Only simple string values are supported.
```tsx theme={null}
// ❌ NOT supported
navigateToPage({
to: '/page',
params: {
user: { name: "John", age: 30 }, // Objects not supported
items: [1, 2, 3] // Arrays not supported
}
});
// ✅ Supported - use JSON.stringify and parse if needed
navigateToPage({
to: '/page',
params: {
user: JSON.stringify({ name: "John", age: 30 })
}
});
// Then parse it when accessing
const { params } = usePageRoute();
const userData = params.user ? JSON.parse(params.user) : null;
```
When using JSON.stringify for complex data, be mindful of URL length limits. Very large objects will create excessively long URLs.
### URL length limits
Query parameters are part of the URL, which has practical length limits:
* **Browsers**: Most browsers support URLs up to 2,000-8,000 characters
* **Best practice**: Keep URLs under 2,000 characters for maximum compatibility
* **Recommendation**: Use parameters for IDs and simple state, not for large data payloads
If you need to pass large amounts of data:
1. Store the data on your backend
2. Pass only an ID through a URL parameter
3. Fetch the full data using the ID when the page loads
```tsx theme={null}
// ❌ Bad - passing large data through URL
navigateToPage({
to: '/report',
params: {
data: JSON.stringify(hugeDataObject) // Too large!
}
});
// ✅ Good - pass ID and fetch data
navigateToPage({
to: '/report',
params: {
reportId: "report-123" // Small ID
}
});
// In the report page, fetch the full data
const ReportPage = () => {
const { params } = usePageRoute();
// Fetch report data using the ID
const reportData = useFetchReport(params.reportId);
// ...
};
```
## Path normalization
When navigating to pages or defining routes, the routing system automatically normalizes paths to ensure consistent behavior. Understanding these normalization rules can help you avoid unexpected routing issues.
### Normalization rules
The following transformations are applied to paths:
1. **Leading slashes are added**: If a path doesn't start with `/`, one is automatically added.
* `"foo"` → `"/foo"`
* `"docs"` → `"/docs"`
2. **Trailing slashes are removed**: Paths should not end with a slash, and any trailing slashes are automatically removed.
* `"foo/"` → `"/foo"`
* `"docs/"` → `"/docs"`
* `"support/contact/"` → `"/support/contact"`
3. **Multiple consecutive slashes are collapsed**: Any sequence of multiple slashes is reduced to a single slash.
* `"foo//bar"` → `"/foo/bar"`
* `"docs///api"` → `"/docs/api"`
* `"//support"` → `"/support"`
4. **Empty paths are treated as root**: An empty string or single slash both represent the home page.
* `""` → `"/"`
* `"/"` → `"/"`
### Examples
Here are some examples of how different path formats are normalized:
| Input Path | Normalized Path |
| ------------------ | ---------------- |
| `"docs"` | `"/docs"` |
| `"/docs"` | `"/docs"` |
| `"docs/"` | `"/docs"` |
| `"/docs/"` | `"/docs"` |
| `"support/faq"` | `"/support/faq"` |
| `"support//faq"` | `"/support/faq"` |
| `"//docs///api//"` | `"/docs/api"` |
| `""` | `"/"` |
| `"/"` | `"/"` |
### Practical implications
Because of path normalization:
* **You can use either format in route definitions**: Both `path="docs"` and `path="/docs"` will work the same way after normalization.
```tsx theme={null}
// Both are equivalent after normalization
```
* **Navigation calls are forgiving**: When navigating to pages, you don't need to worry about exact path formatting. See the [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking) guide for more details.
* **Consistency in comparisons**: When comparing paths using `usePageRoute()`, always compare against normalized paths.
```tsx theme={null}
import { usePageRoute } from "@hubspot/ui-extensions/pages";
const MyComponent = () => {
const { path } = usePageRoute();
// path is always normalized, so compare with normalized format
if (path === "/docs") {
// This will match /docs, docs/, /docs/, etc.
}
return Current page: {path};
};
```
While path normalization is forgiving, it's best practice to use consistent path formatting throughout your code.
## Getting the current page path
Use the `usePageRoute` hook to get the current page path. This is useful for determining which page is currently active, which can help with navigation highlighting and conditional rendering.
```tsx theme={null}
import { usePageRoute, PageLink } from "@hubspot/ui-extensions/pages";
const Navigation = () => {
const { path } = usePageRoute();
// For home page: "/"
// For /docs: "/docs"
// For /support/faq: "/support/faq"
return (
HomeDocsSupport
);
};
```
The `path` value returned by `usePageRoute` is always the normalized path, so you can reliably compare it against expected values.
### Common use cases
**Highlighting active navigation items:**
```tsx theme={null}
const { path } = usePageRoute();
const isActive = path === "/docs";
```
**Identifying the active route in layout components:**
```tsx theme={null}
const { routeId } = usePageRoute();
const showSidebar = routeId === "dashboard" || routeId === "analytics";
```
**Conditional rendering based on current page:**
```tsx theme={null}
const { path } = usePageRoute();
const showBreadcrumbs = path.includes("/support/");
```
**Analytics and tracking:**
```tsx theme={null}
const { path } = usePageRoute();
useEffect(() => {
trackPageView(path);
}, [path]);
```
## Related resources
* [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking)
* [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes)
* [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header)
* [Create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages)
* [App pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference)
# App pages reference
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference
Reference information for building app pages on the latest version of the developer platform.
Below, find reference information for building app pages.
## Project structure
To add app pages to an app, create a `pages` directory within `src/app`. The `pages` directory should contain:
* A JSON configuration file that defines the pages configuration (`Pages-hsmeta.json` is recommended).
* A React file that renders the pages component (`Pages.jsx` or `Pages.tsx` is recommended).
* A `package.json` file to handle any needed dependencies.
```shell theme={null}
project-folder/
└── src/
└── app/
├── app-hsmeta.json
└── pages/
├── pages-hsmeta.json
├── Pages.tsx
└── package.json
```
## App pages configuration
In the `*-hsmeta.json` configuration file for your app pages, include the properties below.
```json theme={null}
{
"uid": "my-app-pages",
"type": "page",
"config": {
"entrypoint": "/app/pages/Pages.tsx"
}
}
```
| Field | Type | Description |
| --------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `uid` | String | The pages component's unique identifier. This can be any string, but should meaningfully identify the component. |
| `type` | String | The type of component, which should be `page` in this case. |
| `config` | Object | An object containing configuration details. |
| `entrypoint` | String | The file path of the pages component's front-end React code. |
## Building the React front-end
The UI of app pages is created by a React component file, either `.jsx` or `.tsx`. This file lives in the `pages/` directory alongside the [pages configuration JSON file](#app-pages-configuration) (`*-hsmeta.json`). In the configuration file, you'll specify the path of the React file in the `entrypoint` field.
Routes are defined using `createPageRouter`, which accepts JSX route definitions and returns a React component. For complete examples, see the [create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages) guide.
## App page URLs
Your app pages can be accessed using the following URL patterns:
| Target | URL |
| ---------------------- | ------------------------------------------------------------------------------- |
| Home page | `https://app.hubspot.com/app/{hubId}/{appId}` |
| Home page with params | `https://app.hubspot.com/app/{hubId}/{appId}?param1Name=param1Value` |
| Named page | `https://app.hubspot.com/app/{hubId}/{appId}/{pagePath}` |
| Named page with params | `https://app.hubspot.com/app/{hubId}/{appId}/{pagePath}?param1Name=param1Value` |
For example, following the URL path structure above, if a user who installed your app has a HubSpot account ID (i.e., their `hubId`) of 12345 and your app ID was 67890, they could navigate to their home page at the following URL:
```
https://app.hubspot.com/app/12345/67890
```
## React page hooks
App pages provide hooks to access routing information within your page components.
### usePageRoute
Use the `usePageRoute` hook to access the current page path and all parameters (both path parameters and query parameters):
```tsx theme={null}
import { usePageRoute } from "@hubspot/ui-extensions/pages";
const MyPage = () => {
const { path, routeId, params } = usePageRoute();
// path will be "/" for the home page or the current page path like "/docs"
// routeId is the id assigned to the matched route (e.g., "home", "docs")
// params contains both path parameters and query parameters
return (
<>
Current path: {path}Route: {routeId}Params: {params.foo}, {params.anotherParam}
>
);
};
```
The `usePageRoute` hook returns an object with:
| Property | Type | Description |
| --------- | ------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `path` | String | The current normalized page path (e.g., `"/"`, `"/docs"`). |
| `routeId` | String | The `id` of the matched route, as defined on the route's `id` prop. Useful for identifying the active route in layout components. |
| `params` | Object | An object containing all parameters, including both path parameters (e.g., `:contactId`) and query parameters from the URL. |
## Programmatic navigation
You can navigate to app pages programmatically using the `navigateToPage` action from the `useExtensionActions` hook:
```tsx theme={null}
import { Button, hubspot, useExtensionActions } from "@hubspot/ui-extensions";
const MyComponent = () => {
const { navigateToPage } = useExtensionActions();
const handleNavigate = () => {
navigateToPage({
to: '/docs',
params: { foo: 'bar', anotherParam: 'abc' }
});
};
return (
);
};
```
The `navigateToPage` function accepts an options object with the following properties:
| Property | Type | Description |
| -------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `to` | String | The path to navigate to (e.g., `"/"` for home, `"/docs"` for a named page). The path will be normalized (e.g., `"docs"` becomes `"/docs"`). |
| `params` | Object | An object containing query parameters to include in the URL (e.g., `{ foo: 'bar' }`). |
## createPageRouter
The `createPageRouter` function accepts JSX route definitions and returns a React component that handles routing. Call it at the module level with your `` tree:
```tsx theme={null}
import { createPageRouter, PageRoutes } from "@hubspot/ui-extensions/pages";
const PageRouter = createPageRouter(
);
function App() {
return ;
}
```
### Parameters
| Parameter | Type | Description |
| ------------------------------ | ----------- | ----------------------------------------------------------- |
| `routes` | `ReactNode` | A `` JSX tree defining all routes for your app. |
### Returns
A React component (`PageRouter`) that you render in your app to activate routing.
## Page routing components
The `PageRoutes` component and its sub-components are used with [`createPageRouter`](#createpagerouter) to define routing for app pages:
* **`PageRoutes`**: Container for route definitions. Supports `layoutComponent` for wrapping child routes with shared UI.
* **`PageRoutes.IndexRoute`**: Defines the home page (index route) that renders at the root URL of your app.
* **`PageRoutes.Route`**: Defines a named page route with a `path` and `component`.
* **`PageRoutes.AnyRoute`**: Defines a catch-all route for unmatched paths, useful for custom 404 pages.
Each route sub-component also accepts an optional `id` prop. When a route matches, the `id` value is available as `routeId` from `usePageRoute()`. See the [route IDs](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing#route-ids) section in the routing guide for details.
For detailed documentation on routing components including nested routes, path parameters, and wildcard routes, see:
* [Page routing guide](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing)
* [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes)
## Manage dependencies
You can include dependencies for your app pages in a `package.json` file within the `pages/` directory. By default, when adding app pages through the `hs project add` command, a `package.json` file will be created for you with the following dependencies:
* `@hubspot/ui-extensions`
* `react`
* `typescript`
To install dependencies for project components with a `package.json` file, you can run the `hs project install-deps` command in your project directory.
```json theme={null}
{
"name": "hubspot-example-extension",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@hubspot/ui-extensions": "latest",
"react": "^18.2.0"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
```
## Related resources
* [Create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages)
* [Page routing](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing)
* [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking)
* [Testing app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/testing)
* [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header)
* [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes)
# Testing app pages
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/testing
Learn how to test app pages with mocked routing, paths, and parameters.
App pages support routing and navigation, which requires special consideration when writing tests. This guide shows you how to test app pages using the UI extensions SDK testing utilities with mocked routing state, paths, and parameters.
## Create mocks
When you create a renderer with `createRenderer('pages')`, the following mocks are available:
```tsx theme={null}
const { mocks, render, findByTestId } = createRenderer('pages');
// Mock the current route (controls usePageRoute)
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: { contactId: '123', tab: 'activity' }
});
// Mock navigation action
mocks.actions.navigateToPage; // FunctionSpy for testing navigation calls
```
Keep the following in mind as you mock out your components and their behavior:
* Use `'pages'` as the extension point location when creating a renderer
* Mock the current route using `mocks.router.setCurrentRoute()` with path and params
* Always render the full `` component with ``, not individual page components
* Use `findByTestId` for reliable component querying in tests
## Testing app pages with routing and parameters
This example demonstrates the most common testing scenario: rendering an app with routing and testing that the correct page component displays with the right parameters.
```tsx theme={null}
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { Text, Alert } from '@hubspot/ui-extensions';
import { createPageRouter, PageRoutes, usePageRoute } from '@hubspot/ui-extensions/pages';
// Define page components
const HomePage = () => Home Page;
const ContactDetailsPage = () => {
const { path, params } = usePageRoute();
const { contactId, tab } = params;
if (!contactId) {
return Missing contact ID;
}
return (
<>
Contact ID: {contactId}Tab: {tab || 'overview'}Path: {path}
>
);
};
const NotFoundPage = () => Page Not Found;
// Define the app structure with routes
const PageRouter = createPageRouter(
);
const AppPages = () => ;
// Test: home page renders
test('renders home page for root path', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/',
params: {}
});
render();
expect(findByTestId(Text, 'home-page')).toBeDefined();
});
// Test: page with path parameter
test('renders contact page with path parameter', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: { contactId: '123' }
});
render();
expect(findByTestId(Text, 'contact-id').text).toEqual('Contact ID: 123');
expect(findByTestId(Text, 'current-path').text).toEqual('Path: /contacts/:contactId');
});
// Test: page with path and query parameters
test('renders contact page with path and query parameters', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: {
contactId: '456',
tab: 'activity'
}
});
render();
expect(findByTestId(Text, 'contact-id').text).toEqual('Contact ID: 456');
expect(findByTestId(Text, 'active-tab').text).toEqual('Tab: activity');
});
// Test: error state when parameter is missing
test('shows error when contact ID is missing', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: { contactId: '' }
});
render();
expect(findByTestId(Alert, 'error-alert')).toBeDefined();
});
// Test: 404 page for unknown route
test('renders 404 page for unknown path', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/unknown-path',
params: {}
});
render();
expect(findByTestId(Text, '404-page')).toBeDefined();
});
```
Always render the full `` component with `` in your tests. This ensures you're testing the actual routing behavior, not just individual page components in isolation.
## Testing navigation
The following code blocks demonstrate how to test that your components call `navigateToPage` with the correct arguments:
```tsx theme={null}
import { Button } from '@hubspot/ui-extensions';
const GoToContactButton = ({ context }) => {
const handleClick = () => {
context.actions.navigateToPage({
pagePath: '/contacts/:contactId',
params: { contactId: '123', tab: 'activity' }
});
};
return (
);
};
test('navigates when button is clicked', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
render();
// Trigger navigation
findByTestId(Button, 'nav-button').trigger('onClick');
// Assert navigation was called
expect(mocks.actions.navigateToPage.called).toBe(true);
expect(mocks.actions.navigateToPage.callCount).toBe(1);
// Assert navigation arguments
const [navArgs] = mocks.actions.navigateToPage.calls[0];
expect(navArgs).toEqual({
pagePath: '/contacts/:contactId',
params: { contactId: '123', tab: 'activity' }
});
});
```
## Testing route IDs
When your components use `routeId` from `usePageRoute()`, include the `routeId` field in your `setCurrentRoute` call to mock the matched route's ID:
```tsx theme={null}
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { Text } from '@hubspot/ui-extensions';
import { createPageRouter, PageRoutes, PageBreadcrumbs, PageTitle, PageLink, usePageRoute } from '@hubspot/ui-extensions/pages';
function AppLayout({ children }: { children: ReactNode }) {
const { routeId } = usePageRoute();
const titles: Record = {
home: 'Home',
support: 'Support',
};
return (
<>
{titles[routeId]}
{children}
>
);
}
const PageRouter = createPageRouter(
);
const AppPages = () => ;
test('layout renders correct title based on routeId', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/support',
routeId: 'support',
params: {}
});
render();
expect(findByTestId(PageTitle, 'page-title').children).toEqual('Support');
});
```
## Testing recommendations and examples
The sections below provide guidance on how to test your app pages and their associated functionality:
### Always render the full app structure
Render `` with ``, not individual page components:
```tsx theme={null}
// Good - tests actual routing
const PageRouter = createPageRouter(
);
const AppPages = () => ;
test('renders contact page', () => {
const { mocks, render } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: { contactId: '123' }
});
render();
// ...
});
// Bad - bypasses routing
test('renders contact page', () => {
const { mocks, render } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: { contactId: '123' }
});
render(); // Doesn't test routing!
// ...
});
```
### Use findByTestId for reliable querying
Add `testId` props to components for stable, maintainable tests:
```tsx theme={null}
const ContactPage = () => {
const { params } = usePageRoute();
return (
<>
Contact: {params.contactId}Active
>
);
};
test('displays contact', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: { contactId: '123' }
});
render();
expect(findByTestId(Text, 'contact-id').text).toEqual('Contact: 123');
});
```
### Mock only what you need
Only mock the parameters your component actually uses:
```tsx theme={null}
// Good - minimal mocking
test('displays contact name', () => {
const { mocks, render } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: { contactId: '123' }
});
render();
// ...
});
// Unnecessary - extra params
test('displays contact name', () => {
const { mocks, render } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts/:contactId',
params: {
contactId: '123',
dealId: '456', // Unused
companyId: '789' // Unused
}
});
render();
// ...
});
```
### Test error states
Test how your pages handle missing or invalid parameters:
```tsx theme={null}
const ContactPage = () => {
const { params } = usePageRoute();
if (!params.contactId) {
return Missing contact ID;
}
return Contact: {params.contactId};
};
test('shows error when contact ID is missing', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/contacts',
params: {}
});
render();
expect(findByTestId(Alert, 'error')).toBeDefined();
});
```
### Remember parameters are strings
All parameters are strings. Convert types when needed:
```tsx theme={null}
const AnalyticsPage = () => {
const { params } = usePageRoute();
// Convert string params to proper types
const countNumber = parseInt(params.count || '0', 10);
const enabledBoolean = params.enabled === 'true';
return (
<>
Count: {countNumber}Enabled: {enabledBoolean ? 'Yes' : 'No'}
>
);
};
test('converts parameter types', () => {
const { mocks, render, findByTestId } = createRenderer('pages');
mocks.router.setCurrentRoute({
path: '/analytics',
params: { count: '42', enabled: 'true' }
});
render();
expect(findByTestId(Text, 'count').text).toEqual('Count: 42');
expect(findByTestId(Text, 'enabled').text).toEqual('Enabled: Yes');
});
```
## Related resources
Check out the following guides for additional guidance on testing and navigating between your pages:
* [Testing overview](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/overview)
* [Testing reference](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/reference)
* [Mocking](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/mocking)
* [Page routing](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing)
* [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking)
# Create a settings page
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page
Learn how to create a settings page for your app.
You can create a React-based settings page for your app on the latest versions (`2025.2` and `2026.03`) of the developer platform, that users who install your app can navigate to and customize for their account.
This guide walks you through how to build a settings component, using React, which replaces the [previous settings experience](/docs/apps/legacy-apps/public-apps/create-an-app-settings-page). If you deploy your new settings component, users who install your app going forward will immediately see your new settings extension instead of any previously built settings page.
Before you begin, follow the [quickstart guide](/docs/apps/developer-platform/add-features/app-objects/quickstart-guide-to-app-objects) to create your first app on the latest version of the developer platform.
**Please note:** if you previously created a [settings page](/docs/apps/legacy-apps/public-apps/create-an-app-settings-page) for your legacy app, you'll need to refactor your settings experience using the new configuration options outlined in this guide. It's recommended that you note all of the interface elements in the current production version of your app before you start configuring your new settings component on the latest version of the developer platform. Once you move to the new settings component for your app, you'll lose access to the previous WYSIWYG configuration UI you previously used.
## Prerequisites
* Ensure you've installed the latest beta version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli).
* If you haven't done so yet, [create a new app](/docs/getting-started/quickstart).
## Create an app settings page
To add a settings page component to an existing app, use the terminal to navigate into your local project directory, then run the following command:
```shell theme={null}
hs project add
```
Then, when prompted to select a component to add, select **Settings**.

A new `settings/` directory will be created in the project's `src/app/` directory. The `settings/` directory will contain:
* A JSON configuration file (`*-hsmeta.json`)
* A React component file (`.jsx`)
* A `package.json` file
```shell theme={null}
myProject
└── src/
└── app/
└── settings/
├── new-settings-page-hsmeta.json
├── NewSettingsPage.tsx
└── package.json
```
```json theme={null}
{
"uid": "settings_extension",
"type": "settings",
"config": {
"entrypoint": "/app/settings/NewSettingsPage.tsx"
}
}
```
```tsx theme={null}
import React from "react";
import { EmptyState, Text } from "@hubspot/ui-extensions";
import { hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ context }) => {
return ;
});
const NewSettingsPage = ({ context }) => {
return (
Build your application settings page here!
);
};
```
```json theme={null}
{
"name": "hubspot-example-extension",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@hubspot/ui-extensions": "latest",
"react": "^18.2.0"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
```
To upload your settings page to HubSpot:
* Run `hs project install-deps` from within your local project directory to install necessary dependencies. This will create a `package-lock.json` file, which will speed up the build of the uploaded settings extension, as well as ensure that any dependencies in your local development environment and production match.
* Then, run `hs project upload`.
* After the project finishes deploying, open the project in HubSpot by running `hs project open`. Alternatively, in HubSpot you can navigate to **Development** > **Projects**, then click the **name** of your project.
* Your settings component should now be listed on the details page.
## View the app settings page in HubSpot
To verify the settings component is working correctly:
* In the HubSpot account where you've installed the app, click the **Marketplace** icon, then click **Connected apps**.
* Click the **My apps** tab to view a list of the account's currently installed apps.
* Click the **name** of your app, which will redirect you to your app's overview page.
* On the overview page, click the **Settings** tab.
You can now continue to build out your app's settings page as needed. Similar to building app cards, you'll use the UI extensions SDK to add functionality and visual elements to your settings page. Note that all the existing limitations around building UI extensions apply to building a settings page.
* Use `hubspot.fetch` to leverage your backend to save and retrieve settings. Learn more about using this approach in the legacy documentation.
* Check out the reference documentation on [standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for how to use React components when building your extension, or use the component in the [Figma Design Kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit).
* Use the `hs project dev` command to iteratively build out your settings page and preview your changes locally.
The local development server will only pick up changes saved to the front-end React file. If you update the `*-hsmeta.json` or `package.json` files, you'll need to stop the server, upload your changes, then start the server again.
## Component best practices
The sections below outline several best practices to keep in mind as you build out the settings experience for your app.
### Organizing content
If you have enough content in your settings extension to warrant the need to separate and organize all of the user's settings data, you should consider using the [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel), [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal), [Accordion](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/accordion), and [Tabs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tabs) components accordingly. Consider how you want to present and arrange your settings and the corresponding data that should be fetched for each component.
### Tabs
If you're using [Tabs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tabs), use the `default` tab variant. The settings extension is already contained within an enclosed variant tab, and a second layer of enclosed tabs will visually clash with the design.
The following snippet outlines how to structure your tabs.
```jsx theme={null}
Here is the content of the first tab.This is where the content of the second tab goes.;
```
Refer to [individual component reference docs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for more best practices, guidelines, and code examples to get you started. HubSpot also provides design pattern guidance for [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/buttons), [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/forms), and [Table](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/tables) component usage.
# Fetching data for UI extensions
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/fetching-data
Learn the different ways to fetch data in UI extensions, including CRM hooks, actions, serverless functions, and the hubspot.fetch() API.
UI extensions have multiple ways to fetch data. The right approach depends on what data you need and where the extension runs.
| Method | When to use | Where it works |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| [`useCrmProperties`](#crm-data-hooks) | Fetch properties from the current CRM record with automatic state management, formatting, and live updates. | CRM extension points only |
| [`useAssociations`](#crm-data-hooks) | Fetch records associated with the current CRM record with pagination and formatting. | CRM extension points only |
| [`actions.fetchCrmObjectProperties`](#fetchcrmobjectproperties-action) | Fetch CRM property data with manual control over when fetching occurs (imperative) | CRM extension points only |
| [`hubspot.serverless()`](#serverless-functions) | Retrieve or write data server-side using JavaScript within HubSpot's infrastructure, removing the need to manage your own server. | All extension points |
| [`hubspot.fetch()`](#hubspot-fetch) | Send requests from your extension to a backend that you manage, or to a public endpoint. | All extension points |
If you receive raw property values via `actions.fetchCrmObjectProperties`, a `hubspot.serverless()` call that reads CRM data, or a `hubspot.fetch()` request to the HubSpot CRM APIs, you can pass the results to the [`formatCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/utilities) utility to apply HubSpot's display formatting. For example, resolve enumeration values to their display labels.
## CRM data hooks
The CRM data hooks fetch data using the context of the CRM record where an extension is displayed. They handle loading state, error handling, and property formatting automatically. They also offer utility functions for data refetching and pagination.
The CRM-specific hooks are only available in CRM extension points: `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, and `helpdesk.sidebar`.
Learn more about using CRM data hooks on the [hooks reference page](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#CRM-specific-hooks).
```jsx theme={null}
import { useCrmProperties, useAssociations } from "@hubspot/ui-extensions/crm";
```
## fetchCrmObjectProperties action
Use `actions.fetchCrmObjectProperties` to fetch property values from the current CRM record imperatively. For example, when you need to control exactly when fetching occurs rather than fetching on render.
This action is only available in CRM extension points: `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, and `helpdesk.sidebar`.
For most use cases, [`useCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usecrmproperties) is the better choice. It provides automatic state management, property formatting, and live updates when properties change.
Learn more about using `fetchCrmObjectProperties` on the [actions reference page](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#fetch-crm-property-data).
## Serverless functions
Use `hubspot.serverless()` to call a serverless function from your UI extension. Serverless JavaScript functions execute within HubSpot's infrastructure, which eliminates the need to manage your own external server.
Learn more about [serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/overview).
**Please note:** an *Enterprise* subscription is required to install an app with serverless functions. You can use a [developer test account](/docs/getting-started/account-types#developer-test-accounts) to test serverless functionality during development without an *Enterprise* subscription.
```jsx theme={null}
import { hubspot } from '@hubspot/ui-extensions';
const result = await hubspot.serverless('my_function', {
parameters: { key: 'value' },
propertiesToSend: ['firstname', 'email']
});
```
## hubspot.fetch()
Use `hubspot.fetch()` to make requests from a UI extension to your own backend or a third-party API. It works in all extension points (app cards, app homes, app settings, etc.).
To fetch data using this method, you'll need to:
* Provide a REST-based backend service to handle requests.
* Include the request URLs as `permittedUrls` in the [app's configuration](/docs/apps/developer-platform/build-apps/app-configuration).
**Please note:** apps that use [Sensitive Data scopes](/docs/api-reference/latest/crm/properties/sensitive-data) cannot use `hubspot.fetch()`, preventing Sensitive Data from being sent to external services.
### Specify permitted URLs to fetch from
To make calls to your backend or a third-party service, update the app's `*-hsmeta.json` [configuration file](/docs/apps/developer-platform/build-apps/app-configuration) to include the URLs you'll be requesting. The URLs must be specified in the `fetch` array of `permittedUrls`.
```json theme={null}
"permittedUrls": {
"fetch": ["https://api.example.com/api/data/"],
"img": [],
"iframe": []
}
```
Note the following when configuring permitted URLs:
* Fetch URLs must be valid HTTPS URLs and cannot be `localhost`. If you want to send requests to a locally running backend, learn how to [proxy requests](#proxying-requests-to-a-locally-running-backend).
* Entries in `permittedUrls.fetch` are treated as prefixes. A request to `https://api.example.com/api/data/users` will be allowed if `https://api.example.com/api/data/` is listed.
* Wildcards are not supported.
* If a URL is not included in `permittedUrls.fetch`, the request will fail with a 403 Forbidden error.
| `permittedUrls` entry | Request URL | Allowed? |
| ----------------------------------- | ---------------------------------------- | -------- |
| `https://api.example.com/api/data/` | `https://api.example.com/api/data/users` | Yes |
| `https://api.example.com/api/data/` | `https://api.example.com/other/endpoint` | No |
| `https://api.example.com/` | `https://api.example.com/api/data/users` | Yes |
When running the local development server, any `hubspot.fetch()` request will still go to your hosted backend via a HubSpot-managed data fetch service. If you need to update the allowlist while running the dev server, you'll need to run `hs project upload` for the change to take effect.
### Method
The method contract for `hubspot.fetch()` is as follows:
```js theme={null}
import { hubspot } from '@hubspot/ui-extensions';
interface Options {
method?: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'PATCH';
timeout?: number;
body?: {
[key: string | number]: unknown;
}
}
hubspot.fetch(resource: string | URL): Promise
hubspot.fetch(resource: string, options?: Options): Promise
```
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `method` | String | The HTTP method to use. |
| `timeout` | Number | Time in milliseconds to allow for the request to complete before timing out. Maximum value is 120000 (2 minutes). When not specified, the default timeout is 15 seconds. See [Limits](#limits) for retry behavior. |
| `body` | Object | The request body. |
**Please note:** for security reasons, you should not store secrets in your React code to communicate with third-party backends. Instead, use your own backend to fetch data from other third-party APIs after [validating the HubSpot signature](/docs/apps/legacy-apps/authentication/validating-requests) on the request.
### Headers and metadata
To ensure that the requests hitting your backend are coming from HubSpot, several headers are included in the request. You can use these headers, along with the incoming request fields, to verify the signature of the request. Learn more about [validating HubSpot requests](/docs/apps/legacy-apps/authentication/validating-requests).
HubSpot will always populate headers related to request signing and also allow you to pass a custom `Authorization` header from `hubspot.fetch()`. See the [example hubspot.fetch() request with Authorization header](#authorization-header) for more information. All other client-supplied request headers are stripped. Response headers from your backend are not returned.
HubSpot also automatically adds the following query parameters to each request to supply metadata:
* `userId`
* `portalId`
* `userEmail`
* `appId`
As the request URL is hashed as part of the signature header, this will help you securely retrieve the identity of the user making requests to your backend.
Note the following:
* While you can use `hubspot.fetch()` to pass an `Authorization` request header, `hubspot.fetch()` does not pass other client-supplied request headers or return response headers set by your backend server.
* If you're not seeing appended metadata passed with `hubspot.fetch()` requests, check whether you have a `local.json` file that's currently rerouting requests via a proxy. If so, disable this local data fetch feature by renaming the file to `local.json.bak` and restarting the development server.
### Limits
Requests made with `hubspot.fetch()` are subject to the following limits:
* **Concurrency:** each app is allowed up to 20 concurrent requests per account. Additional requests are rejected with `429` and can be retried after a delay.
* **Timeout:** each request has a default timeout of 15 seconds. It can be increased up to 120 seconds (2 minutes) using the `timeout` option. HubSpot will automatically retry a request once if there are issues establishing a connection, or if the request fails with a `5XX` status code within the timeout window. Request duration time includes the time required to establish an HTTP connection.
* **Payload size:** both request and response payloads are limited to 1 MB.
If you're familiar with using the [browser Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), note that `hubspot.fetch()` is not a one-to-one replacement. Key differences between the two APIs are listed below.
| Feature | Native `fetch()` | `hubspot.fetch()` |
| ---------------- | --------------------- | ----------------------------------- |
| Custom headers | All headers supported | `Authorization` header only |
| Response headers | Returned | Not returned |
| Credentials mode | Supported | Not supported |
| Cache control | Supported | Not supported |
| Timeout | Browser default | 15 seconds default, 120 seconds max |
| Payload size | Large | 1MB max |
| URL requirements | Any URL | Must be in `permittedUrls` |
### Proxying requests to a locally running backend
If you have a locally running backend, you can set up a proxy to remap `hubspot.fetch()` requests made during local development. This proxy is configured through a `local.json` file in your project, and will prevent requests from being routed through HubSpot's data fetch service.
To proxy requests to a locally running backend:
1. Create a `local.json` file in the same directory as your `public-app.json` file. In this file, define a proxy that remaps requests made using `hubspot.fetch()`. This mapping will only happen for the locally running extension. You can include multiple proxy key-value pairs in the `proxy` object.
```json theme={null}
{
"proxy": {
"https://example.com": "http://localhost:8080"
}
}
```
* Each proxy URL must be a valid URL and use HTTPS.
* Path-based routing is not supported. For example, the following proxy won't work: `"https://example.com/a": "http://localhost:8080"`
2. Upload your project by running `hs project upload`.
3. With your project uploaded, run `hs project dev` to start the development server. The CLI should confirm that it has detected your proxy.

4. When you request the mapped URL using `hubspot.fetch()`, the CLI will confirm the remapping.

#### Request signatures during proxied local development
By default, when proxying requests during local development, requests will not be signed or include metadata in query parameters. However, if you want to introduce request signing into the local development process, you can inject the `CLIENT_SECRET` environment variable into the local development process.
##### Injecting CLIENT\_SECRET
After setting up your `local.json` file to proxy specific domains, you can inject the `CLIENT_SECRET` variable when starting the local development server by prepending the `hs project dev` command with the variable:
```shell theme={null}
CLIENT_SECRET="abc123" hs project dev
```
Note that this doesn't have to be a real `CLIENT_SECRET` value, as long as you inject the same variable in your locally running backend that you're using for `hs project dev`. For example, your backend might include the following Node or Python code:
```js JavaScript theme={null}
CLIENT_SECRET="abc123" node my-app.js
// and also
CLIENT_SECRET="abc123" npm run dev
```
```py Python theme={null}
CLIENT_SECRET="abc123" python my-app.py
```
And to access the `CLIENT_SECRET` variable:
```js JavaScript theme={null}
console.log(process.env.CLIENT_SECRET);
```
```py Python theme={null}
import os
print(os.environ['CLIENT_SECRET'])
```
Once you've finalized your request signing logic and have permanently added it to your backend code, you'll need to inject the `CLIENT_SECRET` variable from your app into the `hs project dev` command permanently. For ease of use, consider baking the variable into your start scripts for local development.
To validate HubSpot request signatures in your custom backend, check out the [request validation guide](/docs/apps/legacy-apps/authentication/validating-requests). You can also use HubSpot's [`@hubspot/api-client`](https://www.npmjs.com/package/@hubspot/api-client) npm module to verify requests and sign them yourself. For example:
```js theme={null}
import { Signature } = from '@hubspot/api-client'
const url = `${req.protocol}://${req.header('host')}${req.url}`
const method = req.method;
const clientSecret = process.env.CLIENT_SECRET
const signatureV3 = req.header('X-HubSpot-Signature-v3');
const timestamp = req.header('X-HubSpot-Request-Timestamp');
// Reject the request if the timestamp is older than 5 minutes.
if (parseInt(timestamp, 10) < (Date.now() - 5 * 60 * 1000)) {
throw Error('Bad request. Timestamp too old.')
}
const requestBody = req.body === undefined || req.body === null
? ''
: req.body;
const validV3 = Signature.isValid({
signatureVersion: 'v3',
signature: signatureV3,
method,
clientSecret,
requestBody,
url,
timestamp,
});
if (!validV3) {
throw Error('Bad request. Invalid signature.')
}
```
### hubspot.fetch examples
Below are examples of `hubspot.fetch` requests to illustrate basic and authorization header usage.
#### Basic usage
```js theme={null}
import React, { useEffect } from "react";
import { hubspot, Text } from "@hubspot/ui-extensions";
hubspot.extend(({ context }) => );
const Hello = ({ context }) => {
useEffect(() => {
const fetchData = async () => {
const response = await hubspot.fetch("https://api.example.com/data", {
timeout: 2_000,
method: "GET",
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const data = await response.json();
console.log(data);
};
fetchData().catch(err => console.error("Something went wrong", err));
}, []);
return Hello world;
};
```
#### Authorization header
You may return a short-lived authorization token from your backend service after validating the HubSpot signature. You can then use this token to access other resources.
To get the access token from your backend server in the UI extension:
```js wrap theme={null}
hubspot.fetch(`${BACKEND_ENDPOINT}/get-access-token`, {
timeout: 3000,
method: 'GET',
})
.then((response: Response) => {
response.json().then((data) => setAccessToken(data.accessToken));
})
```
To return a short-lived access token from your backend server:
```js wrap theme={null}
app.get("/get-access-token", (req, res) => {
validateHubspotSignatureOrThrow(req);
res.json({
accessToken: generateShortLivedAccessToken(req.query.userEmail),
expiryTime: addMinutes(currentTime, 10),
});
});
```
To attach access tokens to other UI extension requests:
```js theme={null}
hubspot.fetch('https://www.oauth-enabled-api.com/', {
timeout: 3000,
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`
}
}).then((response: Response) => {
...
})
```
### Monitoring and logs
To monitor `hubspot.fetch()` activity, you can view request logs in HubSpot:
1. In your HubSpot account, navigate to **Development**.
2. In the left sidebar, navigate to **Monitoring** > **Logs**.
3. On the monitoring page, click the **UI Extensions** tab.
On the *UI Extensions* tab, use the **tabs** to view the various logs available. Learn more about [monitoring and logging for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring).
# Logging and monitoring for UI extensions
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring
Monitor and debug UI extensions during local development and after deployment using HubSpot's built-in logging and monitoring tools.
HubSpot provides tools for monitoring and debugging UI extensions throughout the development lifecycle. During local development, the extension logs panel surfaces errors, warnings, and custom log messages directly in the browser. For deployed extensions, HubSpot's built-in monitoring page lets you review render logs, `hubspot.fetch()` requests, and custom log output.
## Logging during local development
To help you identify and resolve issues before deploying a UI extension, HubSpot provides a logs panel that you can access from locally running UI extensions. The panel will surface errors, warnings, and other debugging information related to your extension code, such as invalid UI component prop values.
### Using the logs panel
When an error or warning is detected in the locally running UI extension, a button will appear in the bottom right of the extension, which you can click to open the logs panel.
Each log entry includes the severity level and a message describing the issue. When applicable, log entries may include links to relevant documentation to help you resolve the issue. The panel will display up to 50 of the most recent logs collected during your current local development session.
### Hiding the logs panel
If you prefer not to see the logs panel during local development, you can hide it by setting the following value in your browser's `localStorage`:
```js theme={null}
localStorage.setItem('hubspot::Extensions:logs:disabled', 'true')
```
To re-enable the logs panel, remove the key or set it to `'false'`:
```js theme={null}
localStorage.removeItem('hubspot::Extensions:logs:disabled')
```
### Custom log messages
In addition to the logs that are automatically surfaced by the extension logs panel, you can send your own log messages using the [`logger` API](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/logging) provided by the UI extensions SDK.
You can test out custom log messages using the example code below, which creates an extension with buttons to trigger specific errors:
```jsx wrap expandable theme={null}
import React from "react";
import { Button, Divider, Flex, Text, hubspot, logger, } from "@hubspot/ui-extensions";
logger.warn("Warning in the middle tab, before my extension");
hubspot.extend(({ context }) => );
const MiddleTabLogging = ({ context }) => {
logger.debug(JSON.stringify(context, null, 2));
const callFetchSuccess = async () => {
try {
const response = await hubspot.fetch("https://jsonplaceholder.typicode.com/posts/1", { method: "GET" });
const result = await response.json();
logger.info(JSON.stringify(result, null, 2));
} catch (error) {
logger.error(error.message);
}
};
const callFetchFail = async () => {
try {
const response = await hubspot.fetch("https://jsonplaceholder.typicode.com/posts/404", { method: "GET" });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
logger.info(JSON.stringify(result, null, 2));
} catch (error) {
logger.error(error.message);
}
};
return (
Test out the logger with the following buttons.The browser's developer console will show your events in local dev.Test fetch functionsTest different log levels.
Deploy the app and crash the card. Use the Trace ID to see what happened in the Log Traces tab in your private
app's dashboard.
);
};
```
## Monitoring deployed extensions
To monitor UI extension activity, you can view request logs in HubSpot:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar, navigate to **Monitoring** > **Logs**.
* On the monitoring page, click the **UI Extensions** tab.
On the *UI Extensions* tab, use the various **tabs** to view the different log types:
* **Extension Render**: logs related to UI extensions loading in their configured locations.
* **hubspot.fetch()**: logs related to `hubspot.fetch()` requests.
* **Extension Log**: custom messages logged via the [logger API](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/logging).
To view more information about a log entry, click the **ellipsis** button in the *Actions* column of the row, then select an action:
* **Open details:** review the details for the entry and use the provided IDs for deeper debugging.
* **Open tracing:** view the complete tracing details for the log entry.
# UI extensions overview
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/overview
Learn about the different types of UI extensions and how to build them.
HubSpot UI extensions allow you to build rich, contextual custom interfaces in HubSpot. Tailor CRM records, workflows, and app experiences with dynamic data, custom layouts, and interactive React-based components, all powered by the [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/overview).
Below, learn more about when to use UI extensions, how they work, and how to get started.
If you have an existing legacy CRM card, check out HubSpot's [Legacy CRM Card to UI Extension Converter](https://github.com/HubSpot/ui-extensions-examples/tree/main/legacy-card-converter), which provides an example implementation of an app card that functions like a legacy CRM card.
## When to use UI extensions
UI extensions are ideal when:
* You want to display external system data in HubSpot.
* You need to guide users through multi-step flows.
* You want to enable users to customize CRM records with richer functionality than HubSpot's default UI allows.
* You want HubSpot to host your app's configuration UI.
* You need a secure and scalable way to embed custom UI logic without hosting your own frontend architecture.
## How UI extensions work
At a high level, a UI extension consists of:
* A JSON configuration file (`*-hsmeta.json`) that registers where the extension appears in HubSpot and specifies the entry point.
* An entry point (`hubspot.extend()`) that initializes the extension and receives context and actions from HubSpot.
* A React component (`.jsx` or `.tsx`) that defines your UI. TypeScript is fully supported and recommended for better type safety.
* Optional data fetching via the `hubspot.fetch()` API to request data from external services.
During runtime:
1. HubSpot loads your extension in a secure, sandboxed environment.
2. Context is passed into your component, including information about the current user, HubSpot account, and (for CRM extensions) the current record's ID and object type.
3. Your component can fetch CRM data using [SDK hooks](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks) or call external services using `hubspot.fetch()`.
4. The UI renders and reacts to user interactions.
This architecture ensures secure isolation, predictable performance, and a consistent developer experience.
UI extensions are subject to the following limitations:
* Each `hubspot.fetch()` request has a default timeout of 15 seconds, configurable up to 120 seconds (2 minutes) using the `timeout` option.
* Request and response payloads are limited to 1 MB each.
* Each app is allowed up to 20 concurrent `hubspot.fetch()` requests per account.
* `hubspot.fetch()` does not support custom request or response headers (except for the `Authorization` header).
## What you can build
### App cards
Create app cards to surface insights, trigger external workflows, or bring data from third-party services directly into HubSpot. You can build app cards for [CRM records](https://knowledge.hubspot.com/records/view-and-filter-records), [preview panels](https://knowledge.hubspot.com/records/preview-a-record), and the [help desk](https://knowledge.hubspot.com/help-desk/overview-of-the-help-desk-workspace) sidebar.
Common use cases include:
* Displaying enriched company or contact data.
* Embedding task lists, timeline details, or customer insights.
* Triggering external workflows or actions from within a record.
* Rendering custom layouts, accordions, tables, and charts.
Learn more about [building app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview).
Apps that use [Sensitive Data scopes](/docs/api-reference/latest/crm/properties/sensitive-data) can include app cards, but cannot use `hubspot.fetch()` or serverless functions. Learn more about [Sensitive Data restrictions for app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview#sensitive-data-scopes).
### App home pages
A home page is a full-screen extension, ideal for dashboards, analytics views, and complex workflows.
Common use cases include:
* Building analytics dashboards.
* Enabling multi-step flows or guided processes.
* Aggregating cross-object CRM insights.
* Creating custom workspace experiences.
Learn more about [building app home pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview).
### Settings pages
Settings pages allow you to build configuration UIs that live inside HubSpot’s settings. These pages are ideal for:
* Managing API keys or OAuth credentials.
* Managing feature toggles or custom configuration.
* Connecting external services.
* Mapping CRM properties to external systems.
Learn more about [building app settings pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page).
## Building UI extensions
All UI extensions share the same development workflow:
1. **Create the extension:** run the `hs project add` command to add a card, home page, or settings page component to an existing project. Or, follow the [quickstart](/docs/getting-started/quickstart) to build your first extension.
2. **Build with React:** build your extension's UI with React or TypeScript, using HubSpot's [UI component library](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) to render elements.
3. **Register with the SDK:** use `hubspot.extend()` to register your extension with HubSpot.
4. **Upload and test:** run `hs project upload` to deploy, and `hs project dev` for local development. You can also build testing into your project using a set of provided [testing utilities](#local-development-and-testing).
### Supported locations
UI extensions can appear in specific locations throughout HubSpot. By default, an app card is eligible for every surface that's compatible with its card type and `objectTypes`, and account admins choose where to place it. To restrict a card to one surface, set the `location` field in its JSON configuration file:
```json highlight={7} theme={null}
{
"uid": "example-card",
"type": "card",
"config": {
"name": "Hello Example App",
"description": "A description of the card's purpose.",
"location": "crm.record.tab",
"entrypoint": "/app/cards/ExampleCard.jsx",
"objectTypes": ["contacts"]
}
}
```
| Location | Value | Description |
| ------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CRM record middle column | `crm.record.tab` | Appears in the middle column of CRM record pages, within default or custom tabs. Also available in the sales workspace target accounts preview panel when `objectTypes` is `COMPANIES`. |
| CRM record sidebar | `crm.record.sidebar` | Appears in the right sidebar of CRM record pages. Also available in the sales workspace deals sidebar when `objectTypes` is `DEALS`. |
| CRM preview panel | `crm.preview` | Appears in the preview panel accessible throughout the CRM, including record pages, index pages, board views, and the lists tool. |
| Help desk sidebar | `helpdesk.sidebar` | Appears in ticket sidebars within help desk, including the ticket preview panel and the right sidebar of ticket views. |
| App pages | `home` | Appears in the [app home page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages). |
| App settings page | `settings` | Appears in the [app settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page). |
Learn more about supported objects and location configuration in the [app cards reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference#card-locations).
### The UI extensions SDK
The [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/overview) provides the foundation for all UI extensions, including:
* **Context access**: retrieve information about the current user, account, and CRM record.
* **Actions**: display alerts, copy text to clipboard, reload pages, and open overlays.
* **Hooks**: fetch extension metadata, CRM properties, and associations.
### Fetching external data
Use the `hubspot.fetch()` API to request data from external services. Learn more about [fetching data for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data), including:
* Configuring permitted URLs.
* Request signing and validation.
* Proxying requests during local development.
### UI components
HubSpot provides a library of reusable components for building extension interfaces, including:
* **[Standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview#standard-components)**: general-purpose components like buttons, forms, tables, and layout components. Imported from `@hubspot/ui-extensions`.
* **[CRM data components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview)**: components that automatically fetch and display data from CRM records, such as property lists, association tables, and reports. Imported from `@hubspot/ui-extensions/crm`.
* **[CRM action components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview)**: components that trigger built-in CRM actions like adding notes, sending emails, or creating records. Imported from `@hubspot/ui-extensions/crm`.
Check out HubSpot's [Figma Design Kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit) to help you create more detailed designs, share demos with stakeholders, and keep teams aligned during the development process.
### Local development and testing
Use the `hs project dev` command to start a local development server with hot reload. This enables you to preview changes to your extension in real-time without needing to upload your project after every change.
To help debug during local development, HubSpot provides an extension logs panel that surfaces errors, warnings, and custom log messages directly in the browser.
For deployed extensions, you can review render logs, `hubspot.fetch()` requests, and custom log output. Learn more about [logging and monitoring for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring).
The UI extensions SDK also provides [testing utilities](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/overview) for writing unit tests, including:
* Rendering components in a test environment.
* Querying and interacting with rendered output.
* Mocking context, actions, and CRM data hooks.
* Debugging rendered component trees.
### Code quality and linting
HubSpot provides an ESLint configuration specifically designed for UI extensions. This catches common issues like using unavailable browser APIs and incorrect imports before runtime. Learn more about [linting for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview).
## Next steps
* [Quickstart](/docs/getting-started/quickstart): create your first app on the developer platform.
* [Create an app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card): create a custom card that displays on CRM records.
* [Create a home page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview): build a landing page for your app.
* [Create a settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page): add a configuration interface for your app.
# Share code between your extensions using npm workspaces
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/code-sharing-with-npm-workspaces
Learn how to use npm workspaces to share code between your UI extensions.
As you build out your UI extensions, you can leverage [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces/) to share code across your [app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card), [settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page), [app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages), and [serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/overview). This allows you to consolidate common logic such as formatting dates and currency, and ensure that any fixes or updates are rolled out consistently to each UI extension.
## Prerequisites
Before getting started, install the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). In a terminal window, run the following command:
```shell theme={null}
npm install -g @hubspot/cli
```
The CLI needs to be version `8.0.0` or later.
* You can check which version of the CLI you have by running `hs --version`.
* If needed, you can run the command `npm install -g @hubspot/cli@latest` to update to the latest version of the HubSpot CLI.
## Project structure
The code for your card, setting page, and home page follows the same directory structure as outlined in the [app configuration article](/docs/apps/developer-platform/build-apps/app-configuration). The main change you'll need to make to set up code sharing between your extensions will be to add a `package.json` file at the root of your project under `src/app/`. The naming convention and scope you choose for these shared packages is entirely customizable, allowing you to use a pattern you're already comfortable with, such as `@packages/types`.
An example directory structure that demonstrates the root `package.json` location, along with the shared code for `types`, `utils`, and `components` is shown below:
```shell highlight={6-16} theme={null}
my-hubspot-project/
└── hsproject.json
└── src/
└── app/
└── app-hsmeta.json
└── package.json
└── packages/
└── types/
└── package.json
└── index.ts
└── utils/
└── package.json
└── index.ts
└── components/
└── package.json
└── index.tsx
└── cards/
└── MyCard.jsx
└── my-app-card-hsmeta.json
└── package.json
└── settings/
└── Settings.tsx
└── settings-hsmeta.json
└── package.json
└── pages/
└── Home.tsx
└── package.json
```
## Configure npm workspaces
After you create a `package.json` file in the `src/app/` directory of your project, open the `package.json` file and define the `name` and `workspaces` fields:
* `name`: the name of your project (e.g., `my-hubspot-project`).
* `workspaces`: an array that includes the UI extension directories where you plan to use your shared code, along with the directory where your shared code lives (e.g., `packages/*`).
For example, following the same directory structure shown above, the resulting `package.json` file would be:
```json theme={null}
{
"name": "my-hubspot-project",
"workspaces": ["cards", "settings", "pages", "packages/*"]
}
```
## Create shared packages
You can use any naming convention when defining the package scope for your shared code (e.g., `@my-hubspot-project` in the examples below).
The code blocks below provide examples for defining shared `types`, `utils`, and `components` that you can then import into your UI extensions.
### Types
The examples below define a simple `Money` type under `packages/types` in your project.
```json theme={null}
{
"name": "@my-hubspot-project/types",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}
```
```js theme={null}
export interface Money {
amount: number;
currency: 'USD' | 'EUR' | 'GBP';
}
```
### Utilities
The examples below define a utility under `packages/utils` that will format data using the `Money` type from above.
```json theme={null}
{
"name": "@my-hubspot-project/utils",
"private": true,
"version": "1.0.0",
"main": "index.ts",
"dependencies": {
"@my-hubspot-project/types": "1.0.0"
}
}
```
```js theme={null}
import { Money } from "@my-hubspot-project/types";
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
export function formatMoney(money: Money): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: money.currency,
}).format(money.amount);
}
```
### Components
The examples below demonstrate a shared component that lives under `packages/components`.
```json theme={null}
{
"name": "@my-hubspot-project/components",
"private": true,
"version": "1.0.0",
"main": "index.tsx",
"dependencies": {
"@hubspot/ui-extensions": "latest",
"react": "^18.2.0"
}
}
```
```js theme={null}
import { Text, Flex, Divider } from "@hubspot/ui-extensions";
interface SectionProps {
title: string;
children: React.ReactNode;
}
export const Section = ({ title, children }: SectionProps) => (
{title}
{children}
);
```
## Add dependencies to extensions
After you've defined your shared code under `packages/`, add the path to your shared packages in each of your extension's respective `package.json` files:
```json theme={null}
{
"name": "@my-hubspot-project/my-cards",
"dependencies": {
"@hubspot/ui-extensions": "latest",
"@my-hubspot-project/types": "1.0.0",
"@my-hubspot-project/components": "1.0.0",
"@my-hubspot-project/utils": "1.0.0"
}
}
```
## Use shared code in your UI extensions
The following code block shows how to use your shared code within one of your UI extensions:
```jsx theme={null}
import { hubspot } from "@hubspot/ui-extensions";
import { Text } from "@hubspot/ui-extensions";
import { Money } from "@my-hubspot-project/types";
import { Section } from "@my-hubspot-project/components";
import { formatDate, formatMoney } from "@my-hubspot-project/utils";
const dealAmount: Money = { amount: 15000, currency: 'USD' };
hubspot.extend<'crm.record.tab'>(({ context }) => (
Amount: {formatMoney(dealAmount)}Close Date: {formatDate('2024-03-15')}
));
```
After you've added the imports of your shared code in your UI extensions, you can proceed with the next step of installing the shared packages so the project will build.
## Install shared packages
To ensure your shared packages can be imported successfully in your UI extension, run the following command anywhere in your project:
```shell theme={null}
hs project install-deps
```
Alternatively, your shared packages will automatically be installed next time you run `hs project dev` or `hs project upload`.
# Migrating to v1.0 (ESLint 9)
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/migrate
Migrate an existing ESLint configuration to ESLint 9.
Version 1.0 is a major update that moves to ESLint 9's [flat config format](https://eslint.org/docs/latest/use/core-concepts/glossary#flat-config) and unbundles third-party rules to give you more control over your linting setup. The flat config files don't allow nesting configuration files in subdirectories -- instead, all nesting must be in one configuration file.
If you already have an ESLint configuration in your project, review the table below to see what's changed:
| Before (v0.x) | After (v1.0) |
| ----------------------------------------------- | ----------------------------------- |
| ESLint 8 | ESLint 9+ required |
| Legacy config (`.eslintrc.*`) | Flat config (`eslint.config.js`) |
| Bundled `eslint:recommended` | Install separately via `@eslint/js` |
| Bundled `eslint-plugin-react` recommended rules | Install and configure separately |
| Bundled `eslint-plugin-react-hooks` | Install and configure separately |
| Bundled `eslint-config-prettier` | Install and configure separately |
| CommonJS (`require()`) | ESM (`import`) |
After reviewing the above changes, proceed with the steps below to migrate to the latest version.
## Migrate your ESLint config
### 1. Review your current config
Before migrating, open your existing `.eslintrc.*` file and note:
* Any custom `rules` you've added or modified
* Any additional `plugins` you're using
* Any `overrides` for specific file patterns
* Any `env` or `globals` settings
You'll need to translate these to flat config format. Keep your old config file for reference until migration is complete.
### 2. Update dependencies
Upgrade to ESLint 9 and the new package version by running the following command:
```shell theme={null}
npm i --save-dev eslint@^9 @hubspot/eslint-config-ui-extensions@^1
```
### 3. Set up ESM
ES modules is required for a flat configuration, so you'll need to ensure your `package.json` includes `"type": "module"`:
```json theme={null}
{
"type": "module"
}
```
### 4. Create a new ESLint config file
Create an `eslint.config.js` file in your project root. Start with either the minimal or recommended setup below, then add your custom rules. Read more about each addition in the [Recommended setup section of the overview](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview#recommended-setup).
**Minimal setup** (HubSpot UI extensions rules only):
```js theme={null}
import { config } from '@hubspot/eslint-config-ui-extensions';
export default [
...config,
// Add your custom rules here (see Step 5)
];
```
**Recommended setup** (replaces previously bundled configs, plus extras):
```shell theme={null}
npm i --save-dev @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-config-prettier eslint-plugin-unused-imports
```
```js theme={null}
import { defineConfig } from 'eslint/config';
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import unusedImports from 'eslint-plugin-unused-imports';
import prettier from 'eslint-config-prettier';
import { config as uiExtensionsConfig } from '@hubspot/eslint-config-ui-extensions';
export default defineConfig([
js.configs.recommended,
tseslint.configs.recommended,
tseslint.configs.stylistic,
react.configs.flat.recommended,
react.configs.flat['jsx-runtime'],
{
settings: {
react: { version: 'detect' },
},
},
...uiExtensionsConfig,
reactHooks.configs.flat['recommended-latest'],
{
plugins: {
'unused-imports': unusedImports,
},
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
'react/prop-types': 'off',
},
},
prettier,
// Add your custom rules here (see Step 5)
]);
```
### 5. Migrate your custom configuration
Refer to your old config file and translate your customizations to flat config format.
Custom rules:
```js theme={null}
// Before (.eslintrc.js)
module.exports = {
rules: {
'no-console': 'warn',
'prefer-const': 'error',
},
};
// After (eslint.config.js) - add to your config array
{
rules: {
'no-console': 'warn',
'prefer-const': 'error',
},
}
```
Overrides (file-specific rules):
```js theme={null}
// Before (.eslintrc.js)
module.exports = {
overrides: [
{
files: ['*.test.js'],
rules: {
'no-console': 'off',
},
},
],
};
// After (eslint.config.js) - add to your config array
{
files: ['**/*.test.js'],
rules: {
'no-console': 'off',
},
}
```
Globals:
```js theme={null}
// Before (.eslintrc.js)
module.exports = {
globals: {
myGlobal: 'readonly',
},
};
// After (eslint.config.js) - add to your config array
{
languageOptions: {
globals: {
myGlobal: 'readonly',
},
},
}
```
Environments:
Flat config uses `globals` instead of `env`.
```js theme={null}
// Before (.eslintrc.js)
module.exports = {
env: {
browser: true,
node: true,
},
};
// After (eslint.config.js)
import globals from 'globals';
export default [
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
// ... rest of config
];
```
For more details, see the [official ESLint migration guide](https://eslint.org/docs/latest/use/configure/migration-guide).
### 6. Testing and cleanup
Run ESLint to verify your migration:
```shell theme={null}
npm run lint
```
Once you've verified that everything works, you can delete your old `.eslintrc.*` file.
## Troubleshooting
### ESLint seems to ignore your new config
ESLint 9 looks for `eslint.config.js` by default and ignores `.eslintrc.*` files. If it seems like ESLint is ignoring your new config, make sure:
1. You created `eslint.config.js` in your project root.
2. The file is correctly exporting an array (check for syntax errors).
### Error: Cannot use require() to import an ES module
Your config file is being loaded as CommonJS. Make sure:
1. Your `package.json` has `"type": "module"`.
2. Your config file is named `eslint.config.js` (not `.cjs`).
### Errors about missing plugins or rules
If you see errors like `Definition for rule 'react/prop-types' was not found`, you're using a rule from a plugin that was previously bundled but is now opt-in. Either:
1. Install the plugin and add it to your config (see [recommended setup](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview#recommended-setup)).
2. Remove the rule if you no longer need it.
### Error: Invalid option 'extends'
Flat config uses a different structure than legacy config. You can't use `extends`, `env`, `plugins` (as an array), or `overrides` the same way. See [Step 5 above](#5-migrate-your-custom-configuration) for translation examples.
# Linting for UI extensions
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview
An overview of ESLint shareable configurations for building UI extensions on HubSpot with React.
For security and reliability, UI extensions run in a sandboxed web worker environment with restricted APIs. This can cause issues or unexpected behavior when developing UI extensions, especially if you're not fully familiar with the UI extensions environment.
The `@hubspot/eslint-config-ui-extensions` package provides ESLint rules specifically designed for HubSpot UI extensions, and catches common issues like using unavailable browser APIs, incorrect imports, and other patterns that won't work in the sandboxed web worker context.
It's recommended to set up ESLint with this configuration when starting a new UI extensions project, but can be implemented at any time.
## Automatic configuration
If you don't yet have linting configured and want to set it up automatically, you can run the `hs project lint` command from the root of your project to set up the necessary configuration files and dependencies.
## Manual installation
**Upgrading from v0.x?** See the [migration guide](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/migrate) for step-by-step instructions.
Install [ESLint](https://eslint.org/) 9+ alongside this package:
```shell theme={null}
npm i --save-dev eslint@^9 @hubspot/eslint-config-ui-extensions
```
## Basic usage
Create an `eslint.config.js` file in the root of your project:
```js theme={null}
import { config } from '@hubspot/eslint-config-ui-extensions';
export default [
...config,
// Your project-specific overrides
];
```
Make sure your `package.json` has `"type": "module"` set, then add a lint script:
```json theme={null}
{
"type": "module",
"scripts": {
"lint": "eslint ."
}
}
```
Run `npm run lint` to check for linting issues.
## Recommended setup
The basic config above only includes HubSpot UI extensions-specific rules. For a production-ready setup, it's recommended that you add standard ESLint rules, TypeScript support, React Hooks linting, Prettier compatibility, and unused import detection.
### Install additional dependencies
```sh theme={null}
npm i --save-dev @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-config-prettier eslint-plugin-unused-imports
```
### Full config
```js theme={null}
import { defineConfig } from 'eslint/config';
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import unusedImports from 'eslint-plugin-unused-imports';
import prettier from 'eslint-config-prettier';
import { config as uiExtensionsConfig } from '@hubspot/eslint-config-ui-extensions';
export default defineConfig([
// Standard ESLint recommended rules
js.configs.recommended,
// TypeScript support with stylistic rules
tseslint.configs.recommended,
tseslint.configs.stylistic,
// React recommended rules + JSX runtime support (React 17+)
react.configs.flat.recommended,
react.configs.flat['jsx-runtime'],
{
settings: {
react: { version: 'detect' },
},
},
// HubSpot UI extensions rules
...uiExtensionsConfig,
// React Hooks rules
reactHooks.configs.flat['recommended-latest']
// Unused imports detection (auto-fixable with --fix)
{
plugins: {
'unused-imports': unusedImports,
},
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
'react/prop-types': 'off',
},
},
// Prettier compatibility (must be last to override other configs)
prettier,
]);
```
### What each addition provides
| Package | Purpose |
| :----------------------------- | :--------------------------------------------------------------------------------------------------------- |
| `@eslint/js` | ESLint's built-in recommended rules for catching common JavaScript issues |
| `typescript-eslint` | TypeScript parsing and linting rules (`stylistic` adds consistent code style enforcement) |
| `eslint-plugin-react` | React-specific rules; `jsx-runtime` config is for React 17+ (no need to import React in every file) |
| `eslint-plugin-react-hooks` | Enforces [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) and verifies dependency arrays |
| `eslint-config-prettier` | Disables ESLint rules that conflict with Prettier formatting |
| `eslint-plugin-unused-imports` | Auto-fixable detection and removal of unused imports |
### JavaScript-only projects
If you're not using TypeScript, you can simplify the config:
```js theme={null}
import { defineConfig } from 'eslint/config';
import js from '@eslint/js';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import unusedImports from 'eslint-plugin-unused-imports';
import prettier from 'eslint-config-prettier';
import { config as uiExtensionsConfig } from '@hubspot/eslint-config-ui-extensions';
export default defineConfig([
js.configs.recommended,
react.configs.flat.recommended,
react.configs.flat['jsx-runtime'],
{
settings: {
react: { version: 'detect' },
},
},
...uiExtensionsConfig,
reactHooks.configs.flat['recommended-latest'],
{
plugins: {
'unused-imports': unusedImports,
},
rules: {
'no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
'react/prop-types': 'off',
},
},
prettier,
]);
```
## Rules
The rules included in the shared config from `@hubspot/eslint-config-ui-extensions` are enabled by default, and should always be enabled when building UI extensions.
| Name | Description |
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [no-browser-dialogs](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-browser-dialogs) | Prevent usage of native browser dialog APIs (alert, confirm, and prompt) in UI extensions. |
| [no-browser-storage](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-browser-storage) | Prevent usage of browser storage APIs (`localStorage` and `sessionStorage`) in UI extensions, which run in a sandboxed web worker environment where these APIs are not available. |
| [no-console](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-console) | Prevent usage of console methods in UI extensions in favor of the SDK logger utility. |
| [no-dom-access](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-dom-access) | Prevent access to the DOM (`document` object) in UI extensions, which run in a sandboxed web worker without DOM access. |
| [no-html-elements](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-html-elements) | Disallow usage of unsupported HTML elements in UI extensions. |
| [no-invalid-extension-point-imports](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-invalid-extension-point-imports) | Prevent importing components from unsupported extension points. |
| [no-invalid-image-src](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-invalid-image-src) | Only allow valid image URLs in the src attribute of Image components from @hubspot/ui-extensions. |
| [no-native-http](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-native-http) | Prevent usage of native browser HTTP APIs (fetch and XMLHttpRequest) in UI extensions. |
| [no-parent-imports](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-parent-imports) | Prevent importing files from outside the extension point root directory. This rule can be automatically fixed by the [`--fix` CLI option](https://eslint.org/docs/latest/use/command-line-interface#--fix). |
| [no-restricted-globals](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-restricted-globals) | Prevent usage of browser globals that are not available in the sandboxed web worker environment. |
# no-browser-dialogs
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-browser-dialogs
Prevent usage of native browser dialog APIs (alert, confirm, and prompt) in UI extensions.
The `no-browser-dialogs` rule prevents usage of the following native browser dialog APIs: [alert()](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert), [confirm()](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm), and [prompt()](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt).
## Rule details
UI extensions run in a sandboxed web worker environment where native browser dialog APIs (`alert()`, `confirm()`, and `prompt()`) are intentionally replaced with error-throwing functions for security reasons.
These blocking dialogs would freeze the entire UI and negatively impact user experience. Instead, UI extensions provide non-blocking alternatives that integrate seamlessly with the HubSpot interface.
## Dialog method alternatives
Use the following UI extension alternatives instead of browser dialog APIs:
| Native API | Alternative | Purpose |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| `alert()` (for pop-ups) | [`actions.addAlert()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners) | Display toast notifications |
| `alert()` (for inline alerts) | [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) component | Display inline alerts |
| `confirm()` | [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) + [`
);
};
```
### Prompts
Instead of using `prompt()`:
```jsx theme={null}
// Incorrect
function getUserInput() {
const name = prompt('Enter your name:');
return name;
}
// Also incorrect
const value = self.prompt('Enter a value:', 'default');
```
Use a combination of [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) and [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/input) components:
```jsx theme={null}
import {
Button,
Input,
Modal,
ModalBody,
ModalFooter,
Text,
} from '@hubspot/ui-extensions';
import { useState } from 'react';
const Extension = ({ actions }) => {
const [inputValue, setInputValue] = useState('');
const handleSubmit = () => {
actions.addAlert({
type: 'success',
message: `You entered: ${inputValue}`,
});
};
return (
setInputValue(value)}
/>
{
handleSubmit();
closeOverlay();
}}
>
Submit
closeOverlay()}
>
Cancel
}
>
Get Input
);
};
```
## Related resources
* [actions.addAlert()](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners)
* [Alert Component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert)
* [Modal Component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal)
# no-browser-storage
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-browser-storage
Prevent usage of browser storage APIs (localStorage and sessionStorage) in UI extensions.
The `no-browser-storage` rule prevents usage of browser storage APIs ([localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)).
## Rule details
UI extensions run in a sandboxed web worker environment where browser storage APIs (`localStorage` and `sessionStorage`) are intentionally unavailable for security and isolation. This prevents extensions from accessing storage data from the host application, storing sensitive data without proper security controls, or interfering with other extensions.
Storage APIs don't exist at runtime and will throw errors if accessed. Instead, use React state for temporary data or backend APIs for persistent data.
## Storage alternatives
Use the following UI extension alternatives instead of browser storage APIs:
| Use Case | Alternative | Purpose |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| Temporary session data | [React state](https://react.dev/learn/managing-state) (`useState`, `useReducer`) | Store component state |
| Persistent user data | [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#hubspot-fetch) + backend API | Store data across sessions |
| Complex app state | [React Context](https://react.dev/reference/react/createContext) + `useReducer` | Manage shared state |
| API response caching | React state with [custom hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) | Cache data in memory |
See the next section for an example of each alternative.
## Examples
### Temporary data
Instead of using `sessionStorage` for temporary form data:
```jsx theme={null}
// Incorrect
function cacheApiResponse(endpoint, data) {
sessionStorage.setItem(`cache:${endpoint}`, JSON.stringify(data));
}
const Extension = () => {
const cached = sessionStorage.getItem('formData');
return {cached};
};
```
Use React state:
```jsx theme={null}
import { useState } from 'react';
import { Input, Button, Flex } from '@hubspot/ui-extensions';
const Extension = () => {
const [formData, setFormData] = useState({ name: '', email: '' });
return (
setFormData({ ...formData, name: value })}
/>
setFormData({ ...formData, email: value })}
/>
console.log(formData)}>Submit
);
};
```
### Persistent data
Instead of using `localStorage` for persistent user preferences:
```jsx theme={null}
// Incorrect
const savePreferences = (theme) => {
localStorage.setItem('userPrefs', JSON.stringify({ theme }));
};
const Extension = () => {
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem('userPrefs');
return saved ? JSON.parse(saved).theme : 'light';
});
return Theme: {theme};
};
```
Use [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#hubspot-fetch) to persist data via your backend:
```jsx theme={null}
import { useState, useEffect } from 'react';
import { hubspot, logger } from '@hubspot/ui-extensions';
import { Button, Text, LoadingSpinner, Flex } from '@hubspot/ui-extensions';
const Extension = () => {
const [settings, setSettings] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadSettings = async () => {
try {
const response = await hubspot.fetch('/api/user/preferences');
const data = await response.json();
setSettings(data);
} catch (error) {
logger.error('Failed to load settings', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const updateSetting = async (key, value) => {
try {
await hubspot.fetch('/api/user/preferences', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [key]: value }),
});
setSettings({ ...settings, [key]: value });
} catch (error) {
logger.error('Failed to update setting', error);
}
};
if (loading) return ;
return (
Theme: {settings?.theme || 'default'} updateSetting('theme', 'dark')}>
Switch to Dark
);
};
```
### Complex state management
Instead of using `localStorage` for app-wide state:
```jsx theme={null}
// Incorrect
const AppState = {
save: (key, value) => {
const state = JSON.parse(localStorage.getItem('appState') || '{}');
state[key] = value;
localStorage.setItem('appState', JSON.stringify(state));
},
get: (key) => {
const state = JSON.parse(localStorage.getItem('appState') || '{}');
return state[key];
}
};
```
Use React Context with `useReducer`:
```jsx theme={null}
import { createContext, useContext, useReducer } from 'react';
import { hubspot } from '@hubspot/ui-extensions';
import { Button, Text } from '@hubspot/ui-extensions';
const AppStateContext = createContext();
const initialState = {
cache: {},
settings: {},
};
function stateReducer(state, action) {
switch (action.type) {
case 'CACHE_DATA':
return { ...state, cache: { ...state.cache, [action.key]: action.data } };
case 'UPDATE_SETTINGS':
return { ...state, settings: { ...state.settings, ...action.payload } };
default:
return state;
}
}
const AppStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(stateReducer, initialState);
return (
{children}
);
};
const DataComponent = () => {
const { state, dispatch } = useContext(AppStateContext);
const cacheData = (key, data) => {
dispatch({ type: 'CACHE_DATA', key, data });
};
return (
<>
Cached items: {Object.keys(state.cache).length} cacheData('user', { name: 'John' })}>
Cache User Data
>
);
};
hubspot.extend(() => (
));
```
## Related resources
* [UI extensions SDK overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk)
* [hubspot.fetch() API](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#hubspot-fetch)
* [React state management](https://react.dev/learn/managing-state)
# no-console
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-console
Prevent usage of console methods in UI extensions in favor of the SDK logger utility.
The `no-console` rule prevents usage of native [console](https://developer.mozilla.org/en-US/docs/Web/API/console) methods ([`console.log()`](https://developer.mozilla.org/en-US/docs/Web/API/console/log_static), [`console.error()`](https://developer.mozilla.org/en-US/docs/Web/API/console/error_static), etc.) in UI extensions.
## Rule details
UI extensions should use the SDK's structured [`logger`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#logging) utility instead of native `console.*` methods. This helps streamline UI extension debugging, as you won't need to sort through other framework logs and network activity to find extension-specific logs.
The SDK logger integrates with HubSpot's logging infrastructure, providing proper log levels, formatting, and the ability to filter and search across installs. Using `console.*` methods bypasses these benefits.
## Console method alternatives
Use the following UI extension alternatives instead of browser console methods:
| Console API | SDK Logger Alternative | Purpose |
| ----------------- | ---------------------- | --------------------- |
| `console.log()` | `logger.debug()` | Debug-level logging |
| `console.info()` | `logger.info()` | Informational logging |
| `console.warn()` | `logger.warn()` | Warning-level logging |
| `console.debug()` | `logger.debug()` | Debug-level logging |
| `console.error()` | `logger.error()` | Error-level logging |
**For user-facing errors:** use [`actions.addAlert()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners) or the [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) component instead of console output.
See the next section for an example of each alternative.
## Examples
### Basic logging
Instead of using `console.*` methods for logging:
```jsx theme={null}
// Incorrect
function debugData(data) {
console.log('Fetched data:', data);
}
window.console.log('Component mounted');
console.info('User logged in');
console.warn('Deprecated feature used');
console.debug('Debug information');
```
Use the SDK's [`logger`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#logging):
```jsx theme={null}
import { hubspot, logger, Text } from '@hubspot/ui-extensions';
const Extension = () => {
const debugData = (data) => {
logger.debug('Fetched data:', data);
};
logger.info('User logged in');
logger.warn('Deprecated feature used');
return Content here;
};
export default hubspot.extend(() => );
```
### Error handling
Instead of using `console.error()`:
```jsx theme={null}
// Incorrect
function handleError(error) {
console.error('An error occurred:', error);
}
globalThis.console.error('Failed to load data');
```
Use [`logger.error()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#logging) for debugging and [`actions.addAlert()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners) for user-facing errors:
```jsx theme={null}
import { Text, hubspot, logger } from '@hubspot/ui-extensions';
const Extension = ({ actions }) => {
const handleError = (error) => {
// Log for debugging
logger.error('Failed to save data:', error);
// Show user-friendly message
actions.addAlert({
type: 'danger',
message: 'Unable to save. Please try again.',
});
};
return Content here;
};
export default hubspot.extend(({ actions }) => (
));
```
Or use the [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) component for inline error messages:
```jsx theme={null}
import { Alert, Text, hubspot, logger } from '@hubspot/ui-extensions';
import { useState } from 'react';
const Extension = () => {
const [error, setError] = useState(null);
const handleAction = async () => {
try {
// Some operation...
} catch (err) {
logger.error('Operation failed:', err);
setError('Unable to complete the operation. Please try again.');
}
};
return (
<>
{error && (
{error}
)}
{/* Rest of component */}
>
);
};
export default hubspot.extend(() => );
```
## Related resources
* [SDK logger](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#logging)
* [actions.addAlert()](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners)
* [Alert component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert)
# no-dom-access
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-dom-access
Learn more about the `no-dom-access` ESLint rule, provided by the `@hubspot/eslint-config-ui-extensions` package.
The `no-dom-access` rule prevents usage of the [document](https://developer.mozilla.org/en-US/docs/Web/API/Document) object and DOM APIs in UI extensions.
## Rule details
UI extensions run in a sandboxed web worker environment where the DOM is intentionally unavailable. This architectural constraint ensures security and stability by preventing extensions from accessing sensitive host application data, modifying the main HubSpot interface, or interfering with other extensions.
The `document` object and DOM APIs don't exist at runtime. Attempting to use them will cause runtime errors. Instead, build your UI entirely with React components and hooks from [`@hubspot/ui-extensions`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk).
## DOM alternatives
Use the following UI extension alternatives instead of DOM APIs:
| DOM API | Alternative | Purpose |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------- |
| `document.querySelector()` | [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) | Build interface with React components |
| `document.getElementById()` | [React refs](https://react.dev/reference/react/useRef) | Reference elements within components |
| `document.body` / `document.title` | Component props and state | Manage UI through React state |
| `element.innerHTML` | JSX and UI components | Render content declaratively |
| `document.cookie` | [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#hubspot-fetch) + backend | Store data via backend APIs |
See the next section for an example of each alternative.
## Examples
### Querying elements
Instead of using `document.querySelector()` or `document.getElementById()`:
```jsx theme={null}
// Incorrect
const element = document.querySelector('.my-element');
const body = document.body;
const title = window.document.title;
const cookie = window['document'].cookie;
```
Build your UI with [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview):
```jsx theme={null}
import { Text, Button, Flex, Heading } from '@hubspot/ui-extensions';
import { hubspot } from '@hubspot/ui-extensions';
import { useState } from 'react';
const Extension = () => {
const [count, setCount] = useState(0);
return (
Counter ExampleCount: {count} setCount(count + 1)}>
Increment
);
};
hubspot.extend(() => );
```
### Element references
Instead of using `document.getElementById()` in effects:
```jsx theme={null}
// Incorrect
import { useEffect } from 'react';
function Component() {
useEffect(() => {
const element = document.getElementById('my-id');
element.focus();
}, []);
return Component;
}
```
Use [React refs](https://react.dev/reference/react/useRef) and component props:
```jsx theme={null}
import { Input, Flex } from '@hubspot/ui-extensions';
import { hubspot } from '@hubspot/ui-extensions';
import { useState, useRef, useEffect } from 'react';
const Extension = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const nameInputRef = useRef(null);
useEffect(() => {
// Focus the input using ref
if (nameInputRef.current) {
nameInputRef.current.focus();
}
}, []);
return (
setName(value)}
/>
setEmail(value)}
/>
);
};
hubspot.extend(() => );
```
### Data fetching
Instead of fetching data and manipulating the DOM:
```jsx theme={null}
// Incorrect
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
document.getElementById('username').textContent = data.name;
});
}, []);
```
Use [CRM data fetching hooks](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#fetch-crm-property-data) and React state:
```jsx theme={null}
import { Text, LoadingSpinner, Flex } from '@hubspot/ui-extensions';
import { hubspot, logger } from '@hubspot/ui-extensions';
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
import { useState, useEffect } from 'react';
const Extension = () => {
const { properties, isLoading, error } = useCrmProperties([
'firstname',
'lastname',
'email',
]);
const [displayName, setDisplayName] = useState('');
useEffect(() => {
if (properties.firstname && properties.lastname) {
const fullName = `${properties.firstname} ${properties.lastname}`;
setDisplayName(fullName);
logger.info(`Loaded contact: ${fullName}`);
}
}, [properties]);
if (isLoading) return ;
if (error) return Error loading data;
return (
{displayName}{properties.email}
);
};
hubspot.extend(() => );
```
## Related resources
* [UI extensions SDK overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk)
* [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview)
* [Fetching data](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data)
# no-html-elements
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-html-elements
Disallow usage of unsupported HTML elements in UI extensions.
The `no-html-elements` rule disallows usage of standard [HTML elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements) (`
`, ``, ``, etc.) in UI extensions.
## Rule details
UI extensions only support components provided by the [`@hubspot/ui-extensions` package](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview). Standard HTML elements are not supported in the UI extensions runtime environment and will fail to render.
This rule detects and reports usage of any standard HTML elements in your extension code. Note that other third-party React component libraries are also not supported in UI extensions, though they are not currently linted by this rule.
## HTML element alternatives
Use the following UI extension components instead of standard HTML elements:
| HTML Element | UI Extension Component |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `
`initial` (default): the item is sized according to its height and width. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container.
`auto`: the item is sized according to its height and width, but grows to absorb any extra free space in the flex container, and shrinks to its minimum size to fit the container.
`none`: the item is sized according to its height and width. It is fully inflexible: it neither shrinks nor grows in relation to the flex container.
Number: tells a component to fill all available space, shared evenly amongst other components with the same parent. The larger the flex given, the higher the ratio of space a component will take compared to its siblings.
|
You only need to use `Box` as a wrapper for components that you want to adjust. For example, if you wrap one component in a `Box` with a `flex` value, only that one component will have its width adjusted based on the available empty space.
```jsx theme={null}
Tile 1Tile 2Tile 3;
```
## Usage examples
### Using numbers in flex
Use the `flex` prop in a `Box` component to assign any extra spacing to components using either a default value (e.g. `auto`) or a specific number. When using a number, the components will be distributed based on the ratio of their assigned numbers. This also means that, when assigning a `flex` value for only one `Box`, you can use any number, because any number by itself will result in all available space being assigned to that `Box`.
For example, the four tiles below take up an increasing amount of space based on their `flex` values.
```jsx theme={null}
flex = 1flex = 2flex = 3flex = 4;
```
### Using alignSelf
Use the `alignSelf` prop to override alignment rules for individual `Box` components.
```jsx theme={null}
Top rightMiddleBottom left;
```
## Related components
* [Flex](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex)
* [Divider](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/divider)
* [Tile](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tile)
# Button
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button
Learn about the Button component for use in UI extensions.
The `Button` component renders a single button. Use this component to enable users to perform actions, such as submitting a form, sending data to an external system, or deleting data. Button text is passed into the component like a standard HTML element, rather than through a prop.
Below, learn how to implement buttons in a UI extension. For guidance on button design, check out the [Button design patterns](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/buttons).
```jsx theme={null}
import { Button } from "@hubspot/ui-extensions";
const Extension = () => {
return (
{
console.log("Someone clicked the button!");
}}
href={{
url: "https://wikipedia.org",
external: true,
}}
variant="primary"
size="md"
type="button"
>
Click me!
);
};
```
## Props
| **Prop** | **Type** | **Description** |
| ---------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disabled` | Boolean | When set to `true`, the button will render in a greyed-out state and cannot be clicked. |
| `href` | String \| Object | Include this prop to open a URL on click. Can be set to a URL string or an object with the following fields:
`url` (string): the URL that will open on click.
`external` (boolean, optional): set to `true` to open the URL in a new tab and display an external link icon. By default:
Links to HubSpot app pages will open in the same tab and will not include an icon.
Links to non-HubSpot app pages will open in a new tab and include the icon.
When a button includes both `href` and an `onClick` action, both will be executed on button click. |
| `onClick` | `() => void` | A function that will be invoked when the button is clicked. It receives no arguments and it's return value is ignored. |
| `overlay` | Object | Include a [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) or [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays). |
| `size` | `'xs'`, `'extra-small'` \| `'sm'`, `'small'` \| `'med'`, `'medium'` (default) | The size of the button. |
| `truncate` | Boolean | When set to `true`, long button text will be truncated with an ellipsis (`...`), with the full text displayed in a tooltip on hover. |
| `type` | `'button'` (default) \| `'reset'` \| `'submit'` | Sets the `role` HTML attribute of the button. |
| `variant` | `'primary'` \| `'secondary'` (default) \| `'destructive'` \| `'transparent'` | Sets the color of the button. See [variants section](#variants) for more information. |
## Variants
Using the `variant` prop, you can set the color of the button.
* `'primary'`: a dark blue button for the most frequently used or most important action on an extension. Each extension should only have one primary button.
* `'secondary'`: a grey button to provide alternative or non-primary actions. Each extension should include no more than two secondary buttons.
* `'destructive'`: a red button for actions that delete, disconnect, or perform any action that the user can't undo. Button text should clearly communicate what is being deleted or disconnected. After a destructive button is clicked, the user should have to verify or confirm the action.
* `'transparent'`: a button with the background and border color removed, styled like a hyperlink.
**Please note:**
HubSpot does not provide variant options for the orange buttons you’ll find across the app (both solid and outlined). Those color variants are reserved for the HubSpot product, which helps to maintain the hierarchy of available actions on a given page.
## Usage examples
* Use a `'primary'` button at the end of a form to submit data to another system.
* Use a `'secondary'` button next to a primary form submit button to reset form fields.
* Use a `'destructive'` button to enable users to delete a contact's data from an external system.
* Set a button to `'disabled'` when a contact doesn't qualify for a form submission due to missing criteria or other ineligibility.
## Guidelines
* **DO:** set button text that clearly communicates what action will occur when a user clicks it. Text should be unambiguous and concise (\~2-4 words).
* **DO:** use sentence-casing for button text (only the first word capitalized).
* **DO:** minimize the number of buttons that appear on a page record across all extensions.
* **DO:** always open links to pages outside of the HubSpot app in a new tab (`external: true`).
* **DON'T:** include multiple primary buttons in a single extension.
* **DON'T:** use a destructive button unless the consequences are significant or irreversible.
## Related components
* [ButtonRow](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button-row)
* [CRM action components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview)
* [CrmActionButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-button)
* [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel)
# ButtonRow
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button-row
Learn about the ButtonRow component for use in UI extensions.
The `ButtonRow` component renders a row of specified [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button) components. Use this component when you want to include multiple buttons in a row.
When the number of included buttons exceeds the available space, the extra buttons will be presented as a [Dropdown](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/dropdown) style button, which you can configure using the `dropDownButtonOptions` prop.
1. **Primary button:** only use one per extension.
2. **Secondary button:** only use with a primary and/or destructive button.
3. **Destructive button:** only use for actions that are destructive, paired with a secondary button.
```jsx theme={null}
import { Button, ButtonRow } from "@hubspot/ui-extensions";
const Extension = () => {
return (
{
console.log("Regular button clicked");
}}
>
Regular Button
{
console.log("Reset button clicked");
}}
variant="destructive"
type="reset"
>
Reset
{
console.log("Submit button clicked");
}}
variant="primary"
type="submit"
>
Submit
);
};
```
## Props
ReactNode>} description={<>Sets the content that will render inside the component. This prop is passed implicitly by providing sub-components.>} />
boolean>} description={<>By default, when the number of buttons exceeds the available horizontal space, the extra buttons will collapse into a single dropdown menu button. Set this prop to true to prevent the dropdown button from being interacted with.>} />
{ text: string, size: "extra-small" | "md" | "medium" | "sm" | "small" | "xs", variant: "primary" | "secondary" | "transparent" }>}
description={<>
When the included buttons exceed the available space, use this prop to customize the dropdown button. This prop takes an object containing the following key: 'value' pairs:
size: the size of the button. Can be 'xs', 'sm', or 'md' (default).
text: the button's text. By default, is set to More. If set to an empty value, will display a gear icon.
variant: the button variation. Can be 'primary', 'secondary' (default), or 'transparent'.
>}
/>
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Dropdown variants
Buttons that exceed available space will be presented in one [Dropdown](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/dropdown) style button. You can use the `dropDownButtonOptions` prop to customize its appearance. This prop takes an object that can include `size`, `text`, and `variant` fields. The `size` and `variant` fields use the same options available for those props in the `Dropdown` component.
Comma-separate values in `dropDownButtonOptions` to configure multiple options.
```jsx theme={null}
import { Button, ButtonRow } from "@hubspot/ui-extensions";
const Extension = () => {
return (
Primary
Destructive
SubmitOther
);
};
```
## Usage examples
* A `'primary'` and `'secondary'` button in a row to progress through a multi-step form.
* A `'destructive'` and `'secondary'` button in a row to confirm and cancel a contact deletion.
## Guidelines
* **DO:** include a secondary button with a destructive button to allow users to cancel the action.
* **DON'T:** use multiples of the same button type in a row. For example, don't include more than one primary button in one row.
* **DON'T:** use more than two secondary buttons in a single extension.
* **DON'T:** use more than three buttons in a row.
## Related components
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
* [CRM action components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview)
* [CrmActionButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-button)
# Checkbox
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/checkbox
Learn about the Checkbox component for use in UI extensions.
The `Checkbox` component renders single checkbox input. If you want to display multiple checkboxes, you should use [ToggleGroup](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle-group) instead, as it comes with extra logic for handling multiple checkboxes and radio buttons.
```jsx theme={null}
import { Checkbox } from "@hubspot/ui-extensions";
const Extension = () => {
return (
Super Admin
);
};
```
## Props
| Prop | Type | Description |
| ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `aria-label` | String | The checkbox's accessibility label. |
| `checked` | Boolean | When set to `true`, the checkbox is selected. Default is `false`. |
| `description` | String | Text that describes the field's purpose. |
| `initialIsChecked` | Boolean | When set to `true`, the checkbox is selected by default. Default is `false`. |
| `inline` | Boolean | When set to `true`, arranges checkboxes side by side. Default is `false`. |
| `name` | String | The checkbox's unique identifier. |
| `onChange` | `(checked: boolean, value: string) => void` | A callback function that is called when the checkbox is selected or cleared. Passes the new value. |
| `readOnly` | Boolean | When set to `true`, the checkbox cannot be selected. |
| `value` | String | The checkbox value. This value is not displayed on the card, but is passed on the server side when submitted, along with the checkbox name. |
| `variant` | `'sm'`, `'small'` \| `'default'` | The size of the checkbox |
## Related components
* [RadioButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/radio-button)
* [ToggleGroup](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle-group)
* [TextArea](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text-area)
# CurrencyInput
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/currency-input
An input field with currency formatting
The `CurrencyInput` component renders an input field with currency formatting, symbols, and locale-specific display patterns.
```jsx theme={null}
import { CurrencyInput } from "@hubspot/ui-extensions";
const Extension = () => {
const [amount, setAmount] = useState(1234.56);
return (
setAmount(value)}
description="Enter the transaction amount"
/>
);
};
```
## Props
| Prop | Type | Description |
| ------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `currency` Required | String | ISO 4217 currency code (e.g., "USD", "EUR", "JPY") |
| `label` Required | String | The label text to display for the form input element |
| `name` Required | String | The unique identifier for the input element (like HTML name attribute) |
| `defaultValue` | Number \| undefined | The value of the input on the initial render. |
| `description` | String | Instructional message to help understand the input's purpose. |
| `error` | Boolean | If `true`, shows validation message as error; if `false`, shows as success. Default is `false`. |
| `max` | Number | Sets the upper bound of the input (handled by underlying component). |
| `min` | Number | Sets the lower bound of the input (handled by underlying component). |
| `onBlur` | `(value: number) => void` | Callback when the input loses focus. |
| `onChange` | `(value: number) => void` | Callback when the value changes. |
| `onFocus` | `(value: number) => void` | Callback when the input gains focus. |
| `placeholder` | String | Text that appears when the input has no value. |
| `precision` | Number | Sets the number of decimal places for the currency. If not provided, defaults to currency-specific precision. |
| `readOnly` | Boolean | Determines if the field is editable. Default is `false`. |
| `required` | Boolean | Determines if the required indicator should be displayed. Default is `false`. |
| `tooltip` | String | Text that appears in a tooltip next to the input label. |
| `validationMessage` | String | Text to show under the input for error/success validation. Default is `''`. |
| `value` | Number \| undefined | The current value of the input |
## Related components
* [Input](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/input)
* [TextArea](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text-area)
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
# DateInput | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/date-input
Learn about the date input component for use in UI extensions.
The `DateInput` component renders an input field where a user can select a date.
```jsx theme={null}
import { Flex, DateInput } from "@hubspot/ui-extensions";
function RemoteApp() {
const [dateValue, setDateValue] = useState(null);
return (
{
setDateValue(value);
}}
value={dateValue}
format="ll"
/>
);
}
```
## Props
| Prop | Type | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clearButtonLabel` | String | Sets the label of the button that clears the date. |
| `defaultValue` | Object | The default date value. Uses the same format as the `value` field. |
| `description` | String | Text that describes the field's purpose. |
| `error` | Boolean | When set to `true`, `validationMessage` is displayed as an error message if provided. The input will also render its error state to let the user know there's an error. If left `false` (default), `validationMessage` is displayed as a success message. |
| `format` | `'short'` (default) \| `'long'` \| `'medium'` \| `'standard'` \| `'YYYY-MM-DD'` \| `L` \| `LL` \| `ll` | The date format.
`short`: 09/04/1986
`long`: September 4, 1986
`medium`: Sep 4, 1986
`standard`: 1986-09-04
`YYYY-MM-DD`: 1986-09-04
`L`: 09/04/1986
`LL`: September 4, 1986
`ll`: Sep 4, 1986
|
| `label` | String | The text that displays above the input. |
| `max` | Object | A Sets the latest valid date available using the following format:`{ year: number;``month: number;``date: number }` |
| `maxValidationMessage` | String | Sets the message that users will see when the hover over dates after the max date. |
| `min` | Object | Sets the earliest valid date available using the following format:`{ year: number;``month: number;``date: number }` |
| `minValidationMessage` | String | Sets the message that users will see when they hover over dates before the min date. |
| `name` | String | The input's unique identifier. |
| `onBlur` | `(value: DateInputEventsPayload) => void` | A function that is called and passes the value when the field loses focus. |
| `onChange` | `(checked: boolean, value: string) => void` | A callback function that is invoked when the value is committed. Currently, this occurs on `onBlur` of the input and when the user submits the form. |
| `onFocus` | `(value: DateInputEventsPayload) => void` | A function that is called and passed the value when the field gets focused. |
| `readOnly` | Boolean | When set to `true`, the checkbox cannot be selected. |
| `required` | Boolean | When set to `true`, displays a required field indicator. |
| `timezone` | `'userTz'` (default) \| `'portalTz'` | Sets the timezone that the component will user to calculate valid dates.
`userTz` (default): the user's time zone.
`portalTz`: the portal's default time zone.
|
| `todayButtonLabel` | String | Sets the label of the button that inserts today's date. |
| `tooltip` | String | The text that displays in a tooltip next to the label. |
| `validationMessage` | String | The text to display if the input has an error. |
| `value` | Object | The value of the input. Must include the year, month, and day: `{ year: number; month: number; date: number }`
`year`: the four-digit year (e.g., `2023`).
`month`: starting at `0`, the number of the month (e.g., `0` = January, `11` = December).
`date`: the number of the day (e.g., `1` = the first day of the month).
|
## Related components
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
* [MultiSelect](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/multi-select)
* [NumberInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/number-input)
# DescriptionList | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/description-list
Learn about the DescriptionList component for use in UI extensions.
The `DescriptionList` component renders pairs of labels and values. Use this component to display pairs of labels and values in a way that's easy to read at a glance. It also contains a `DescriptionListItem` subcomponent.
1. **Label:** describes the information being displayed.
2. **Value:** the information to display, contained in a `Text` component.
```jsx theme={null}
import { DescriptionList, DescriptionListItem, Text } from "@hubspot/ui-extensions";
const Extension = () => {
return (
AlanTuring
);
};
```
## Props
**`` props**
| **Prop** | **Type** | **Description** |
| ----------- | --------------------------- | ----------------------------------------------------------- |
| `direction` | `column` (default) \| `row` | The direction that the label and value pairs are displayed. |
**`` props**
| **Prop** | **Type** | **Description** |
| -------- | -------- | ----------------------------- |
| `label` | String | Text to display as the label. |
## Variants
By default, list items will be stacked vertically. You can use the `direction` prop to stack them horizontally.
* `row`:
* `column` (default):
## Usage examples
* Display easy to scan information for a sales rep to use on a call.
* Highlight the most recently updated properties on a company record.
## Guidelines
* **DO:** keep copy succinct, ideally one word each for the label and value.
* **DO:** use the horizontal orientation for horizontal layouts, and vertical orientation for column layouts.
* **DON'T:** use this component to display long strings of text.
* **DON'T:** use this component for lists that you want to be editable in the UI.
## Related components
* [List](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/list)
* [Table](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/table)
* [Statistics](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/statistics)
* [CrmPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list)
# Divider
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/divider
Learn about the Divider component for use in UI extensions.
The `Divider` component renders a grey, horizontal line for spacing out components vertically or creating sections in an extension. Use this component to space out other components when the content needs more separation than white space.
```jsx theme={null}
import { Divider, Text } from "@hubspot/ui-extensions";
const Extension = () => {
return (
<>
Text above the divider.Text below the divider.
>
);
};
```
## Props
"extra-large" | "extra-small" | "flush" | "large" | "lg" | "md" | "medium" | "sm" | "small" | "xl" | "xs">} description={<>The space between the divider and the content above and below it.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Variants
Using the `size` prop, you can set the amount of padding above and below the divider. Values range from `'extra-small'` to `'extra-large'` (`'small'` by default).
## Guidelines
* **DO:** use dividers to group similar components together.
* **DO:** consider when a new card or component might be needed, rather than using a divider.
* **DON'T:** use two dividers in a row without content between them.
## Related components
* [Box](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/box)
* [Accordion](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/accordion)
* [Tile](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tile)
# Dropdown | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/dropdown
Learn about the Dropdown component for use in UI extensions.
The `Dropdown` component renders a dropdown menu that opens on click, allowing users to select from a compact list of options. Define each dropdown item using `` with optional `onClick` handlers and `overlay` definitions for displaying tooltips and opening modals and panels.
```jsx theme={null}
import { Dropdown, Tooltip } from "@hubspot/ui-extensions";
const Extension = () => {
return (
console.log("clicked")}>Basic Action console.log("clicked")}
overlay={This action does something important}
>
Action with Tooltip
);
};
```
## Props
**`` props**
The `options` prop has been deprecated. Define options using `` child components instead, which provide more flexibility for [using overlays](#adding-overlays).
| **Prop** | **Type** | **Description** |
| ---------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `buttonSize` | `'xs'` \| `'sm'` \| `'md'` (default) | The size of the button. |
| `buttonText` | String | The button text. |
| `disabled` | Boolean | When set to `true`, the dropdown button cannot be focused or clicked. Set to `false` by default. |
| `options` Deprecated | Object | The options included in the dropdown menu. Each option includes:
`label`: the text label for the option.
`onClick`: the function that gets invoked when the option is selected.
|
| `variant` | `'primary'` (default) \| `'secondary'` \| `'transparent'` | The type of dropdown button to display. `'primary'` and `'secondary'` will display a blue and grey button, respectively, while `'transparent'` will display a blue hyperlink. |
**`` props**
| Prop | Type | Description |
| --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `onClick` | `() => void` | The function invoked when the item is clicked. |
| `overlay` | Overlay component | An overlay component to attach to the item, such as a [Tooltip](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tooltip), [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal), or [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel). |
## Adding overlays
Similar to the example above, which adds a `Tooltip` overlay to a dropdown item, you can open modals and panels via dropdown items by defining the `Modal` or `Panel` within a `Dropdown.ButtonItem` component's `overlay` prop.
Learn more about opening overlays in the [UI extension SDK reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays).
```jsx highlight={26-35} theme={null}
import {
Button,
Dropdown,
Tooltip,
Modal,
ModalBody,
ModalFooter,
Text,
hubspot
} from '@hubspot/ui-extensions';
hubspot.extend(({ actions }) => );
const Extension = ({ actions }) => {
return (
console.log('clicked')}>
Basic Action
This action does something important}
>
Action with Tooltip
Modal Content actions.closeOverlay('my-modal')}>Close
}
>
Open modal
);
};
```
## Variants
Using the `variant` and `buttonSize` props, you can set the type of button along with its size.
* `'primary'` buttons with size set to `'xs'`, `'sm'`, and `'md'` respectively:
* `'secondary'` buttons with size set to `'xs'`, `'sm'`, and `'md'` respectively:
* `'transparent'` buttons with size set to `'sm'` and `'md'` respectively:
## Related components
* [CrmActionLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-link)
* [CrmCardActions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-card-actions)
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
# EmptyState | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/empty-state
Learn about the EmptyState component for use in UI extensions.
The `EmptyState` component sets the content that appears when the extension is in an empty state. Use this component when there's no content or data to help guide users.
1. **Image:** the default image that comes with the component.
2. **Title:** the title that describes why the component is in an empty state.
3. **Additional text:** an additional [Text component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text) to provide further guidance. This does not come with the component by default.
4. **Additional button:** an additional [Button component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button) to help users take action. This does not come with the component by default.
```jsx theme={null}
import { EmptyState, Text } from '@hubspot/ui-extensions';
const Extension = ({ data }) => {
if (!data || !data.length) {
return (
Go out there and get some leads!
)
}
return (
{data.map(...)}
);
}
```
## Props
| **Prop** | **Type** | **Description** |
| -------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flush` | Boolean | When set to `true`, removes the default vertical margins for the component. By default, set to `false`. |
| `imageName` | String | The name of the image. See [available images](#available-images). By default, set to `'emptyStateCharts'`. |
| `imageWidth` | Number | The max-width for the image container. By default, set to `250`. |
| `layout` | `'horizontal'` (default) \| `'vertical'` | The layout direction of the content. |
| `reverseOrder` | Boolean | When set to `true`, swaps out the visual order of the text (primary) and image (secondary) content. This ensures that the primary content is presented first to screen readers. By default, set to `false`. |
| `title` | String | The text for the title header. |
## Available images
## Usage examples
* Display when it's the first use of a feature.
* Show when the user is required to take action in order to populate the card with information.
## Guidelines
* **DO:** make empty states informative so that users understand what will appear when the extension is not empty.
* **DO:** make empty states actionable. If relevant, explain the benefits of this area and how to add content or data.
* **DON'T:** make empty states too long.
## Related components
* [Alert](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert)
* [ErrorState](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/error-state)
* [LoadingSpinner](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-spinner)
# Error state | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/error-state
Learn about the ErrorState component for use in UI extensions.
The `ErrorState` component sets the content of an erroring extension. Use this component to guide users through resolving errors that your extension might encounter.
1. **Illustration:** one of three error-themed illustrations.
2. **Title:** the main error message to explain the root cause if known.
3. **Additional text:** an additional [`Text` component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text) to provide further guidance. This does not come with the component by default. Error text should use the following formats:
* **Known cause:** \[what failed] + \[why it failed] + \[next steps]. For example, *Failed to load extension due to outage, please wait a few minutes and try again.*
* **Unknown cause:** \[what failed] + \[next steps]. For example, *Couldn't load data, try refreshing the page or contacting IT.*
4. **Additional button:** an additional [`Button` component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button) to can help users take action. This does not come with the component by default.
```jsx theme={null}
import { ErrorState, Text, Button } from '@hubspot/ui-extensions';
const Extension = ({ data, error, fetchData }) => {
if (error) {
return (
Please try again in a few moments.
Try again
)
}
return (
{data.map(...)}
);
}
```
## Props
| **Prop** | **Type** | **Description** |
| -------- | ---------------------------------------------- | ----------------------------------------- |
| `title` | String | The text of the component header. |
| `type` | `'support'` \| `'lock'` \| `'error'` (default) | The type of image that will be displayed. |
## Variants
Using the `type` prop, you can set one of three illustrations.
```jsx theme={null}
const Extension = ({ data, error, fetchData }) => {
if (error) {
return (
)
}
return (
{data.map(...)}
);
}
```
```jsx theme={null}
const Extension = ({ data, error, fetchData }) => {
if (error) {
return (
)
}
return (
{data.map(...)}
);
}
```
```jsx theme={null}
const Extension = ({ data, error, fetchData }) => {
if (error) {
return (
)
}
return (
{data.map(...)}
);
}
```
## Usage examples
* Use the `default` error type when a card encounters an error when fetching data.
* Use the `support` error type when the user should contact internal or external support to resolve an error.
* Use the `lock` error type when the user needs to log in or doesn't have permission to access the card's data.
## Guidelines
* **DO:** use text that's clear, direct, brief, and helpful.
* **DON'T:** use technical jargon.
* **DON'T:** say "sorry" or use frivolous language such as "oops," "uh-oh," and "it's us, not you."
* **DON'T:** use exclamation points.
## Related components
* [Alert](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert)
* [EmptyState](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/empty-state)
* [LoadingSpinner](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-spinner)
# Flex
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex
Learn about the Flex component for configuring UI extension layout.
The `Flex` component renders an empty `div` container set to `display=flex`. When wrapped around other components, it enables those child components to be arranged using props. `Flex` can contain other `Flex` or `Box` components.
```jsx theme={null}
import { Flex, Tile } from "@hubspot/ui-extensions";
const Extension = () => {
return (
LeftRightBottom
);
};
```
## Props
"baseline" | "center" | "end" | "start" | "stretch">} description={<>Distributes components along the cross-axis using the available free space.>} />
"baseline" | "center" | "end" | "start" | "stretch">} description={<>Distributes a child component along the cross-axis using the available free space. Use this prop for nested Flex and Box components to align them differently from other child components in the Flex group.>} />
ReactNode>} description={<>Sets the content that will render inside the component. This prop is passed implicitly by providing sub-components.>} />
"column" | "row">} description={<>Arranges the components horizontally or vertically by setting the main axis.>} />
"extra-large" | "extra-small" | "flush" | "large" | "lg" | "md" | "medium" | "sm" | "small" | "xl" | "xs">} description={<>Sets the spacing between components.>} />
"around" | "between" | "center" | "end" | "start">} description={<>Distributes components along the main axis using the available free space.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
"nowrap" | "wrap" | false | true>} description={<>Whether components will wrap rather than trying to fit on one line.>} />
## Usage examples
### Horizontal layout
To arrange components horizontally, set `direction` to `row`. Then, use `justify` to configure the horizontal distribution. By default, components will stretch across the container if `justify` is not specified.
justify=\{'between'}
justify=\{'around'}
justify=\{'start'}
justify=\{'center'}
justify=\{'end'}
### Wrap
By default, components in a `row` will be arranged on one line when possible. Use the `wrap` prop to wrap components onto new lines when needed.
```jsx theme={null}
OneTwoThreeFourFiveSixSevenEight;
```
### Vertical layout
To arrange components vertically, set direction to `column`, then use the `align` prop to distribute them. By default, components will stretch across the extension container width when `align` is not specified.
align=\{'start'}
align=\{'center'}
align=\{'end'}
### Spacing
In the `Flex` component, you can use the `gap` prop to apply even spacing between the tiles. This prop will apply spacing equally for both `row` and `column` directions.
```jsx theme={null}
Tile 1Tile 2Tile 3;
```
### Using Flex in Flex
You can wrap child `Flex` components with `Flex` to set more specific rules for individual components. A child `Flex` component will not inherit props specified in the parent `Flex` component, so you'll need to repeat any props you've previously defined to maintain them.
```jsx theme={null}
LeftRightBottom;
```
## Related components
* [Tile](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tile)
* [Box](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/box)
* [Divider](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/divider)
* [Inline](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/inline)
# Form
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form
Learn about the Form component for use in UI extensions.
The `Form` component renders a form that can contain other subcomponents, such as [Input](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/input), [Select](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/select), and [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button). Use this component to enable users to submit data to HubSpot or an external system.
Below, learn how to implement a form in a UI extension. For guidance on form design, check out the [Form design patterns](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/forms).
1. **Label:** the input label.
2. **Text input:** an [Input](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/input) component with a placeholder value of *First name*.
3. **Label:** the select input label.
4. **Select input:** a [Select](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/select) component with value of *Customer*.
5. **Button:** a [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button) component to submit the form's information.
```jsx theme={null}
import { Form, Input, Button } from "@hubspot/ui-extensions";
const Extension = () => {
return (
);
};
```
## Props
| **Prop** | **Type** | **Description** |
| -------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `autoComplete` | `'on'` (default) \| `'off'` | Set this field to `'off'` to prevent autocompletion software (e.g., browser, password managers) from auto-filling form fields. Based on the [autocomplete HTML attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete). |
| `onSubmit` | Function | The function that is called when the form is submitted. It will receive a `RemoteEvent` as an argument and its return value will be ignored. |
## Usage examples
* A form to submit customer information to an external database.
* A form to place a product order on behalf of a customer.
## Guidelines
* **DO:** include text inputs when a user should be able to submit any value.
* **DO:** include select inputs when a user should only be able to select from a set of values.
* **DO:** include descriptions and placeholder text to provide context to users.
* **DO:** always position the submit button at the bottom of the form.
* **DON'T:** include a form without a submit button.
## Related components
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
* [DateInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/date-input)
* [Input](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/input)
* [MultiSelect](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/multi-select)
* [NumberInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/number-input)
* [Select](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/select)
# Heading
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/heading
Learn about the Heading component for use in UI extensions.
The `Heading` component renders large heading text. Use this component to introduce or differentiate sections of your component.
```jsx theme={null}
import { Heading } from "@hubspot/ui-extensions";
const Extension = () => {
return Heading text;
};
```
## Props
ReactNode>} description={<>Sets the content that will render inside the component. This prop is passed implicitly by providing sub-components.>} />
boolean>} description={<>When set to true, text will not line break.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Usage example
The title at the top of an extension to introduce its content.
## Guidelines
* **DO:** use headers to give users a summary of the information that the extension contains.
* **DON'T:** use more than one heading for each page or section in the extension.
* **DON'T:** use headers for paragraphs or long sentences.
## Related components
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
* [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link)
* [Accordion](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/accordion)
# Icon
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/icon
Learn about the Icon component for use in UI extensions.
Use the `Icon` component to render a visual icon within other components. It can generally be used inside most components, excluding ones that don't support child components (e.g., the `Input` component does not support icons).
Always pair icons with text. If that's not possible, include the `screenReaderText` prop to convey the icon's meaning for users with screen readers.
```jsx theme={null}
import { Alert, Button, Flex, Icon, Text } from "@hubspot/ui-extensions";
const Extension = () => {
return (
42 contacts updated
Sync contacts
Last synced 5 minutes ago
);
};
```
## Props
"add" | "appointment" | "approvals" | "artificialIntelligence" | "artificialIntelligenceEnhanced" | "attach" | "bank" | "block" | "book" | "bulb" | "calling" | "callingHangup" | "callingMade" | "callingMissed" | "callingVoicemail" | "callTranscript" | "campaigns" | "cap" | "checkCircle" | "circleFilled" | "circleHollow" | "clock" | "comment" | "contact" | "copy" | "crm" | "dataSync" | "date" | "delay" | "delete" | "description" | "developerProjects" | "documents" | "downCarat" | "download" | "edit" | "ellipses" | "email" | "emailOpen" | "emailThreadedReplies" | "enrichment" | "enroll" | "exclamation" | "exclamationCircle" | "facebook" | "faceHappy" | "faceHappyFilled" | "faceNeutral" | "faceNeutralFilled" | "faceSad" | "faceSadFilled" | "favoriteHollow" | "file" | "filledXCircleIcon" | "filter" | "flame" | "folder" | "folderOpen" | "forward" | "gauge" | "generateChart" | "gift" | "globe" | "globeLine" | "goal" | "googlePlus" | "guidedActions" | "hash" | "hide" | "home" | "hubDB" | "image" | "imageGallery" | "inbox" | "info" | "infoNoCircle" | "insertVideo" | "instagram" | "integrations" | "invoice" | "key" | "language" | "left" | "lessCircle" | "lesson" | "light" | "link" | "linkedin" | "listView" | "location" | "locked" | "mention" | "messages" | "mobile" | "moreCircle" | "notEditable" | "notification" | "notificationOff" | "objectAssociations" | "objectAssociationsManyToMany" | "objectAssociationsManyToOne" | "office365" | "order" | "paymentSubscriptions" | "pin" | "pinterest" | "powerPointFile" | "presentation" | "product" | "publish" | "question" | "questionAnswer" | "questionCircle" | "quickbooks" | "quote" | "readMore" | "readOnlyView" | "realEstateListing" | "recentlySelected" | "record" | "redo" | "refresh" | "registration" | "remove" | "replace" | "reports" | "right" | "robot" | "rotate" | "rss" | "salesQuote" | "salesTemplates" | "save" | "search" | "send" | "sequences" | "settings" | "shoppingCart" | "signal" | "signalPoor" | "signature" | "snooze" | "sortAlpAsc" | "sortAlpDesc" | "sortAmtAsc" | "sortAmtDesc" | "sortNumAsc" | "sortNumDesc" | "sortTableAsc" | "sortTableDesc" | "spellCheck" | "sprocket" | "star" | "stopRecord" | "strike" | "styles" | "success" | "tablet" | "tag" | "tasks" | "test" | "text" | "textBodyExpanded" | "textColor" | "textDataType" | "textSnippet" | "thumbsDown" | "thumbsUp" | "ticket" | "translate" | "trophy" | "twitter" | "undo" | "upCarat" | "upload" | "video" | "videoFile" | "videoPlayerSubtitles" | "view" | "viewDetails" | "warning" | "website" | "workflows" | "x" | "xCircle" | "xing" | "youtube" | "youtubePlay" | "zoomIn" | "zoomOut">} description={<>Sets the icon to display. See all available icons.>} />
"alert" | "inherit" | "success" | "warning">} description={<>The color of the icon. See the colors section for details.>} />
string>} description={<>Sets the text that screen readers will read for the icon.>} />
"large" | "lg" | "md" | "medium" | "sm" | "small">} description={<>By default, the size of the icon is set automatically based on the parent component. If you need to override the default size, you can specify one to use instead.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Colors
Using the `color` prop, you can set an icon to one of the following colors:
Default
`color="inherit"`
Alert
`color="alert"`
Warning
`color="warning"`
Success
`color="success"`
## Spacing
By default, spacing is not added around the icon. However, To add a space between the icon and adjacent text, you can manually add a space with the space bar, or by using ` ` and `{" "}` as shown below.
```jsx highlight={3, 6, 9} theme={null}
<>
42 contacts updated
Sync contacts
{" "}Last synced 5 minutes ago
>
```
## Available icons
Below are the currently available icons and their `name` values, which you can copy by clicking the icon.
## Related components
* [Alert](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert)
* [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag)
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
# Illustration
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/illustration
Learn about the Illustration component for use in UI extensions.
The `Illustration` component renders an illustration from HubSpot's illustration library. Use this component to add a visual indicator to your app card.
```jsx theme={null}
import { Illustration } from "@hubspot/ui-extensions";
const Extension = () => {
return (
);
};
```
## Props
| Prop | Type | Description |
| -------- | ------ | -------------------------------------------------------------------------------------- |
| `alt` | String | The illustration's alt text for accessibility. Default value is ` illustration`. |
| `height` | Number | The height of the illustration in pixels. |
| `name` | String | The name of the illustration. See [available illustrations](#available-illustrations). |
| `width` | Number | The width of the illustration in pixels. |
## Available illustrations
## Related components
* [Icon](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/icon)
* [EmptyState](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/empty-state)
* [ErrorState](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/error-state)
# Image
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image
Learn about the Image component for use in UI extensions.
The `Image` component renders an image. Use this component to add a logo or other visual brand identity asset, or to accentuate other content in the extension.
Images cannot exceed the width of the extension's container at various screen sizes, and values beyond that maximum width will not be applied to the image.
```jsx theme={null}
import { Image } from "@hubspot/ui-extensions";
const Extension = () => {
return (
{
console.log("Someone clicked the image!");
}}
width={200}
/>
);
};
```
## Props
| **Prop** | **Type** | **Description** |
| --------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `alt` | String | The alt text for the image, similar to the `alt` attribute for the HTML [img tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#attributes). |
| `height` | Number | The pixel height of the image. |
| `href` | String \| Object | When provided, sets the URL that will open when the image is clicked. Can be set to a URL string or an object with the following fields:
`url` (string): the URL that will open on click.
`external` (boolean, optional): set to `true` to open the URL in a new tab. By default:
Links to HubSpot app pages will open in the same tab.
Links to non-HubSpot app pages will open in a new tab.
When an image includes both `href` and an `onClick` action, both will be executed on button click. |
| `onClick` | `() => void` | A function that will be called when the image is clicked. This function will receive no arguments and any returned values will be ignored. |
| `overlay` | Object | Include a [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) or [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays). |
| `src` | String | The source of the image display. You can specify a URL or you can `import` the image directly if it's within your project. Learn more about [image sources](#image-guidelines). |
| `width` | Number | The pixel width of the image. |
## Image guidelines
* Supported types: the `Image` component supports the following file types: `.jpg`, `.jpeg`, `.png`, `.gif`, `.svg`, `.webp`.
* Supported sources: the `src` prop specifies the source of the image, which can be a URL (shown above) or you can import the file if it's included in your project. To import images, first add the files to your project, then import them by relative path (shown below). While there's no specific file size limit for images bundled in projects, the total size of your project cannot exceed 50MB.
```jsx theme={null}
import { Image } from "@hubspot/ui-extensions";
import myImage from "./images/myImage.png";
import myImage2 from "./images/myImage2.png";
const Extension = () => {
return (
<>
>
);
};
```
## Related components
* [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link)
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
* [Heading](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/heading)
# Inline | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/inline
Learn about the Inline component for UI extension layout management.
The `Inline` component uses flexbox styling to organize child components in a horizontal row. Similar to `Flex` and `Box`, use `Inline` to [manage the layout](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/manage-ui-extension-layout) of a UI extension.
```jsx theme={null}
import { Inline, Input, Select, Button, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(() => );
function Extension() {
return (
<>
Search
>
);
}
```
## Props
| Prop | Type | Description |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `align` | `'start'` \| `'center'` \| `'end'` \| `'baseline'` \| `'stretch'` (default) | Distributes components along the cross-axis using the available free space. |
| `gap` | `'flush'` (default) \| `'extra-small'`, `'xs'` \| `'small'`, `'sm'` \| `'medium'`, `'md'` \| `'large'`, `'lg'` \| `'extra-large'`, `'xl'` | The amount of spacing between components. |
| `justify` | `'start'` (default) \| `'center'` \| `'end'` \| `'around'` \| `'between'` | Distributes components along the main axis using the available free space. |
## Related components
* [Flex](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex)
* [Divider](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/divider)
* [Tile](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tile)
# Input
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/input
Learn about the Input component for use in UI extensions.
The `Input` component renders a text input field where a user can enter a custom text value. Like other inputs, this component should only be used within a [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form) that has a submit button.
1. **Label:** the input's label.
2. **Description:** the text that describes the field's purpose.
3. **Placeholder:** the placeholder value that displays when no value has been entered.
4. **Required field indicator:** communicates to the user that the field is required for form submission.
5. **Tooltip:** on hover, displays additional information about the field.
```jsx theme={null}
import { useState } from "react";
import { Form, Input } from "@hubspot/ui-extensions";
const Extension = () => {
const [name, setName] = useState("");
const [validationMessage, setValidationMessage] = useState("");
const [isValid, setIsValid] = useState(true);
return (
);
};
```
## Props
string>} description={<>The text that displays above the input. Required if inputType is not set to hidden.>} />
string>} description={<>The input's unique identifier, similar to the HTML input element name attribute.>} />
string>} description={<>The value of the input on the first render.>} />
string>} description={<>Displayed text that describes the field's purpose.>} />
boolean>} description={<>When set to true, validationMessage is displayed as an error message if provided. The input will also render in an error state so that users are aware. If left false, validationMessage is displayed as a success message.>} />
(value: string) => void>} description={<>A function that is called and passes the value when the field loses focus.>} />
(value: string) => void>} description={<>A callback function that is invoked when the value is committed. Currently, these are onBlur of the input and when the user submits the form.>} />
(value: string) => void>} description={<>A function that is called and passed the value when the field gets focused.>} />
(value: string) => void>} description={<>A function that is called and passes the value when the field is edited by the user. Should be used for validation. It's recommended that you don't use this value to update state (use onChange instead).>} />
string>} description={<>The text that appears in the input before a value is set.>} />
boolean>} description={<>When set to true, users will not be able to enter a value into the field.>} />
boolean>} description={<>When set to true, displays a required field indicator.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
string>} description={<>The text that displays in a tooltip next to the input label.>} />
"password" | "text">} description={<>The type of input. An input with the 'password' type will hide the characters that the user types.>} />
string>} description={<>The text to show if the input has an error.>} />
string>} description={<>The value of the input.>} />
## Usage example
Use for form fields where a user can enter any text value, such as email address or name.
## Guidelines
* **DO:** make label and description text concise and clear.
* **DO:** include placeholder text to help users understand what's expected in the field.
* **DO:** indicate if a field is required.
* **DO:** include clear validation error messages so that users know how to fix errors.
* **DON'T:** use this component for long responses, such as open-ended comments or feedback. Instead, use the [TextArea component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text-area).
* **DON'T:** use placeholder text for critical information, as it will disappear once users begin to type. Critical information should be placed in the label and descriptive text, with additional context in the tooltip if needed.
## Related components
* [Select](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/select)
* [TextArea](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text-area)
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
# Link
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link
Learn about the link component for use in UI extensions.
The `Link` component renders a clickable hyperlink. Use links to direct users to a web page, another part of the HubSpot app, or use them as buttons.
1. **Link text:** text that describes where the link leads.
2. **External link** **icon**: included when setting `external` to `true`. and indicates that the link will open in a new tab. Automatically included when the link leads to a page outside of the HubSpot app.
```jsx theme={null}
import { Link } from "@hubspot/ui-extensions";
const Extension = () => {
return (
Wikipedia
);
};
```
## Props
| Prop | Type | Description |
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `href` | String \| Object | Sets the link's URL and open behavior. Can be set to a URL string or an object with the following fields:
`url` (string): the URL that will open on click.
`external` (boolean, optional): set to `true` to open the URL in a new tab and display an external link icon. By default:
Links to HubSpot app pages will open in the same tab and will not include an icon.
Links to non-HubSpot app pages will open in a new tab and include the icon.
|
| `onClick` | `() => void` | A function that is invoked when the link is clicked. The function receives no arguments and its return value is ignored. |
| `overlay` | Object | Include a [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) or [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays). |
| `preventDefault` | Boolean | When set to `true`, `event.preventDefault()` will be invoked before the `onClick` function is called, preventing automatic navigation to the href URL. |
| `variant` | `'primary'` (default) \| `'light'` \| `'dark'` \| `'destructive'` | The color of the link. See the [variants section](#variants) for more information. |
## Variants
Using the `variant` prop, you can set the following styling:
* `'primary'`: the default blue (`#0091ae`).
* `'light'`: a white link that turns to a lighter shade of blue on hover (`#7fd1de`).
* `'dark'`: a darker shade of blue (`#33475b`).
* `'destructive'`: a red link (`#f2545b`).
## Usage examples
* Use the default `'primary'` variant when you want to link to another page or contact record in HubSpot.
* Use the `'light'` variant when you want to include a link on a dark background.
* Use the `'dark'` variant to include a link in an alert.
* Use the `'destructive'` variant when the link results in an action that can't be undone by the user, such as deleting contact property information.
## Guidelines
* **DO:** space out links so that users can tell when they'll be navigation to a different place.
* **DO:** make link text concise and contextual.
* **DO:** use the `'destructive'` variant sparingly and only when the action can't be undone.
* **DO:** always open links to pages outside of the HubSpot app in a new tab (`external: true`).
* **DON'T:** crowd multiple links together.
* **DON'T:** use the `'dark'` variant outside of alerts.
## Related components
* [Heading](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/heading)
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
* [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image)
# List
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/list
Learn about the List component for use in UI extensions.
The `List` component renders a list of items. Each item in `List` will be wrapped in `
` tags. A list can be styled as inline, ordered, or unordered with the `variant` prop.
```jsx theme={null}
import { List } from '@hubspot/ui-extensions';
const Extension() {
return (
List item 1
List item 2
List item 3
);
}
```
## Props
ReactNode>} description={<>The content of the list. Each child will be wrapped in an li tag.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
"inline-divided" | "inline" | "ordered-styled" | "ordered" | "unordered-styled" | "unordered">} description={<>The type of list to render.>} />
## Variants
By default, lists will be configured as vertically stacked list items without bullets. To customize the styling, use the `variant` prop, as shown below.
To create a bulleted unordered list:
```jsx theme={null}
List item 1
List item 2
List item 3
;
```
To create a numbered list without styling:
```jsx theme={null}
List item 1
List item 2
List item 3
;
```
To create a numbered list with styling:
```jsx theme={null}
List item 1
List item 2
List item 3
;
```
To stack list items horizontally:
```jsx theme={null}
List item 1
List item 2
List item 3
;
```
To stack list items horizontally with a divider between each item:
```jsx theme={null}
List item 1
List item 2
List item 3
;
```
## Related components
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
* [Accordion](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/accordion)
* [DescriptionList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/description-list)
# LoadingButton | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button
Learn about the LoadingButton component for use in UI extensions.
The `LoadingButton` component renders a button with loading state options. It includes the same props as the [Button component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button) with a few additional props for managing loading state.
```jsx theme={null}
import React from "react";
import { useState } from "react";
import { Flex, Heading, LoadingButton, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
function fakeFetchContactName() {
return new Promise(resolve => {
setTimeout(() => {
resolve({ firstname: "Tom", lastname: "Bombadil" });
}, 1000);
});
}
function Extension() {
const [isFetching, setIsFetching] = useState(false);
const [contactName, setContactName] = useState("");
async function handleClick() {
setIsFetching(true);
const { firstname, lastname } = await fakeFetchContactName();
setContactName(`${firstname} ${lastname}`);
setIsFetching(false);
}
return (
Fetch Name
{!isFetching && contactName !== "" && {contactName}}
);
}
```
## Props
| Prop | Type | Description |
| ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disabled` | Boolean | When set to `true`, the button will render in a greyed-out state and cannot be clicked. |
| `href` | String \| Object | Include this prop to open a URL on click. Can be set to a URL string or an object with the following fields:
`url` (string): the URL that will open on click.
`external` (boolean, optional): set to `true` to open the URL in a new tab and display an external link icon. By default:
Links to HubSpot app pages will open in the same tab and will not include an icon.
Links to non-HubSpot app pages will open in a new tab and include the icon.
When a button includes both `href` and an `onClick` action, both will be executed on button click. |
| `loading` | Boolean | Set to `true` to display the loading indicator and disable the button. Default is `false`. |
| `onClick` | `() => void` | A function that will be invoked when the button is clicked. It receives no arguments and its return value is ignored. |
| `overlay` | Object | Include a [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) or [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays). |
| `resultIconName` | String | Set to an [icon name](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/icon) to display an icon after loading. By default, will display a check mark. |
| `size` | `'xs'`, `'extra-small'` \| `'sm'`, `'small'` \| `'med'`, `'medium'` (default) | The size of the button. |
| `type` | `'button'` (default) \| `'reset'` \| `'submit'` | Sets the `role` HTML attribute of the button. |
| `variant` | `'primary'` \| `'secondary'` (default) \| `'destructive'` | Sets the color of the button. See [variants section](#variants) for more information. |
## Opening overlays
Like the [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button), [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link), [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag), and [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image) components, the `LoadingButton` component can open an overlay. In addition, you can use the `overlayOptions` prop to trigger the overlay either on click or when the loading has finished.
```jsx theme={null}
import React from "react";
import { useState } from "react";
import { Flex, Heading, LoadingButton, Panel, PanelBody, PanelSection, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
function fakeFetchContactName() {
return new Promise(resolve => {
setTimeout(() => {
resolve({ firstname: "Tom", lastname: "Bombadil" });
}, 1000);
});
}
function Extension() {
const [isFetching, setIsFetching] = useState(false);
const [contactName, setContactName] = useState("");
async function handleClick() {
setIsFetching(true);
const { firstname, lastname } = await fakeFetchContactName();
setContactName(`${firstname} ${lastname}`);
setIsFetching(false);
}
return (
{contactName !== "" && {contactName}}
}
>
Fetch name (overlay)
);
}
```
## Variants
Using the `variant` prop, you can set the color of the button.
* **Primary:** a dark blue button for the most frequently used or most important action on an extension. Each extension should only have one primary button.
* **Secondary (default):** a grey button to provide alternative or non-primary actions. Each extension should include no more than two secondary buttons.
* **Destructive:** a red button for actions that delete, disconnect, or perform any action that the user can't undo. Button text should clearly communicate what is being deleted or disconnected. After a destructive button is clicked, the user should have to verify or confirm the action.
**Please note:**
HubSpot does not provide variant options for the orange buttons you’ll find across the app (both solid and outlined). Those color variants are reserved for the HubSpot product, which helps to maintain the hierarchy of available actions on a given page.
## Usage examples
* Use a `'primary'` button at the end of a form to submit data to another system.
* Use a `'secondary'` button next to a primary form submit button to reset form fields.
* Use a `'destructive'` button to enable users to delete a contact's data from an external system.
* Set a button to `disabled={true}` when a contact doesn't qualify for a form submission due to missing criteria or other ineligibility.
## Guidelines
* **DO:** set button text that clearly communicates what action will occur when a user clicks it. Text should be unambiguous and concise (\~2-4 words).
* **DO:** use sentence-casing for button text (only the first word capitalized)
* **DO:** minimize the number of buttons that appear on a page record across all extensions.
* **DO:** always open links to pages outside of the HubSpot app in a new tab (`external: true`).
* **DON'T:** include multiple primary buttons in a single extension.
* **DON'T:** use a destructive button unless the consequences are significant or irreversible.
## Related components
* [ButtonRow](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button-row)
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
* [CrmActionButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-button)
* [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel)
# LoadingSpinner
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-spinner
Learn about the LoadingSpinner component for use in UI extensions.
The `LoadingSpinner` component renders a visual indicator for when an extension is loading or processing data.
1. **Label:** the text that describes the loading state.
2. **Size:** the size of the component. From left to right: extra small (`'xs'`), small (`'sm'`, default), medium (`'md'`).
3. **Layout:** the positioning of the spinner. From left to right: `inline`, `centered`.
```jsx theme={null}
import { LoadingSpinner } from "@hubspot/ui-extensions";
const Extension = () => {
return ;
};
```
## Props
| Prop | Type | Description |
| ----------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `label` | String | The text that displays next to the spinner. |
| `layout` | `'inline'` (default) \| `'centered'` | The position of the spinner. |
| `showLabel` | Boolean | When set to `true`, the `label` will appear next to the spinner. Default is `false`. |
| `size` | `'xs'`, `'extra-small'` \| `'sm'`, `'small'` (default) \| `'md'`, `'medium'` | The size of the spinner. |
## Usage examples
* A loading state after the user submits form data to an external system (e.g., "Submitting contact details").
* A loading state as the card retrieves customer purchase history from an external system (e.g., "Loading purchase history").
## Guidelines
* **DO:** keep label text as concise as possible.
* **DO:** use label text to describe what's happening during the loading process.
* **DO:** use complete sentences in label text.
* **DON'T:** include multiple loading spinners at once in a single card to avoid confusion.
## Related components
* [Alert](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert)
* [EmptyState](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/empty-state)
* [ErrorState](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/error-state)
# Modal
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal
Learn about the Modal component for use in UI extensions.
Use the `Modal` component to render a pop-up overlay containing other components. Like the `Panel` component, you'll include the `Modal` component in an `overlay` prop within a [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button), [LoadingButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button), [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link), [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag), or [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image) component.
To see an example of using overlays, check out [HubSpot's Overlay example project](https://github.com/HubSpot/ui-extensions-examples/tree/main/overlay-example). Note that this is a version `2025.1` project, but the overlay implementation would be similar for projects on version `2025.2` or `2026.03`.
To format the modal, you'll use the following subcomponents:
* `ModalBody` (required): contains the main content of the modal.
* `ModalFooter`: an optional component to format the footer section of the modal.
```jsx theme={null}
import React from "react";
import { Button, Modal, ModalBody, Text, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(() => );
const Extension = () => {
return (
<>
Welcome to my modal. Thanks for stopping by!Close the modal by clicking the X in the top right.
}
>
Open modal
>
);
};
```
## Props
| Prop | Type | Description |
| ------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `aria-label` | String | The modal's accessibility label. |
| `id` | String | The unique identifier for the modal. |
| `onClose` | `() => void` | A function that will be invoked when the modal has finished closing. |
| `onOpen` | `() => void` | A function that will be invoked when the modal has finished opening. |
| `title` | String | The title of the modal, displayed at in the modal's top bar. |
| `variant` | `'default'` (default) \| `'danger'` | The type of modal. See the [variants section](#variants) for more information. |
| `width` | `'small'`, `'sm'` (default) \| `'medium'`, `'md'` \| `'large'`, `'lg'` | The width of the modal. |
## Opening and closing modals
By default, HubSpot handles opening the modal when the user clicks the parent [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button), [LoadingButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button), [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link), [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag), or [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image) component. A close button will also be included in the top right of the modal.
In addition, you can add a close mechanism to a modal using a `Button`, `LoadingButton`, `Link`, `Tag` or `Image` with an `onClick` event that triggers the `closeOverlay` action. To use this action, you'll need to include the [actions argument](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#registering-the-extension) in `hubspot.extend()` as seen in the example code below.
Learn more about [opening and closing overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays).
```jsx theme={null}
import { Button, Modal, ModalBody, ModalFooter, Text, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
const OverlayExampleCard = ({ actions }) => {
return (
<>
Welcome to my modal. Thanks for stopping by!Close the modal by clicking the X in the top right, or using the button below actions.closeOverlay("default-modal")}>Close modal
}
>
Open modal
>
);
};
```
**Please note:**
* You can only have one modal open at a time. Opening a modal while another is already open will cause the first one to close.
* A `Modal` can be opened from a `Panel`, but a `Panel` cannot be opened from a `Modal`.
## Variants
Use the `variant` prop to configure the style of modal.
By default, the modal will include a blue colored top bar where the `title` displays.
To configure the modal as a warning with a red colored top bar, set `variant` to `'danger'`.
## Usage examples
* Use the default variant to prompt a user to enter a new set of customer details for the current contact record.
* Use the danger variant to confirm when a user wants to delete a deal record.
## Guidelines
**DO:** use this type of overlay for short messages and confirmations. If you want a more lightweight way to communicate a message to the user, check out the [Alert component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert).
**DO:** use the danger variant to confirm destructive actions that cannot be undone. The text should clearly communicate what is being deleted, and the confirmation button should be explicit about the action. For example, never use the word "Okay" to confirm the deletion of an item. Instead, use "Delete".
Learn more about [overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays), including when to use a `Modal` or a `Panel`.
## Related components
* [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel)
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
* [LoadingButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button)
* [Display iframe modals using the UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk)
# MultiSelect
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/multi-select
Learn about the MultiSelect component for use in UI extensions.
The `MultiSelect` component renders a dropdown menu select field where a user can select multiple values. Commonly used within the [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form) component.
```jsx theme={null}
import { Form, MultiSelect, Button } from "@hubspot/ui-extensions";
function MultiSelectControlledExample() {
const [formValue, setFormValue] = useState([]);
return (
);
}
```
## Props
| Prop | Type | Description |
| ------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `description` | String | Text that describes the field's purpose. |
| `error` | Boolean | When set to `true`, `validationMessage` is displayed as an error message if provided. The input will also render its error state to let the user know there's an error. If left `false` (default), `validationMessage` is displayed as a success message. |
| `label` | String | The text that displays above the dropdown menu. |
| `name` | String | The input's unique identifier. |
| `onChange` | `(value: (string \| number)[]) => void` | A callback function that is invoked when the value is committed. |
| `options` | Array | The options to display in the dropdown menu. `label` will be used as the display text, and `value` should be the option's unique identifier, which is submitted with the form. |
| `readOnly` | Boolean | When set to `true`, users will not be able to enter a value into the field. Set to `false` by default. |
| `required` | Boolean | When set to `true`, displays a required field indicator. |
| `tooltip` | String | The text that displays in a tooltip next to the label. |
| `validationMessage` | String | The text to display if the input has an error. |
| `value` | String \| number | The value of the input. |
## Related components
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
* [DateInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/date-input)
* [NumberInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/number-input)
* [Select](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/select)
# NumberInput
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/number-input
Learn about the NumberInput component for use in UI extensions.
The `NumberInput` component renders a number input field. Commonly used within the [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form) component.
1. **Label:** the input's label.
2. **Description:** the text that describes the field's purpose.
3. **Value:** an entered value.
```jsx theme={null}
import { NumberInput } from "@hubspot/ui-extensions";
const Extension = () => {
const [portalCount, setPortalCount] = useState(0);
return (
setPortalCount(value)}
/>
);
};
```
## Props
| Prop | Type | Description |
| ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `defaultValue` | Number | The value of the input on initial render. |
| `description` | String | Text that describes the field's purpose. |
| `error` | Boolean | When set to `true`, `validationMessage` is displayed as an error message if provided. The input will also render its error state to let the user know there's an error. When `false` (default), `validationMessage` is displayed as a success message. |
| `formatStyle` | `'decimal'` \| `'percentage'` | Formats the input as a decimal or percentage. |
| `label` | String | The text that displays above the dropdown menu. |
| `max` | Number | Sets the upper bound of the input. |
| `min` | Number | Sets the lower bound of the input. |
| `name` | String | The input's unique identifier. |
| `onBlur` | `(value: number) => void` | A function that is called every time the field loses focus, passing the value. |
| `onChange` | `(value: number) => void` | A callback function that is invoked when the value is committed. Currently these times are `onBlur` of the input and when the user submits the form. |
| `onFocus` | `(value: number) => void` | A function that is called every time the field gets focused on, passing the value. |
| `placeholder` | String | The text that appears in the input before a value is set. |
| `precision` | Number | Sets the number of digits to the right of the decimal point. |
| `readOnly` | Boolean | When set to `true`, users will not be able to enter a value into the field. Set to `false` by default. |
| `required` | Boolean | When set to `true`, displays a required field indicator. |
| `tooltip` | String | The text that displays in a tooltip next to the label. |
| `validationMessage` | String | The text to display if the input has an error. |
| `value` | String \| number | The value of the input. |
## Usage example
A field in a form where salespeople can enter the total deal amount.
## Guidelines
* **DO:** make label and description text concise and clear.
* **DO:** include placeholder text to help users understand what's expected in the field.
* **DO:** indicate if there is a minimum or maximum number requirement.
* **DO:** indicate if a field is required.
* **DO:** include clear validation error messages so that users know how to fix errors.
* **DON'T:** use this component for long responses, such as open-ended comments or feedback. Instead, use the [TextArea component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text-area).
* **DON'T:** use placeholder text for critical information, as it will disappear once users begin to type. Critical information should be placed in the label and descriptive text, with additional context in the tooltip if needed.
## Related components
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
* [DateInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/date-input)
* [MultiSelect](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/multi-select)
# Panel
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel
Learn about the Panel component for use in UI extensions.
The `Panel` component renders a panel overlay on the right side of the page and contains other components. Like the [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) component, you'll include the `Panel` component in an `overlay` prop within a [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button), [LoadingButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button), [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link), [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag), or [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image) component.
The `Panel` component uses three subcomponents to control its design and content, which follows the general structure below:
* ``: the outermost container. It must be a top-level component. You cannot put a `Panel` inside another component, such as `Flex`.
* ``: the container that wraps the panel's content and makes it scrollable. Include only one `PanelBody` per `Panel`.
* ``: a container that adds padding and bottom margin to provide spacing between content. You can use [Flex](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex) and [Box](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/box) to further customize content layout.
* ``: a sticky footer component at the bottom of the panel. Include only one `PanelFooter` per `Panel`.
```jsx theme={null}
import { Button, Panel, PanelSection, PanelBody, PanelFooter, Text, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(() => );
const OverlayExampleCard = () => {
return (
<>
Welcome to my panel. Thanks for stopping by!Close the panel by clicking the X in the top right.
}
>
Open panel
>
);
};
```
## Props
Below are the props available for `Panel` and `PanelSection`.
**`` props**
| Prop | Type | Description |
| ------------ | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aria-label` | String | The panel's accessibility label. |
| `id` | String | A unique ID for the panel. |
| `onClose` | `onClose() => void` | A function that will be invoked when the panel has finished closing. |
| `onOpen` | `onOpen() => void` | A function that will be invoked when the panel has finished opening. |
| `title` | String | The text that displays at the top of the panel. |
| `variant` | `'modal'` \| `'default'` (default) | The panel variant. The `modal` variant includes better screen reader focus on the panel and is recommended for visual and motor accessibility and tab navigation. See [variants](#variants) for more information. |
| `width` | `'sm'`, `'small'` (default) \| `'md'`, `'medium'` \| `'lg'`, `'large'` | The width of the panel. |
**`` props**
| Prop | Type | Description |
| ------- | ------- | ------------------------------------------------------------------------------- |
| `flush` | Boolean | When set to `true`, the section will have no bottom margin. Default is `false`. |
## Opening and closing panels
By default, HubSpot handles opening the panel when the user clicks the parent [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button), [LoadingButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button), [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link), [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag), or [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image) component. A close button will also be included in the top right of the panel.
In addition, you can add a close mechanism to a panel using a `Button`, `LoadingButton`, `Link`, `Tag` or `Image` with an `onClick` event that triggers the `closeOverlay` action. To use this action, you'll need to include the [actions argument](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#registering-the-extension) in `hubspot.extend()` as seen in the example code below. /apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#registering-the-extension
Learn more about [opening and closing overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays).
```jsx theme={null}
import { Button, Panel, PanelSection, PanelBody, PanelFooter, Text, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
const OverlayExampleCard = ({ actions }) => {
return (
<>
Welcome to my panel. Thanks for stopping by!Close the panel by clicking the X in the top right, or using the button below {
actions.closeOverlay("my-panel");
}}
>
Close
}
>
Open panel
>
);
};
```
**Please note:**
* You can only have one panel open at a time. Opening a panel while another is already open will cause the first one to close.
* A `Modal` can be opened from a `Panel`, but a `Panel` cannot be opened from a `Modal`.
## Variants
By default, the panel will only obscure the content on the right side of the page where it opens. Using the `variants` prop, you can add an additional overlay behind the panel to blur the rest of the page. This variant puts more focus on the panel and improves accessibility for users with screen readers. Because the `modal` variant obscures the rest of the page's content, use it only when users don't need other context from the page.
## Usage examples
Use a panel when a user needs to submitting an order form for a customer.
## Guidelines
Use this component when users need to fill out a longer or multi-step form.
## Related components
* [Box](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/box)
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
* [LoadingButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button)
* [Divider](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/divider)
* [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link)
* [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image)
* [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal)
* [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag)
* [Tile](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tile)
# ProgressBar
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/progress-bar
Learn about the ProgressBar component for use in UI extensions.
The `ProgressBar` component renders a visual indicator showing a numeric and/or percentage-based representation of progress. The percentage is calculated based on the maximum possible value specified in the component.
1. **Title:** the text that displays above the bar to describe the data it represents.
2. **Completion percentage:** the percent value of how much progress has been made.
3. **Value description:** the text that describes the current state of the bar's value.
4. **Variant:** the color of the progress bar.
```jsx theme={null}
import { ProgressBar } from "@hubspot/ui-extensions";
const Extension = () => {
return ;
};
```
## Props
| Prop | Type | Description |
| ------------------ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `aria-label` | String | The accessibility label. |
| `maxValue` | Number | The maximum value of the progress bar. Default is `100`. |
| `showPercentage` | Boolean | When set to `true`, the progress bar will display the completion percentage. Default is `false`. |
| `title` | String | The text that displays above the progress bar. |
| `value` | Number | The number representing the progress so far. Default is `0`. |
| `valueDescription` | String | The text that explains the current state of the `value` property. For example, `"150 out of 250"`. |
| `variant` | `'success'` (default) \| `'warning'` \| `'danger'` | The color to indicate progress sentiment. |
## Variants
Using the variant prop, you can set the following progress bar colors:
* `'success'`: a green bar to indicate movement towards a positive goal or outcome.
* `'warning'`: a yellow bar to indicate movement towards a negative outcome or limitation.
* `'danger'`: a red bar to indicate movement towards an extremely negative outcome or when a limitation has been reached.
## Usage examples
* Evaluating the sale of products against a quota or goal.
* Communicating the stage progress of a deal or ticket.
* Monitoring the number of support calls or tickets per customer.
## Guidelines
* **DO:** use the `showPercentage` prop to give the user more information about the status of the progress.
* **DON'T:** use more than 3-4 progress bars in a single card.
## Related components
* [StepIndicator](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/step-indicator)
* [LoadingSpinner](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-spinner)
* [Statistics](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/statistics)
# RadioButton
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/radio-button
Learn about the RadioButton component for use in UI extensions.
The `RadioButton` component renders a radio select button. If you want to include more than two radio buttons, or are building a form, it's recommended to use the [ToggleGroup](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle-group) component instead.
```jsx theme={null}
import { RadioButton } from "@hubspot/ui-extensions";
function Extension() {
const [roleType, setRoleType] = useState("support");
return (
<>
{
setRoleType("superAdmin");
}}
>
Super Admin
{
setRoleType("support");
}}
>
Customer Support
>
);
}
```
## Props
| Prop | Type | Description |
| ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `checked` | Boolean | Whether the radio button is currently selected. Default is `false`. |
| `description` | String | Text that describes the field's purpose. |
| `initialIsChecked` | Boolean | When set to `true`, the option will be selected by default. Default is `false`. |
| `inline` | Boolean | When set to `true`, arranges radio buttons side by side. Default is `false`. |
| `name` | String | The input's unique identifier. |
| `onChange` | `(checked: boolean, value: string) => void` | A callback function that is invoked when the radio button is selected. Passes the new value. |
| `readonly` | Boolean | When set to `true`, users will not be able to enter a value into the field. Set to `false` by default. |
| `value` | String \| number | The radio button value. This value is not displayed, but is passed on the server side when submitted, along with `name`. |
| `variant` | `'sm'`, `'small'` \| `'default'` (default) | The size of the checkbox. |
## Related components
* [Checkbox](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/checkbox)
* [ToggleGroup](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle-group)
* [TextArea](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text-area)
# ScoreCircle
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/scorecircle
Learn about the ScoreCircle component for use in UI extensions.
The `ScoreCircle` component displays a score value (0-100) as a circular progress indicator with color-coded status. Color is automatically applied based on the [score value](#score-values).
Use this component to provide data visualization for use cases such as:
* Completion percentages for tasks or projects.
* Quality scores or performance metrics.
* Health scores or ratings.
```jsx theme={null}
import { ScoreCircle, Flex } from '@hubspot/ui-extensions';
hubspot.extend(() => (
));
```
## Props
number>} description={<>The score value to display. Must be a number between 0 and 100. Decimal values are automatically rounded down to the nearest integer.>} />
## Score values
The component automatically validates score values and applies color-coded statuses based on the score value:
* **Success (green):** score >= 66
* **Warning (yellow):** score >= 33 and \< 66
* **Alert (red):** score \< 33
The component includes automatic handling for invalid scores:
* If the score is less than 0 or greater than 100, an error is logged and the component displays `–` instead of the score.
* If the score is a decimal value, it is automatically rounded down to the nearest integer and a warning is logged.
## Accessibility
The component includes the following accessibility features:
* Uses the `meter` [ARIA role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/meter_role) to indicate a scalar measurement.
* Provides `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` attributes for screen readers.
* Includes an accessible label describing the score value or error state.
## Guidelines
* **DO:** provide context around the score with additional text or labels.
* **DO:** use the automatic color coding to communicate status at a glance.
* **DON'T:** use `ScoreCircle` for values outside the 0-100 range.
## Related components
* [Statistics](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/statistics)
* [ProgressBar](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/progress-bar)
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
# SearchInput | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/search-input
Learn about the SearchInput component for enabling search in UI extensions.
The `SearchInput` component renders an input field that enables users to search. The component doesn't provide search functionality out of the box, but includes props for managing input state and validation.
```jsx theme={null}
import React, { useState } from "react";
import { Button, Flex, Form, SearchInput, Text, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
const Extension = ({ sendAlert }) => {
const [searchValue, setSearchValue] = useState("");
const handleSubmit = formData => {
const searchTerm = formData.targetValue.search;
sendAlert({
message: `You searched for: "${searchTerm}"`,
type: "success",
});
};
return (
);
};
```
## Props
| Prop | Type | Description |
| ---------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clearable` | Boolean | When set to `true`, shows a clear button to clear the input. Default is `true`. |
| `description` | String | Instructional message to help understand the purpose of the input. |
| `error` | Boolean | When set to `true`, renders the error state and shows `validationMessage` as an error. |
| `getValidationMessage` | `(value: string) => string \| null` | Called to validate the input value and display a validation message. Depending on the width and length of message, the message display may appear above or inline with the input. |
| `label` | String | The label text to display for the input. |
| `name` | String | The unique identifier for the input. |
| `onBlur` | `(value: string) => void` | A function that is called when the field loses focus. |
| `onChange` | `(value: string) => void` | A callback function that is invoked when the input value changes. |
| `onFocus` | `(value: string) => void` | A function that is called when the field gets focused. |
| `onInput` | `(value: string) => void` | A callback function that is invoked every time the field is edited by the user. |
| `placeholder` | String | Text that appears in the input when it has no value set. |
| `readOnly` | Boolean | When set to `true`, the field is not editable. |
| `required` | Boolean | When set to `true`, displays a required field indicator. |
| `tooltip` | String | Text that will appear in a tooltip on hover next to the input label. |
| `validationMessage` | String | The text to show under the input for error or success validations. Will appear below the input. Learn more about [validation messages](#validation-messages). |
| `value` | String | The value of the input. |
## Validation messages
Using the `validationMessage` and `getValidationMessage` props, you can implement validation into the search input. By default, `validationMessage` will render as a green success message. When the `error` prop is `true`, the message will render as red and the search input will include a red border.
```jsx theme={null}
<>
>;
```
## Table search example
The example below creates a searchable table.
```jsx theme={null}
import React, { useState } from "react";
import {
Button,
Divider,
Form,
Heading,
Table,
TableHead,
TableHeader,
TableRow,
TableBody,
TableCell,
Flex,
SearchInput,
hubspot,
} from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
const Extension = ({ sendAlert }) => {
const tableData = [
{ name: "Mark Scout", role: "Team Lead (MDR)" },
{ name: "Dylan George", role: "Macrodata Refiner" },
{ name: "Irving Bailiff", role: "Senior Refiner" },
{ name: "Helly Riggs", role: "New Hire" },
{ name: "Burt Goodman", role: "Department Head (O&D)" },
];
const [filteredData, setFilteredData] = useState(tableData);
const handleSubmit = e => {
const searchTerm = e.targetValue.searchInput;
sendAlert({ message: `You've searched for: ${searchTerm}` });
};
const searchTable = e => {
const searchTerm = e.targetValue.searchInput;
// Filter the table data based on search term
const filtered = tableData.filter(
item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.role.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredData(filtered);
sendAlert({
message: `Searched for: ${searchTerm}. Found ${filtered.length} result(s).`,
});
};
return (
<>
>
);
};
```
## Related components
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
* [DateInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/date-input)
* [NumberInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/number-input)
# Actions
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions
Reference information for actions provided by the UI extensions SDK.
Actions are functions provided by the UI extensions SDK that your extension can call to interact with HubSpot. Calling an action triggers behavior in HubSpot, such as displaying an alert banner, reloading the page, or opening a modal.
You can access actions using either a props-based approach or a hook-based approach:
* **Props-based approach:** destructure `actions` from the callback passed to `hubspot.extend()`, then pass it to your component as a prop.
* **Hook-based approach:** call the [`useExtensionActions`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#useextensionactions) hook directly within your component.
The actions available depend on where your extension is loaded. [Universal actions](#universal-actions) work in all extension points. [CRM property actions](#crm-property-actions) are only available in CRM record extension points.
Note that some UI components include a set of actions separate from the SDK actions below, such as the [CRM action components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview).
Both approaches to access actions provide identical functionality, so the choice is a matter of preference. As a general guideline:
* **Hook-based approach:** cleaner component APIs, no prop drilling.
* **Props-based approach:** explicit dependency injection.
Either way, the data is the same, as shown in the code example below.
```tsx expandable theme={null}
// Props-based approach - destructure context/actions and pass as props
hubspot.extend(({ context, actions }) => (
));
function MyExtension({ context, actions }) {
return (
actions.addAlert({ message: "Hello!" })}>
User: {context.user.firstName} from {context.location}
);
}
// Hook-based approach - call hooks directly in your component
hubspot.extend(() => );
function MyExtension() {
const { actions, context } = useExtensionApi<'crm.record.tab'>();
return (
actions.addAlert({ message: "Hello!" })}>
User: {context.user.firstName} from {context.location}
);
}
```
## Universal actions
**Universal actions**
Supported in all extension points: `settings`, `home`, `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`
| Action | Description |
| ------------------------------------------------ | ---------------------------------- |
| [`addAlert`](#display-alert-banners) | Display alert banners to the user. |
| [`reloadPage`](#reload-page) | Reload the current page. |
| [`copyTextToClipboard`](#copy-text-to-clipboard) | Copy text to the user's clipboard. |
| [`closeOverlay`](#open-overlays) | Close an open overlay or modal. |
| [`openIframeModal`](#open-an-iframe-in-a-modal) | Open an iframe in a modal window. |
### Display alert banners
Use the `addAlert` method to send alert banners as a feedback for any actions to indicate success or failure. `addAlert` is a part of the `actions` object that can be passed to extension via `hubspot.extend`. If you instead want to render an alert within a card, check out the [Alert component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert).
For example, the code below results in an app card that displays a success alert after fetching data from an external source. Note that the `addAlert` action is passed into `hubspot.extend()` and the `Extension` component, then is triggered when the `hubspot.fetch()` function successfully executes.
```jsx highlight={5, 7,27-31,38-42 } theme={null}
import React, { useState } from "react";
import { Text, Button, LoadingSpinner } from "@hubspot/ui-extensions";
import { hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({actions}) => );
const Extension = ({ addAlert }) => {
const [totalUsers, setTotalUsers] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUserData = async () => {
try {
setLoading(true);
setError(null);
const response = await hubspot.fetch('https://myExternalData.com/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setTotalUsers(result.stats.totalUsers);
// Show success alert banner
addAlert({
title: "Data fetched successfully",
message: `Retrieved total users from API`,
type: "success"
});
} catch (err) {
setError(err.message);
console.error('Error fetching data:', err);
// Show error alert banner
addAlert({
title: "Data Fetch Failed",
message: `Failed to retrieve data: ${err.message}`,
type: "danger"
});
} finally {
setLoading(false);
}
};
if (loading) {
return (
<>
Fetching user data...
>
);
}
return (
<>
Click the button to fetch the total number of registered users from the API.
Fetch user data
{totalUsers !== null && (
Number of users: {totalUsers}
)}
>
);
};
```
| Prop | Type | Description |
| --------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title` | String | The bolded text of the alert. |
| `message` | String | The main alert text. |
| `type` | `'info'` (default) \| `'tip'` \| `'success'` \| `'warning'` \| `'danger'` | The color of the alert.
`info`: a blue alert to provide general information.
`success`: a green alert indicating a positive outcome.
`warning`: a yellow alert indicating caution.
`danger`: a red alert indicating a negative outcome.
`tip`: a white alert to provide guidance.
|
### Reload page
Use the `reloadPage` action to reload the current page. This action can be accessed through the actions argument (`actions.reloadPage`).
```jsx theme={null}
import { Button, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
function Extension({ actions }) {
return (
actions.reloadPage()}>
Reload page
);
}
```
### Copy text to clipboard
Use the `copyTextToClipboard` action to copy text to your clipboard. This action can be accessed through the actions argument (`actions.copyTextToClipboard`) and returns a promise that resolves once the system clipboard has been updated. Its functionality is provided by the [Clipboard: writeText() method](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText) and follows the same requirements.
This action only works after the user has interacted with the page after loading ([transient activation](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/User_activation#transient_activation)).
```jsx theme={null}
import React from "react";
import { Button, Flex, hubspot, TextArea } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
function Extension({ actions }) {
const textToCopy = `Copy me!`;
// Use copy action on event handler
async function handleOnClick() {
try {
// The function is async, make sure to await it.
await actions.copyTextToClipboard(textToCopy);
actions.addAlert({
type: "success",
message: "Text copied to clipboard.",
});
} catch (error) {
// User error handling. copyTextToClipboard can fail with a `notAllowed` error.
console.log(error);
actions.addAlert({
type: "warning",
message: "Couldn't copy text.",
});
}
}
return (
Copy text
);
}
```
This action should be run by explicit user interaction, otherwise the action will fail by running before the page has rendered. For example, the following implementation would fail:
```jsx theme={null}
function CopyButtonBadExample({ actions }) {
const textToCopyWithoutUserPermission = `Please don't try this, it will fail`;
useEffect(() => {
/**
* Don't run copyTextToClipboard without explicit interaction.
* This will fail because the action will run before the page
* has rendered.
*/
async function badStuff() {
try {
await actions.copyTextToClipboard(textToCopyWithoutUserPermission);
actions.addAlert({
type: 'success',
message: 'text copied to clipboard',
});
} catch (error) {
console.log(error);
actions.addAlert({
type: 'warning',
message: "can't copy value",
});
}
}
badStuff();
}, []);
```
### Open overlays
To add another layer of UI to your extension, you can include overlays using the [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) and [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel) components.
* `Modal`: a pop-up dialog box best suited for short messages and action confirmations. A `'danger'` variant is included for destructive actions, such as deleting a contact.
* `Panel`: a slide-out sidebar best suited for longer, compartmentalized tasks that users might need to perform, such as multi-step forms. Includes a `'modal'` variant to obscure page content outside of the panel to focus the user on the panel task.
To add either type of overlay to an extension:
* Add the `overlay` prop to a [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button), [LoadingButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button), [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link), [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag), or [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image) component.
* Add the `Modal` or `Panel` component into the `overlay` prop.
```jsx expandable theme={null}
import { Button, Panel, PanelSection, PanelBody, PanelFooter, Text, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
const OverlayExampleCard = ({ actions }) => {
return (
<>
Welcome to my panel. Thanks for stopping by!Close the panel by clicking the X in the top right, or using the button below {
actions.closeOverlay("my-panel");
}}
>
Close
}
>
Open panel
>
);
};
```
```jsx expandable theme={null}
import { Button, Modal, ModalBody, ModalFooter, Text, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
const OverlayExampleCard = ({ actions }) => {
return (
<>
Welcome to my modal. Thanks for stopping by!Close the modal by clicking the X in the top right, or using the button below actions.closeOverlay("default-modal")}>Close modal
}
>
Open modal
>
);
};
```
By default, overlays include a close button in the top right, as shown in the example below.
* Only one `Modal` can be open at a time per extension. Opening a `Modal` when another one is already open will cause the first one to close.
* A `Modal` can be opened from a `Panel`, but a `Panel` can't be opened from a `Modal`.
### Open an iframe in a modal
Use the `openIframeModal` action to open an iframe in a modal window. This action accepts two arguments: a payload object that describes the modal, and an optional callback function that runs when the modal is closed.
The payload object for `openIframeModal` includes the following fields:
| Field | Type | Description |
| -------- | --------------------------------------- | ------------------------------------------------------------------- |
| `uri` | String Required | The URL to load in the iframe. |
| `height` | Number Required | The height of the modal in pixels. |
| `width` | Number Required | The width of the modal in pixels. |
| `title` | String | The title displayed at the top of the modal. |
| `flush` | Boolean | When `true`, removes the default padding around the iframe content. |
For example, the following code would result in an extension that opens an iframe on button click. The iframe is configured to contain the Wikipedia homepage with a height and width of 1000px and no padding. Upon closing the modal, a message will be logged to the console.
```jsx expandable wrap theme={null}
import { Link, Button, Text, Box, Flex, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(({ actions }) => );
const Extension = ({ openIframe }) => {
const handleClick = () => {
openIframe(
{
uri: "https://wikipedia.org/",
height: 1000,
width: 1000,
title: "Wikipedia in an iframe",
flush: true,
},
() => console.log("This message will display upon closing the modal.")
);
};
return (
<>
Clicking the button will open a modal dialog with an iframe that displays the content at the provided URL. Get
more info on how to do this:
here
Click me
>
);
};
```
When the user completes an action inside the iframe, the modal should close returning the user to the main page. To close the modal, the integration can use `window.postMessage` to signal that the user is done. The following messages are accepted:
* `{"action": "DONE"}`: the user has successfully completed the action.
* `{"action": "CANCEL"}`: the user has canceled the action.
```json theme={null}
window.top.postMessage(JSON.stringify({"action": "DONE"}), "*");
```
**Note:** The domain where the action originates must match the domain of the `uri` you passed into the `openIframeModal` action. If the domains do not match, the message will be ignored.
## CRM property actions
**CRM property actions**
Only available in `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`
| Action | Description |
| ----------------------------------------------------------- | -------------------------------------------------- |
| [`fetchCrmObjectProperties`](#fetch-crm-property-data) | Fetch property values from the current CRM record. |
| [`refreshObjectProperties`](#refresh-crm-record-properties) | Refresh CRM record properties on the page. |
| [`onCrmPropertiesUpdate`](#listen-for-property-updates) | Listen for updates to CRM record properties. |
While there isn't a dedicated UI component for uploading files, learn about options for [uploading files](#upload-files) in UI extensions.
### Fetch CRM property data
There are multiple ways to fetch CRM property data via the SDK:
* The [`useCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usecrmproperties) hook, which fetches properties from the current CRM record.
* The `fetchCrmObjectProperties` action, which fetches property data client-side at extension load time. This method is described below.
* `propertiesToSend`, which can be included in your `hubspot.fetch()` functions to fetch property data on the back-end at function invocation time.
* Use GraphQL to query CRM data through the `/collector/graphql` endpoint. Learn more about [querying CRM data using GraphQL](/docs/cms/start-building/features/data-driven-content/graphql/query-hubspot-data-using-graphql#query-hubspot-data-using-graphql).
To make GraphQL requests, your app must include the following scopes:
* `collector.graphql_schema.read`
* `collector.graphql_query.execute`
**fetchCrmObjectProperties**
While this method is still supported, you may want to switch to using the [`useCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usecrmproperties) hook instead
Using the `fetchCrmObjectProperties` method, you can get property values from the currently displaying CRM record without having to use HubSpot's APIs. This method is a part of the `actions` object that can be passed to the extension via `hubspot.extend`. You'll first need to add the object to `objectTypes` inside the card's `.json` config file. The objects you specify in `objectTypes` will also set which CRM objects will display the extension.
```jsx theme={null}
hubspot.extend(({ actions }) => );
const HelloWorld = ({ fetchProperties }) => {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
useEffect(() => {
fetchProperties(["firstname", "lastname"]).then(properties => {
setFirstName(properties.firstname);
setLastName(properties.lastname);
});
}, [fetchProperties]);
return (
Hello {firstName} {lastName}
);
};
```
You can specify individual properties or fetch all properties with an asterisk:
```jsx wrap theme={null}
fetchCrmObjectProperties("*").then(properties => console.log(properties));
```
The response for `fetchCrmObjectProperties` is formatted as:
```json theme={null}
{
"property1Name": "property1Value",
"property2Name": "property2Value"
}
```
`fetchCrmObjectProperties` returns raw property values. To apply HubSpot's display formatting to these values (for example, resolving enumeration values to their display labels), pass the results to the [`formatCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/utilities) utility.
### Refresh properties on the CRM record
Use `refreshObjectProperties` to refresh the property data on the CRM record, and any CRM data components on the record without needing to refresh the page. This includes [cards added to the record through HubSpot's UI](https://knowledge.hubspot.com/object-settings/customize-records#manage-cards-in-the-middle-column). This method will work for the CRM objects that you include in the extension's `.json` file in the `objectTypes` array.
This method will not refresh property values in app cards that are fetched using HubSpot's APIs. Only HubSpot's built-in property fields and properties in CRM data components will be refreshed.
```jsx theme={null}
import React, { useState } from 'react';
import {
Divider,
Button,
Input,
Flex,
hubspot
} from '@hubspot/ui-extensions';
hubspot.extend(({ actions }) => (
));
const Extension = ({
refreshObjectProperties,
}) => {
// Your extension logic goes here
// Refresh all properties of the object on the page
refreshObjectProperties();
return (
// Your extension body
);
};
```
### Listen for property updates
Use `onCrmPropertiesUpdate` to subscribe to changes made to properties on the CRM record and run `hubspot.fetch()` functions based on those changes. This only includes changes made from within the HubSpot UI, not property updates from outside the UI, such as via APIs. This action is intended to be used like a React hook.
The full API for this method is as follows:
```js theme={null}
export type onCrmPropertiesUpdateAction = (
properties: Array | '*',
callback: (
properties: Record,
error?: { message: string }
) => void
) => void;
```
As an example, the following function subscribes to updates made to the contact's first and last name properties, then logs those properties to the console.
```jsx theme={null}
onCrmPropertiesUpdate(["firstname", "lastname"], properties => console.log(properties));
```
You can subscribe to all properties by using an asterisk.
```jsx theme={null}
onCrmPropertiesUpdate("*", properties => console.log(properties));
```
To handle potential errors, pass the `error` argument to the callback.
```jsx theme={null}
onCrmPropertiesUpdate(['firstname','lastname'], (properties, error) => {
if(error) {
console.log(error.message}
}
else {
console.log(properties)
}
})
```
### Upload files
While there is no [UI component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for uploading files, there are a few ways you can upload files:
* [Create a custom file type property](https://knowledge.hubspot.com/properties/create-and-edit-properties), then use a [CRM property list component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list) to display and manage the property from CRM records. You can upload up to 10 files per file property, and file uploaded via file properties have the same [size and type limitations](https://knowledge.hubspot.com/files/supported-file-types) as files uploaded to the file manager.
* Include an [iframe modal](#open-an-iframe-in-a-modal) in the extension that loads an upload page, then upload files through the iframe.
# Context
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/context
Reference information for the context object provided by the UI extensions SDK.
The `context` object in the UI extensions SDK contains data related to the authenticated user and HubSpot account, along with data about where the extension was loaded.
## Access context data
You can access context data using either approach:
* **Props-based approach:** destructure `context` from the callback passed to `hubspot.extend()`, then pass it to your component as a prop.
* **Hook-based approach:** call the [`useExtensionContext`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#useextensioncontext) hook directly within your component.
## Fields
The `context` object has the following fields.
### Universal fields
| Field | Type | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `location` | `'crm.record.tab'` \| `'crm.record.sidebar'` \| `'crm.preview'` \| `'helpdesk.sidebar'` \| `'settings'` \| `'home'` | The UI extension's location. |
| `portal.id` | Number | The ID of the HubSpot account. |
| `portal.timezone` | String | The account's timezone. |
| `portal.dataHostingLocation` | `'na1'` \| `'na2'` \| `'na3'` \| `'ap1'` \| `'eu1'` | Geographic identifier that denotes the region where the current portal is hosted. See [HubSpot Cloud Infrastructure FAQ](https://knowledge.hubspot.com/account-security/hubspot-cloud-infrastructure-and-data-hosting-frequently-asked-questions) for more details. |
| `user.id` | Number | The user's ID. |
| `user.email` | String | The user's primary email address. |
| `user.emails` | Array | All of the user's associated email addresses. |
| `user.firstName` | String | The user's first name. |
| `user.lastName` | String | The user's last name. |
| `user.locale` | String | The user's locale. |
| `user.language` | String | The user's UI display language, as selected in their HubSpot profile preferences. Represented as a BCP 47 language code (e.g., `"en"`, `"de"`, `"fr"`). Note that this differs from `user.locale`, which controls date and number formatting — `language` reflects the actual UI language the user has selected in HubSpot. Defaults to `"en"` if the user has not opted into a non-English UI language. |
| `user.teams` | Array | An array containing information about teams that the user is assigned to. Each team object contains the `id` and `name` of the team, along with a `teammates` array that lists the IDs of other users on the team. |
| `user.permissions` | Array | An array of permission strings (e.g., `'integrations-management-write'`). |
| `variables` | Object | All of the project's [config profile](/docs/developer-tooling/local-development/build-with-config-profiles) variables. |
### CRM-specific fields
The following fields are only available in CRM extension points (`crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`):
| Field | Type | Description |
| --------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `crm.objectId` | Number | The ID of the CRM record (e.g., contact ID). |
| `crm.objectTypeId` | String | The ID of the CRM record's object type (e.g., `0-1`). See the [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-ids) for reference. |
| `extension.appId` | Number | The extension's app ID. |
| `extension.appName` | String | The name of the extension's app. |
| `extension.cardTitle` | String | The extension's title. |
# Hooks
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks
Reference information for hooks provided by the UI extensions SDK.
Hooks are used to simplify accessing context, performing actions, and fetching CRM data within UI extensions. The hooks provided by the UI extensions SDK are optimized to prevent unnecessary re-renders and automatically clean up resources when components unmount. You can pass inline arrays and objects to the hooks directly, as memoization is not required.
## Universal hooks
**Universal hooks**
Supported in all extension points: `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`, `settings`, `home`
| Hook | Description |
| --------------------------------------------- | -------------------------------------------------------------- |
| [`useExtensionApi`](#useextensionapi) | Access both context and actions from a single hook. |
| [`useExtensionContext`](#useextensioncontext) | Access contextual information about the extension environment. |
| [`useExtensionActions`](#useextensionactions) | Access actions that can be performed within HubSpot. |
| [`useCrmSearch`](#usecrmsearch) | Search CRM records by query or structured filters. |
| [`useDebounce`](#usedebounce) | Debounce a rapidly-changing value. |
```jsx wrap theme={null}
import {
useExtensionApi,
// OR
useExtensionContext,
useExtensionActions,
useCrmSearch,
useDebounce
} from "@hubspot/ui-extensions";
```
### useExtensionApi
The `useExtensionApi` hook provides access to available actions and contextual information from a single hook, for extension components that need access to both actions and context.
Otherwise, it's best practice to use the more specific `useExtensionActions` or `useExtensionContext`, depending on your use case.
* For a complete list of available context properties, see the [context reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/context).
* For a complete list of available actions, see the [actions reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions). The actions available depend on the extension point location.
The following example uses the `useExtensionApi` hook to display an alert (action) containing the user's first name (context) when a button is clicked.
```jsx theme={null}
import { Button, hubspot, useExtensionApi } from '@hubspot/ui-extensions';
hubspot.extend(() => );
function MyExtension() {
const { actions, context } = useExtensionApi();
return (
actions.addAlert({ message: `Hello ${context.user.firstName}!` })}>
Click me
);
}
```
```tsx theme={null}
import { Button, hubspot, useExtensionApi } from '@hubspot/ui-extensions';
hubspot.extend<'crm.record.tab'>(() => );
function MyExtension() {
const { actions, context } = useExtensionApi<'crm.record.tab'>();
return (
actions.addAlert({ message: `Hello ${context.user.firstName}!` })}>
Click me
);
}
```
### useExtensionContext
The `useExtensionContext` hook provides access to contextual information about the current extension environment, including location and other relevant data.
For a complete list of available context properties, see the [context reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/context).
The following example accesses the current extension location and renders it in a `Text` component:
```jsx theme={null}
import { Text, hubspot, useExtensionContext } from '@hubspot/ui-extensions';
hubspot.extend(() => );
function MyExtension() {
const context = useExtensionContext();
return (
Current location: {context.location}
);
}
```
```tsx theme={null}
import { Text, hubspot, useExtensionContext } from '@hubspot/ui-extensions';
hubspot.extend<'crm.record.tab'>(() => );
function MyExtension() {
const context = useExtensionContext<'crm.record.tab'>();
return (
Current location: {context.location}
);
}
```
### useExtensionActions
The `useExtensionActions` hook provides access to various actions that can be performed within the HubSpot interface. It's a generic hook that can be typed with specific extension point locations for better TypeScript support.
For a complete list of available actions, see the [actions reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions). The actions available depend on the extension point location.
The following example displays an alert when a button is clicked:
```jsx theme={null}
import { Button, hubspot, useExtensionActions } from '@hubspot/ui-extensions';
hubspot.extend(() => );
function MyExtension() {
const { addAlert } = useExtensionActions();
return (
addAlert({ message: "Action completed!" })}>
Click me
);
}
```
```tsx theme={null}
import { Button, hubspot, useExtensionActions } from '@hubspot/ui-extensions';
hubspot.extend<'crm.record.tab'>(() => );
function MyExtension() {
const { addAlert } = useExtensionActions<'crm.record.tab'>();
return (
addAlert({ message: "Action completed!" })}>
Click me
);
}
```
### useCrmSearch
The `useCrmSearch` hook searches CRM records of a given object type. The search is built using any combination of text query and structured filters, and returns record IDs, formatted properties, and optional associations.
```jsx wrap theme={null}
useCrmSearch(
{
objectType: "contact",
properties: ["firstname", "lastname", "email"],
query: "john",
filterGroups: [
{
filters: [
{ propertyName: "createdate", operator: "GT", value: "1704067200000" }
]
}
],
sorts: [{ propertyName: "createdate", direction: "DESCENDING" }],
pageLength: 20,
},
{
propertiesToFormat: "all",
formattingOptions: {
date: {
format: "MM-DD-YYYY",
relative: false,
},
dateTime: {
format: "MM-DD-YYYY hh:mm",
relative: false,
},
currency: {
addSymbol: true,
},
},
}
);
```
Configures the search request with the following parameters:
* `objectType` : the object type to search. Accepts type IDs (`"0-1"`), names (`"contact"`, `"deal"`), or custom object names with a `p_` prefix (`"p_pets"`).
* `properties`: an optional array of properties to return for each result.
* `query`: an optional free-text search query matched against the object's default searchable properties.
* `filterGroups`: an optional array of filter groups. Groups in the array are OR'd together; filters within a single group are AND'd. Each filter includes a `propertyName`, an `operator` (`EQ`, `NEQ`, `LT`, `LTE`, `GT`, `GTE`, `BETWEEN`, `IN`, `NOT_IN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY`, `CONTAINS_TOKEN`, `NOT_CONTAINS_TOKEN`), and a `value`, `values`, or `highValue` field depending on the operator. See [search the CRM](/docs/api-reference/latest/crm/search-the-crm#filter-search-results) for more details.
* `sorts`: an optional array of sort configurations. Each item requires a `propertyName` and a `direction` (`"ASCENDING"` or `"DESCENDING"`).
* `pageLength`: an optional number of results per page. Default: `10`. Max: `200`.
Either `'all'` or an array of property names to format.
Contains formatting options for the values returned from date, datetime, and currency properties.
* The `date` and `dateTime` objects can include `format` and `relative` subfields:
* `format` (string): a date or datetime string like `MM-DD-YYYY` or `MM-DD-YYYY:mm:ss`. Supports [standard date time string formats](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format).
* `relative` (boolean): set to `true` to display the amount of time passed since the returned value (e.g., `(1 day ago)` or `(1 hour ago)`).
* The `currency` object can include `addSymbol` (boolean), which sets whether the currency symbol should display with the number. Set to `true` to display the currency symbol.
**Please note:** formatting is applied based on property type (`date`, `datetime`, `currency`) rather than content. For example, `date` formatting will not apply to a string type property containing a date value.
```jsx theme={null}
{
isLoading: false,
error: null,
results: [
{
"objectId": 123456,
"properties": {
"email": "john@example.com",
"firstname": "John",
"lastname": "Smith"
}
}
],
total: 342,
pagination: {
"hasNextPage": true,
"hasPreviousPage": false,
"currentPage": 1,
"pageSize": 10,
"nextPage": () => {}, // function to fetch results for next page
"previousPage": () => {}, // function to fetch results for previous page
"reset": () => {} // function to reset to page one of results
},
isRefetching: false,
refetch: () => {} // function to refetch the latest search results
}
```
Indicates whether the data is being fetched.
For failed fetch requests, an object with error details. Will be `null` for successful requests.
The matching CRM records for the current page. Each item includes:
* `objectId`: the ID of the CRM record.
* `properties`: an object containing the requested property data, listed in alphabetical order.
The total number of matching records across all pages.
An object with pagination utilities, including:
* `hasNextPage`: a boolean indicating if more pages are available.
* `hasPreviousPage`: a boolean indicating if previous pages exist.
* `currentPage`: the current page number.
* `pageSize`: the number of items per page.
* `nextPage()`: the function to go to the next page.
* `previousPage()`: the function to go to the previous page.
* `reset()`: the function to reset to the first page.
Indicates whether a refetch request is in progress.
A function to refetch the latest search results, with formatting applied as specified in the original hook call. The current page will be preserved.
```jsx wrap highlight={10-22} theme={null}
import {
hubspot,
Text,
Button,
Flex,
useCrmSearch
} from "@hubspot/ui-extensions";
const Extension = () => {
const { results, total, isLoading, error, pagination, isRefetching, refetch } = useCrmSearch(
{
objectType: 'contact',
properties: ['firstname', 'lastname', 'email'],
filterGroups: [{
filters: [
{ propertyName: 'lifecyclestage', operator: 'EQ', value: 'lead' }
]
}],
sorts: [{ propertyName: 'createdate', direction: 'DESCENDING' }],
pageLength: 10,
}
);
if (isLoading) {
return Loading results...;
}
if (isRefetching) {
return Refetching results...;
}
if (error) {
return Error loading results: {error.message};
}
return (
{total} records found. Displaying page {pagination.currentPage}.
{results.map(record => (
{record.properties.firstname} {record.properties.lastname}: {record.properties.email}
))}
Previous page
Next page
Reset to first page
Fetch latest data
);
};
hubspot.extend(() => );
```
```jsx theme={null}
import { useState } from 'react';
import { hubspot, Input, Text, useCrmSearch, useDebounce } from '@hubspot/ui-extensions';
const Extension = () => {
const [query, setQuery] = useState('');
// Waits for the user to stop typing before triggering a new search request
const debouncedQuery = useDebounce(query);
const { results, isLoading } = useCrmSearch({
objectType: 'contact',
properties: ['firstname', 'lastname', 'email'],
query: debouncedQuery,
});
return (
<>
{isLoading && Loading...}
{results.map(record => (
{record.properties.firstname} {record.properties.lastname}
))}
>
);
};
hubspot.extend(() => );
```
```jsx theme={null}
import React from 'react';
import { hubspot, Text, useCrmSearch, useExtensionContext } from '@hubspot/ui-extensions';
const Extension = () => {
const { crm } = useExtensionContext();
const { results, isLoading } = useCrmSearch(
{
objectType: 'deal',
properties: ['dealname', 'amount', 'dealstage'],
filterGroups: [
{
filters: [
{
// 'associations.{objectType}' is a pseudo-property that filters by association
propertyName: 'associations.contact',
operator: 'EQ',
value: crm.objectId,
},
],
},
],
},
{
propertiesToFormat: 'all',
formattingOptions: {
currency: {
addSymbol: true,
},
},
},
);
if (isLoading) return Loading associated deals...;
return (
<>
{results.map((deal) => (
{deal.properties.dealname}Amount: {deal.properties.amount}Stage: {deal.properties.dealstage}
))}
>
);
};
hubspot.extend(() => );
```
### useDebounce
The `useDebounce` hook prevents a rapidly-changing value from propagating until it has stopped changing for a given number of milliseconds. Use it to avoid triggering expensive operations (like [CRM search API](/docs/api-reference/latest/crm/search-the-crm) requests) on every change, and instead wait until the user has paused.
```jsx wrap theme={null}
import { useDebounce } from '@hubspot/ui-extensions';
const [searchText, setSearchText] = useState('');
// debouncedQuery only updates after searchText has been stable for 300ms
const debouncedQuery = useDebounce(searchText, 300);
```
The hook accepts any JSON-serializable value: strings, numbers, booleans, `null`, arrays, and objects. Objects and arrays are compared by deep equality, so a new object reference with the same content won't reset the debounce timer. Passing a non-serializable value (e.g., a function or a `Date` object) will result in incorrect deep equality comparisons.
```jsx wrap theme={null}
const [filters, setFilters] = useState({ status: 'OPEN', assignee: null });
const debouncedFilters = useDebounce(filters, 500);
```
| Parameter | Type | Description |
| ----------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value` | `string` \| `number` \| `boolean` \| `null` \| `object` \| `array` | The value to debounce. Must be JSON-serializable. Objects and arrays are compared by deep equality, so a new object reference with the same content won't reset the debounce timer.
Functions and class instances are not supported. |
| `delayMs` | `number` | Milliseconds to wait after the last change before updating the debounced value. Defaults to `300`. Changing this value mid-render resets the pending timer. Passing a value of `0` still defers the update by one render cycle because the underlying `useEffect` is asynchronous. |
The hook returns the debounced version of `value` with the same type as the input. On the initial render, `value` is returned immediately without delay. The pending timer is cancelled when the component unmounts.
Pair `useDebounce` with `useCrmSearch` (or any data-fetching hook) to avoid firing a request on every keystroke:
```jsx theme={null}
import { useState } from 'react';
import { Input, Text, useDebounce, useCrmSearch, hubspot } from '@hubspot/ui-extensions';
const Extension = () => {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { results, isLoading } = useCrmSearch({
objectType: '0-1',
query: debouncedQuery,
properties: ['firstname', 'lastname', 'email'],
});
return (
<>
{isLoading && Searching...}
{results.map((r) => (
{r.properties.firstname} {r.properties.lastname}
))}
>
);
};
hubspot.extend(() => );
```
Debounce validation to avoid showing an error message on every keystroke and instead waiting until the user has finished typing. In this example, the error only appears after the user pauses for 400ms.
```jsx theme={null}
import { useState } from 'react';
import { Input, Alert, useDebounce, hubspot } from '@hubspot/ui-extensions';
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const Extension = () => {
const [email, setEmail] = useState('');
const debouncedEmail = useDebounce(email, 400);
const isInvalid =
debouncedEmail.length > 0 && !EMAIL_PATTERN.test(debouncedEmail);
return (
<>
{isInvalid && (
Don't forget to add a valid email address!
)}
>
);
};
hubspot.extend(() => );
```
## CRM-specific hooks
These hooks are only available in CRM extension points (`crm.record.tab`, `crm.record.sidebar`, `crm.preview`, and `helpdesk.sidebar`).
* [`useCrmProperties`](#usecrmproperties): fetch properties from the current CRM record
* [`useAssociations`](#useassociations): fetch associated CRM records
```jsx wrap theme={null}
import {
useCrmProperties,
useAssociations
} from "@hubspot/ui-extensions/crm";
```
### useCrmProperties
The `useCrmProperties` hook fetches properties from the current CRM record with optional formatting. It accepts an array of properties to fetch, along with an optional object to format the returned data.
```jsx theme={null}
useCrmProperties(["firstname", "lastname", "email"], {
propertiesToFormat: "all",
formattingOptions: {
date: {
format: "MM-DD-YYYY",
relative: false,
},
dateTime: {
format: "MM-DD-YYYY hh:mm",
relative: false,
},
currency: {
addSymbol: true,
},
},
});
```
Either `'all'` or an array of property names to format.
Contains formatting options for the values returned from date, datetime, and currency properties.
* The `date` and `dateTime` objects can include `format` and `relative` subfields:
* `format` (string): a date or datetime string like `MM-DD-YYYY` or `MM-DD-YYYY:mm:ss`. Supports [standard date time string formats](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format).
* `relative` (boolean): set to `true` to display the amount of time passed since the returned value (e.g., `(1 day ago)` or `(1 hour ago)`).
* The `currency` object can include `addSymbol` (boolean), which sets whether the currency symbol should display with the number. Set to `true` to display the currency symbol.
Formatting is applied based on property type (`date`, `datetime`, `currency`) rather than content. For example, `date` formatting will not apply to a string type property containing a date value.
```jsx theme={null}
{
isLoading: false,
error: null,
properties: {
"firstname": "Joe",
"lastname": "Smith",
"email": "joe@joebiz.com"
},
isRefetching: false,
refetch: () => {} // function to refetch properties
}
```
Indicates whether the data is being fetched.
For failed fetch requests, an object with error details. Will be `null` for successful requests.
An object with key-value pairs of returned properties, as formatted by `formattingOptions`. Properties are returned in alphabetical order.
Indicates whether a refetch request is in progress.
A function to refetch the latest property values, with formatting applied as specified in the original hook call.
```jsx highlight={8-26} theme={null}
import {
hubspot,
Text,
Button
} from "@hubspot/ui-extensions";
import { useCrmProperties } from "@hubspot/ui-extensions/crm";
const Extension = () => {
const { properties, isLoading, error, refetch, isRefetching } = useCrmProperties(
// Array of properties to return
['firstname', 'lastname', 'email'],
// Optional formatting options for returned data
{
propertiesToFormat: 'all',
formattingOptions: {
date: {
format: 'MM-DD-YYYY',
relative: false
},
dateTime: {
format: 'MM-DD-YYYY hh:mm',
relative: false
},
currency: {
addSymbol: true
}
}
}
);
if (isLoading) {
return Loading properties...;
}
if (isRefetching) {
return Refetching properties...;
}
if (error) {
return Error loading properties: {error.message};
}
return (
<>
The contact is "{properties.firstname} {properties.lastname}"
with email "{properties.email}".
Refetch property data
>
);
};
hubspot.extend(() => );
```
### useAssociations
The `useAssociations` hook fetches CRM records of a specific object type associated with the currently displaying record. It accepts an object containing configuration details for the association fetch request, and an optional object that formats returned property data.
```jsx theme={null}
useAssociations(
{
// Object type ID to fetch associations for
toObjectType: "0-1",
// Optional properties to fetch from associated records
properties: ["firstname", "lastname", "email", "phone"],
// Optional pagination settings
pageLength: 25,
},
// Optional formatting configuration (same as useCrmProperties)
{
propertiesToFormat: "all",
formattingOptions: {
date: {
format: "MM-DD-YYYY",
relative: false,
},
dateTime: {
format: "MM-DD-YYYY hh:mm",
relative: false,
},
currency: {
addSymbol: true,
},
},
}
);
```
Configures the association data fetch request with:
* `toObjectType`: the object type ID to fetch associations from (e.g., '0-1' for contacts).
* `properties`: an optional array of properties to fetch from associated records.
* `pageLength`: an optional number of items per page (defaults to 10).
Either `'all'` or an array of property names to format.
Contains formatting options for the values returned from date, datetime, and currency properties.
* The `date` and `dateTime` objects can include `format` and `relative` subfields:
* `format` (string): a date or datetime string like `MM-DD-YYYY` or `MM-DD-YYYY:mm:ss`. Supports [standard date time string formats](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format).
* `relative` (boolean): set to `true` to display the amount of time passed since the returned value (e.g., `(1 day ago)` or `(1 hour ago)`).
* The `currency` object can include `addSymbol` (boolean), which sets whether the currency symbol should display with the number. Set to `true` to display the currency symbol.
```jsx theme={null}
{
isLoading: false,
error: null,
results: [
{
"toObjectId": 70284463640,
"associationTypes": [
{
"category": "HUBSPOT_DEFINED",
"typeId": 449,
"label": ""
}
],
"properties": {
"email": "emailmaria@hubspot.com",
"firstname": "Maria",
"lastname": "Johnson (Sample Contact)"
}
}
],
pagination: {
"hasNextPage": true,
"hasPreviousPage": false,
"currentPage": 1,
"pageSize": 1,
"nextPage": () => {}, // function to fetch results for next page
"previousPage": () => {}, // function to fetch results for previous page
"reset": () => {} // function to reset to page one of results
},
isRefetching: false,
refetch: () => {} // function to refetch the latest associations data
}
```
Indicates whether the data is being fetched.
For failed fetch requests, an object with error details. Will be `null` for successful requests.
Association details of the returned CRM records. Includes the following fields:
* `toObjectId`: the ID of the CRM record.
* `associationTypes`: an array of [association type](/docs/api-reference/latest/crm/associations/associate-records/guide) information.
* `properties`: an object containing the requested property data, listed in alphabetical order.
An object with pagination utilities, including:
* `hasNextPage`: a boolean indicating if more pages are available.
* `hasPreviousPage`: a boolean indicating if previous pages exist.
* `currentPage`: the current page number.
* `pageSize`: the number of items per page.
* `nextPage()`: the function to go to next page.
* `previousPage()`: the function to go to previous page.
* `reset()`: the function to reset to first page.
Indicates whether a refetch request is in progress.
A function to refetch the latest associations data, with formatting applied as specified in the original hook call. The current page will be preserved.
```jsx wrap highlight={10-37} theme={null}
import {
Text,
Button,
Flex,
hubspot
} from "@hubspot/ui-extensions";
import { useAssociations } from "@hubspot/ui-extensions/crm";
const Extension = () => {
const { results, error, isLoading, pagination, isRefetching, refetch } = useAssociations(
{
// Object type ID to fetch associations for
toObjectType: '0-1',
// Optional properties to fetch from associated objects
properties: ['firstname', 'lastname', 'email', 'phone'],
// Optional pagination settings
pageLength: 25,
},
// Optional formatting configuration (same as useCrmProperties)
{
propertiesToFormat: 'all',
formattingOptions: {
date: {
format: 'MM-DD-YYYY',
relative: false
},
dateTime: {
format: 'MM-DD-YYYY hh:mm',
relative: false
},
currency: {
addSymbol: true
}
}
}
);
if (isLoading) {
return Loading associations...;
}
if (isRefetching) {
return Refetching associations...;
}
if (error) {
return Error loading associations: {error.message};
}
return (
Associations (Page {pagination.currentPage})
{results.map((association, index) => (
Association {index + 1}: Object ID {association.toObjectId}
Association Types: {association.associationTypes.map(type => type.label).join(', ')}
{Object.entries(association.properties).map(([key, value]) => (
{key}: {value || 'N/A'}
))}
))}
Previous Page
Next Page
Reset to First Page
Fetch Latest Data
);
};
hubspot.extend(() => );
```
## Best practices
### Always use TypeScript generics
```tsx theme={null}
// ✅ Good - Typed for better IntelliSense and type safety
const actions = useExtensionActions<'crm.record.tab'>();
const context = useExtensionContext<'crm.record.tab'>();
// ❌ Avoid - Less type safety and IntelliSense
const actions = useExtensionActions();
const context = useExtensionContext();
```
### Extract hook calls to component level
```tsx theme={null}
// ✅ Good - Hooks at component level
function MyExtension() {
const { addAlert } = useExtensionActions<'crm.record.tab'>();
const context = useExtensionContext<'crm.record.tab'>();
const handleClick = () => {
addAlert({ message: `Action from ${context.location}` });
};
return Click me;
}
// ❌ Avoid - Don't call hooks in event handlers
function MyExtension() {
const handleClick = () => {
const { addAlert } = useExtensionActions(); // Wrong!
addAlert({ message: "Hello" });
};
return Click me;
}
```
# Logging
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/logging
Reference information for the logger API provided by the UI extensions SDK.
In addition to the logs that are automatically surfaced by the extension logs panel, you can send your own log messages using the `logger` API provided by the UI extensions SDK.
When an extension fails to load on a CRM record, an error message will display. This error message will contain a trace ID, which you can use to locate the custom log messages within the extension's logs.
Learn more about [monitoring and debugging UI extensions during local development and after deployment using HubSpot's built-in logging and monitoring tools](/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring).
## Methods
The following methods are available. Each method accepts a single string argument.
* `logger.info`
* `logger.debug`
* `logger.warn`
* `logger.error`
For example, the following extension code includes a few different log messages to help better identify where an error has occurred:
```jsx expandable wrap theme={null}
import React from "react";
import { Button, Divider, Flex, hubspot, logger, Text } from "@hubspot/ui-extensions";
logger.warn("Warning in the middle tab, before my extension");
hubspot.extend(({ context }) => );
const MiddleTabLogging = ({ context }) => {
logger.debug(JSON.stringify(context, null, 2));
const callFetchSuccess = () => {
return hubspot
.fetch("https://jsonplaceholder.typicode.com/posts/1", { method: "GET" })
.then(response => response.json())
.then(result => logger.info(JSON.stringify(result, null, 2)))
.catch(error => logger.error(error.message));
};
const callFetchFail = () => {
return hubspot
.fetch("https://jsonplaceholder.typicode.com/posts/404", { method: "GET" })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(result => logger.info(JSON.stringify(result, null, 2)))
.catch(error => logger.error(error.message));
};
return (
Test out the logger with the following buttons.The browser's developer console will show your events in local dev.Test fetch functionsFetch success ✅Fetch error ❌Test different log levels. logger.info("Logging an info!")}>logger.info() logger.debug("Logging a debug!")}>logger.debug() logger.warn("Logging a warning!")}>logger.warn() logger.error("Logging an error!")}>logger.error()
Deploy the app and crash the card. Use the Trace ID to see what happened in the Log Traces tab in your private
app's dashboard.
{
throw new Error("Card crashed");
}}
>
Crash the card
);
};
```
## Notes and limitations
* Custom log messages are not sent while in local development mode. They are logged to the browser console instead.
* All logs are sent as batches with a maximum of 100 logs per batch.
* Each HubSpot account is rate limited to 1,000 logs per minute. After exceeding that limit, all logging is stopped until the page is reloaded.
* The logger will queue a maximum of 10,000 pending messages. Any subsequent logs will be dropped until the queue is below the maximum.
* Queued logs are processed at a rate of five seconds per log batch.
* Queued logs are dropped when the page or is refreshed or closed.
# UI extensions SDK overview
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/overview
Learn more about the UI extensions SDK, which provides utilities for app cards.
The UI extensions SDK is the foundation for adding [app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview) functionality, providing methods and utilities that enable you to:
* Access account, extension, and user context
* Perform actions like displaying alert banners
* Access context, perform actions, or fetch data using hooks
* Render the UI through UI components
* Use tooling for testing, linting, and sharing code
* Log custom messages for debugging
## Register the extension
UI extensions, like any React front-end, are written as React components. However, unlike typical React components, you must register your UI extension with HubSpot by including `hubspot.extend()` inside the component file instead of exporting it. This is not required when you create a sub-component. For cleaner code, reuse and include them inside your extension.
The `hubspot.extend()` function receives the following arguments:
* `context`: provides account, extension, and user context to the extension.
* `actions`: makes actions available to the extension.
```jsx wrap theme={null}
hubspot.extend(({ context, actions }) => );
```
The provided arguments can then be passed to the extension component as props.
```jsx theme={null}
// Define the extension to be run within HubSpot
hubspot.extend(({ context, actions }) => (
));
// Define the Extension component, taking in context, and sendAlert as props
const Extension = ({ context, sendAlert }) => {
...
};
```
If your extension doesn't need to access context or perform actions, you don't need to provide these arguments to `hubspot.extend()` or the extension component.
```jsx theme={null}
hubspot.extend(() => );
const Extension = () => {
...
};
```
## Context
The `context` object contains data related to the authenticated user and HubSpot account (e.g., Hub ID, user's email), along with data about where the extension was loaded (e.g., the UI extension's location). You can access context data using either approach:
* **Props-based approach:** destructure `context` from the callback passed to `hubspot.extend()`, then pass it to your component as a prop.
* **Hook-based approach:** call the [`useExtensionContext`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#useextensioncontext) hook directly within your component.
For a full list of supported fields, refer to the [context reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/context).
## Actions
Below are the actions you can perform with the SDK. You can access actions using either a props-based approach or a hook-based approach:
* **Props-based approach:** destructure `actions` from the callback passed to `hubspot.extend()`, then pass it to your component as a prop.
* **Hook-based approach:** call the [`useExtensionActions`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#useextensionactions) hook directly within your component.
For more detail about using the actions, refer to the [actions reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions).
Note that some UI components include a set of actions separate from the SDK actions below, such as the [CRM action components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview).
**Universal actions**
Supported in all extension points: `settings`, `home`, `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`
| Action | Description |
| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| [`addAlert`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#display-alert-banners) | Display alert banners to the user. |
| [`reloadPage`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#reload-page) | Reload the current page. |
| [`copyTextToClipboard`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#copy-text-to-clipboard) | Copy text to the user's clipboard. |
| [`closeOverlay`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#open-overlays) | Close an open overlay or modal. |
| [`openIframeModal`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#open-an-iframe-in-a-modal) | Open an iframe in a modal window. |
**CRM property actions**
Only available in `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`
| Action | Description |
| ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| [`fetchCrmObjectProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#fetch-crm-property-data) | Fetch property values from the current CRM record. |
| [`refreshObjectProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#refresh-crm-record-properties) | Refresh CRM record properties on the page. |
| [`onCrmPropertiesUpdate`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#listen-for-property-updates) | Listen for updates to CRM record properties. |
While there isn't a dedicated UI component for uploading files, learn about options for [uploading files](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#upload-files) in UI extensions.
## Hooks
The SDK provides hooks to simplify accessing context, performing actions, and fetching CRM data within UI extensions. These hooks are optimized to prevent unnecessary re-renders and automatically clean up resources when components unmount. You can pass inline arrays and objects to the hooks directly, as memoization is not required.
For more detail about using the hooks, refer to the [hooks reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks).
**Universal hooks**
Supported in all extension points: `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`, `settings`, `home`
| Hook | Description |
| ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| [`useExtensionApi`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#useextensionapi) | Access both context and actions from a single hook. |
| [`useExtensionContext`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#useextensioncontext) | Access contextual information about the extension environment. |
| [`useExtensionActions`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#useextensionactions) | Access actions that can be performed within HubSpot. |
| [`useCrmSearch`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usecrmsearch) | Search CRM records by query or structured filters. |
| [`useDebounce`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usedebounce) | Debounce a rapidly-changing value. |
```jsx wrap theme={null}
import {
useExtensionApi,
useExtensionContext,
useExtensionActions,
useCrmSearch,
useDebounce
} from "@hubspot/ui-extensions";
```
**CRM-specific hooks**
Only available in `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`
| Hook | Description |
| ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| [`useCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usecrmproperties) | Fetch properties from the current CRM record. |
| [`useAssociations`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#useassociations) | Fetch associated CRM records. |
```jsx wrap theme={null}
import {
useCrmProperties,
useAssociations
} from "@hubspot/ui-extensions/crm";
```
## Utilities
The SDK provides utility functions for working with CRM data outside of hooks and actions. Currently, this includes `formatCrmProperties`, which applies HubSpot's display formatting to raw CRM property values you've retrieved from an action like [`fetchCrmObjectProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#fetch-crm-property-data), or from `hubspot.fetch()` and serverless function requests.
For more detail, refer to the [utilities reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/utilities).
## UI components
When building a UI extension, you can include HubSpot-provided reusable components, ranging from simple text fields to out-of-the-box CRM object reports.
Components are imported at the top of your `tsx` or `jsx` extension file from one of two SDK directories:
* [Standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview#standard-components) are imported from `'@hubspot/ui-extensions'`
* [CRM data](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview#crm-data-components) and [CRM action components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview#crm-action-components) are imported from `'@hubspot/ui-extensions/crm'`
For documentation on the UI components included in the SDK, check out the [UI components reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview).
## Tools
Tooling is available to improve the extension development experience and catch issues before deployment.
* **[Linting](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview):** platform-specific constraints and best practices enforced by the `@hubspot/eslint-config-ui-extensions` ESLint plugin. These rules catch issues and give immediate feedback so you can fix problems before deploying an extension. The linting tools catch common platform violations, like using document, fetch, or native HTML elements.
* **[Testing](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/overview):** automated tests to help catch regressions before deploying. The `@hubspot/ui-extensions/testing` module provides a test renderer that simulates the extension environment, so you can render components, trigger events, and assert on output without deploying.
* **TypeScript:** TypeScript helps catch platform constraint violations while editing, before you run code. For the best experience, configure your `tsconfig.json` with `"lib": ["ES2020", "WebWorker"]`. This excludes DOM types from the type checker, so your editor treats document, window, and native fetch as errors, matching the actual runtime environment.
## Custom log messages
Using `logger` methods, you can send custom log messages to HubSpot for more in-depth troubleshooting of deployed extensions. Custom log messages will appear in the [app's logs in HubSpot](/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring).
The following methods are available:
* `logger.info`
* `logger.debug`
* `logger.warn`
* `logger.error`
Each method accepts a single string argument. Learn more about [creating custom log messages for debugging](/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring#custom-log-messages).
# Utilities
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/utilities
Reference information for utilities provided by the UI extensions SDK.
The UI extensions SDK provides a `formatCrmProperties` utility that applies HubSpot's standard display formatting to CRM property values. This helps keep your extension consistent with HubSpot's UI, and is particularly useful when you get raw property values from:
* The [`fetchCrmObjectProperties` action](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#fetch-crm-property-data)
* [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data) or [serverless function](/docs/apps/developer-platform/add-features/serverless-functions/overview) requests
You don't need to use this function for values returned by the [`useCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usecrmproperties) or [`useCrmSearch` hooks](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usecrmsearch), as they provide their own formatting options.
## Usage
`formatCrmProperties` accepts raw property values and returns them with HubSpot's standard display formatting applied. By default, it formats recognized [property types](/docs/api-reference/legacy/crm/properties/guide#property-type-and-fieldtype-values). For example, it formats phone numbers and numeric values, and resolves enumeration values to their display labels. You can optionally use the `formattingOptions` prop to customize how dates, datetimes, and currencies display.
The function accepts either a single property map or an array of property maps, and returns the same shape it receives.
```jsx theme={null}
import { formatCrmProperties } from '@hubspot/ui-extensions';
// Format a single record
const formatted = await formatCrmProperties({
properties: { firstname: 'john', createdate: '1704067200000' },
objectType: '0-1',
formattingOptions: {
dateTime: {
format: 'MM-DD-YYYY'
}
}
});
// Output: { firstname: 'John', createdate: '01-01-2024' }
// Format multiple records in one call
const results = await formatCrmProperties({
properties: [
{ firstname: 'john', createdate: '1704067200000' },
{ firstname: 'jane', createdate: '1706745600000' },
],
objectType: '0-1',
});
// Output: [{ firstname: 'John', createdate: 'January 1, 2024' }, ...]
```
## Parameters
| Parameter | Type | Description |
| ---------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `properties` | `Record` or `Array>` | One or more CRM property maps to format. Each map is an object of property internal names to raw string values (or `null` for empty properties). If you pass a single map, a single formatted map is returned. If you pass an array, a formatted array is returned. |
| `objectType` | `string` | The object type ID of the CRM records being formatted (e.g., `'0-1'` for contacts, `'0-2'` for companies). This is used to resolve property definitions for formatting. See [full list of object IDs](/docs/api-reference/latest/crm/understanding-the-crm#object-type-id-values) for reference. |
| `formattingOptions` | `FormattingOptions` | Optional formatting configuration. See [`FormattingOptions`](#formatting-options) below for more details. |
`formatCrmProperties` fetches property definitions for the object type over the network. Because calls can fail, it's best to wrap them in a `try/catch` to handle rejections.
### Formatting options
The `formatCrmProperties` function applies HubSpot's standard display formatting to recognized [property types](/docs/api-reference/legacy/crm/properties/guide#property-type-and-fieldtype-values) even without `formattingOptions`. However, you can use the `formattingOptions` prop to customize how date, datetime, and currency properties display.
Below are the available options for the `formattingOptions` parameter.
| Field | Type | Description |
| -------------------- | --------- | --------------------------------------------------------------------------------------- |
| `date.format` | `string` | Display format for date properties (e.g., `'MM-DD-YYYY'`). |
| `date.relative` | `boolean` | When `true`, returns relative date strings (e.g., `'3 days ago'`). |
| `dateTime.format` | `string` | Display format for datetime properties (e.g., `'MM-DD-YYYY hh:mm'`). |
| `dateTime.relative` | `boolean` | When `true`, returns relative datetime strings. |
| `currency.addSymbol` | `boolean` | When `true`, includes the currency symbol in the formatted value (e.g., `'$1,200.00'`). |
The following example applies currency, date, and datetime formatting to a deal's properties:
```jsx theme={null}
const formatted = await formatCrmProperties({
properties: {
amount: '1200',
closedate: '1704067200000',
last_modified_date: '1735689600000',
},
objectType: '0-3',
formattingOptions: {
currency: { addSymbol: true },
date: { format: 'MM-DD-YYYY' },
dateTime: { relative: true },
},
});
```
## Return value
The function returns a `Promise` that resolves to:
* A single `Record` when `properties` is a single object.
* An `Array>` when `properties` is an array.
The result order matches the input order when an array is provided. Property values that are `null` in the input are preserved as `null` in the output.
The function handles the following edge cases:
* If you pass an unknown property name, the function will return its value unchanged.
* If you pass an empty array, the function will return `[]`.
* If you pass an invalid `objectType`, the promise will reject.
# App configuration
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/app-configuration
Reference information for configuration options for apps built on the new developer platform
Below, find reference information for developer platform app features, including configuration file definitions, scopes details, and more.
## Project structure
* All project components must live within the `src` directory specified in the top-level `hsproject.json` config file.
* All app features and components must live within the `src/app/` directory. Within this `app/` directory, you'll define subdirectories for each feature you want your app to support:
* App events are configured within `app-events/`.
* App objects are defined within `app-objects/`.
* All card features are defined within `cards/` .
* Settings page features are defined within `settings/`.
* Serverless functions are configured within `functions/`.
* Telemetry is configured within `telemetry/`.
* Webhook subscriptions are defined within `webhooks/`.
* Custom workflow actions are defined within `workflow-actions/`.
* Within each feature subdirectory, you'll configure the feature are using a `*-hsmeta.json` file. You can prefix the file name with something meaningful to your app (e.g., `my-app-hsmeta.json`), as long as the file ends with `-hsmeta.json`. These files must live at the root level of their respective folder (e.g., `app/my-app-hsmeta.json`, `cards/my-card-hsmeta.json`).
The example directory structure below outlines all available features. Details for configuring the top-level app schema `app-hsmeta.json` file are provided in the [app schema section](#app-schema) below. Once you're ready to add app features, check out the [adding app features](#adding-app-features) section.
```shell theme={null}
my-project-folder/
└── hsproject.json
└── src
└── app/
└── app-hsmeta.json/
└── app-events/
└── my-event-type-hsmeta.json
└── app-objects/
└── my-app-object-hsmeta.json
└── cards/
└── MyCard.jsx
└── my-app-card-hsmeta.json
└── package.json
└── functions/
└── NewFunction.js
└── private-function-hsmeta.json
└── package.json
└── settings/
└── Settings.tsx
└── settings-hsmeta.json
└── package.json
└── telemetry/
└── telemetry-hsmeta.json
└── webhooks/
└── webhooks-hsmeta.json
└── workflow-actions/
└── custom-action-hsmeta.json
```
The HubSpot Visual Studio Code extension provides [type checking](/docs/developer-tooling/local-development/vs-code-extension#validate-hs-meta-json-config-files) for each of the properties in your `*-hsmeta.json` configuration files.
## Specifying UIDs
The `uid` field is an internally unique identifier for your specific app, and must also be globally unique within the project. Any [app features](#adding-app-features) will each have their own `uid` defined in their respective `*-hsmeta.json` files, which must be distinct from the top-level `uid` you choose in your app's `app-hsmeta.json` file.
## App schema
The top-level configuration for your app is specified within an `app-hsmeta.json` configuration file in the `app` directory.
```shell theme={null}
my-project-folder/
└── src
└── app/
└── app-hsmeta.json/
```
Below are the configuration options available for `app-hsmeta.json`.
```json theme={null}
{
"uid": "new_developer_platform_app",
"type": "app",
"config": {
"description": "An example to demonstrate how to build an app with developer projects.",
"name": "my first app",
"logo": "/app/app-logo.png",
"distribution": "marketplace",
"auth": {
"type": "oauth",
"redirectUrls": ["http://localhost:3000/oauth-callback"],
"requiredScopes": ["crm.objects.contacts.read", "crm.objects.contacts.write"],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
"permittedUrls": {
"fetch": ["https://api.hubapi.com"],
"iframe": [],
"img": []
},
"support": {
"supportEmail": "support@example.com",
"documentationUrl": "https://example.com/docs",
"supportUrl": "https://example.com/support",
"supportPhone": "+18005555555"
}
}
}
```
Each of the configuration options are detailed in the table below. More context on [distributing your app](#distribution), configuring [authentication](#authentication), and specifying [scopes](#scopes) are provided in the sections below the table.
| Field | Type | Description |
| ------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uid` | String | An internal unique identifier for the app. Must be globally unique within the project. Can be any string up to 64 characters. Characters can be uppercase or lowercase, and can include numbers, underscores (`_`), dashes (`-`), and periods (`.`). |
| `type` | String | The type of component. Must match the name of the parent folder (`app`). |
| `description` | String | A description of what the app does for the installing user. Can be any string up to 8192 characters. |
| `name` | String | The name of the app, which will display in HubSpot. Can be any string up to 200 characters. Must not start or end with a whitespace character. |
| `logo` | String | A path to a logo file for your app. Supported file types are: `png`, `jpeg`/`jpg`, `gif`, and `bmp`.
Your logo should be recognizable at smaller scales, since it will appear as a thumbnail when the app is installed in HubSpot. |
| `distribution` | String | The method of app distribution, which can be set to one of the following:
`marketplace`: used if you want the app to be eligible for listing in the HubSpot Marketplace.
`private`: used if you only want to install your app in a specific set of allowlisted accounts, or a single account at a time.
Learn more in the [distribution](#distribution) section below.
|
| `auth` | Object | An object containing the app's authentication method details. See the [authentication section](#authentication) below for details. |
| `permittedUrls` | Object | An array containing the URLs that the app is allowed to call. URLs must use the HTTPS scheme and must contain an [authority](https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Web_mechanics/What_is_a_URL#authority), followed by an optional path prefix if needed. |
| `supportEmail` | String | A valid email address that users can contact for support. |
| `documentationUrl` | String | The external URL that users can navigate to for supporting documentation. Must use HTTPS. |
| `supportUrl` | String | The external URL that users can navigate to for additional support. Must use HTTPS. |
| `supportPhone` | String | The phone number that users can contact for support. Must start with a plus sign (`+`). |
### Distribution
The `distribution` field in your app schema allows you to configure how you want to distribute your app:
* If you plan to list your app on the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps), set the `distribution` field to `"marketplace"`. If you choose this option, ensure that you set the `type` within the `auth` property to `oauth`, as detailed in the [authentication](#authentication) section below.
* If you want to allow your app to be installed in a specific set of allowlisted accounts, or if you want to restrict installation to a single account at a time, set `distribution` to `"private"`. Ensure that you set the `type` within the `auth` property accordingly:
* If you want to install your app in multiple accounts based on [an allowlist you configure in your project settings](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#manage-authentication-for-your-app), set the authentication `type` to `oauth`.
* To restrict installation to a single account, either the same you use for development or another account that the installing user has access to, set the authentication `type` to `static`.
### Authentication
Authentication for your app is configured via the `auth` property in your app schema. You can specify your app's scope requirements, redirect URLs, and authentication type.
| Field | Type | Description |
| -------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type` | String | The type of authentication, which can be set to one of the following:
`oauth`: allow installation via OAuth, either to a specific set of allowlisted accounts or listing it in the HubSpot Marketplace.
`static`: restrict installation of your app to a single account that the installing user has access to.
|
| `redirectUrls` | Array | A list of URLs that the OAuth process is allowed to reroute back to. Each app must have at least one auth redirect URL, and it must use HTTPS. The only exception is that `http://localhost` is allowed for testing. |
| `requiredScopes` | Array | A list of your app's required scopes. Each app must include at least one scope, and the installing user must grant these scopes to successfully install the app. [Learn more about scopes below](#scopes). |
| `optionalScopes` | Array | A list of your app's optional scopes. These scopes can be excluded from the authorization during installation if the account or user installing the app doesn't have the proper permissions. In that case, the scope will not be included in the resulting refresh token or access token. [Learn more about scopes below](#scopes). |
| `conditionallyRequiredScopes` | Array | A list of scopes that are required only when included in the `scope` query parameter of the install URL. [Learn more about scopes below](#scopes). |
### Scopes
In the `auth` field of an app configuration file, three [types of scopes](/docs/apps/developer-platform/build-apps/authentication/scopes#app-scope-types) are available: required scopes, conditionally required scopes, and optional scopes.
Apps configured with [static auth](/docs/apps/developer-platform/build-apps/authentication/overview#static-auth) can only define required scopes. If your app uses [OAuth authentication](/docs/apps/developer-platform/build-apps/authentication/overview#oauth), you can also specify conditionally required scopes and optional scopes, which provide more flexibility and control for the permissions that users grant to your app.
At a minimum, your app must include the `read` scope to enable customers to access the associated CRM object or asset type (e.g., `crm.objects.contacts.read` to retrieve contacts).
```json highlight={12-14} theme={null}
{
"uid": "oauth-sample-app",
"type": "app",
"config": {
"description": "An example OAuth app.",
"name": "my first app",
"logo": "/app/app-logo.png",
"distribution": "marketplace",
"auth": {
"type": "oauth",
"redirectUrls": ["http://localhost:3000/oauth-callback"],
"requiredScopes": ["crm.objects.contacts.read", "crm.objects.contacts.write"],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
"permittedUrls": {
"fetch": ["https://api.hubapi.com"],
"iframe": [],
"img": []
},
"support": {
"supportEmail": "support@example.com",
"documentationUrl": "https://example.com/docs",
"supportUrl": "https://example.com/support",
"supportPhone": "+18005555555"
}
}
}
```
For a full list of available scopes, see the [scopes reference](/docs/apps/developer-platform/build-apps/authentication/scopes).
## Adding app features
To configure app features such as webhook subscriptions, custom workflow actions, and app cards, check out the guides below for details on how to add the associated `*-hsmeta.json` files to your project:
* [Create an app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card)
* [Create serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/overview)
* [Define app events](/docs/apps/developer-platform/add-features/app-events/overview)
* [Create app objects](/docs/apps/developer-platform/add-features/app-objects/quickstart-guide-to-app-objects)
* [Create a settings component](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page)
* [Set up a custom workflow action](/docs/apps/developer-platform/add-features/custom-workflow-actions)
* [Configure a webhook subscription](/docs/apps/developer-platform/add-features/configure-webhooks)
* [Add telemetry](/docs/apps/developer-platform/add-features/add-telemetry)
# Make API requests using a service key (BETA)
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/account-service-keys
Learn about how to use a service key to streamline access to data in your HubSpot account.
If you need to set up a simple third-party integration with HubSpot or try out specific HubSpot API endpoints, you can use service keys to query HubSpot's [REST APIs](/docs/api-reference/latest/overview) directly without having to use the CLI or other platform developer tools first.
Service keys are still configured with object-specific scopes (e.g., `crm.objects.contacts.read`) so you can still limit access for each key, and keys can be managed or rotated to keep your account data secure.
This functionality is currently in public beta. By participating in this beta, you agree to HubSpot's [Developer Terms](https://legal.hubspot.com/hs-developer-terms) and [Developer Beta Terms](https://legal.hubspot.com/hubspot-beta-terms). Note that the functionality is still under active development and is subject to change based on testing and feedback.
## Before you get started
The following users in your account have access to create and manage service keys:
* [Super admins](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin)
* Users with the Developer tools access [permission](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#settings-access) in your account settings.
You cannot use service keys to authenticate webhooks, make calls within a UI extension, or leverage other developer platform functionality other than making REST API requests. If you want to leverage these features, you should [create an app](/docs/apps/developer-platform/build-apps/create-an-app) and use its static access token or OAuth access token to make API requests instead.
## Limits
Service keys are subject to the [same limits](/docs/developer-tooling/platform/usage-guidelines#privately-distributed-app-limits) as [privately distributed apps](/docs/apps/developer-platform/build-apps/app-configuration#distribution) built on version `2025.2` and `2026.03` of the developer platform.
## Create a service key
To create a new service key:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Keys** > **Service keys**.
* In the top right, click **Create service key**.
* Enter a **name** for the service key.
* Click **Add new scope**.
* In the right panel, select the **checkbox** for each scope you want your key to be able to access.
* You can also search for a specific scope using the *Find a scope* search bar. You can review a full list of available scopes in [this reference article](/docs/apps/developer-platform/build-apps/authentication/scopes).
* Click **Update** when you're done adding scopes. If you later decide that you require additional scopes, you can also configure them after your key is created.
* Review the scopes you've selected. If you decide your key does not require a specific scope, you can click **Delete** next to that scope to remove it. You can also click **Summary of selected scopes** to view a breakdown of your key's scopes and the associated access granted for each one.
* When you're ready, click **Create** in the top right, then confirm your choice in the dialog box.
## Make API requests with your service key
Once created, your service key can be used immediately to make requests for data in your account:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Keys** > **Service keys**.
* Click the **name** of your service key.
* By default, your service key will be partially hidden for security purposes. Under the key, click **Show** to toggle its visibility, then click **Copy** to copy the key to your clipboard.
* You can then include your service key as a *Bearer* token in your preferred API client or programming language. The code block below demonstrates how to include the key in a `cURL` request in the CLI to [retrieve contacts](/docs/api-reference/latest/crm/objects/contacts/get-contacts).
```shell theme={null}
curl --request GET \
--header "Authorization: Bearer pat-na1-*********-****-****-****-************" \
--url "https://api.hubapi.com/crm/v3/objects/contacts?limit=10&archived=false"
```
## Manage service keys
The details page for your service key provides a centralized space for managing your key.
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Keys** > **Service keys**.
* Click the **name** of your service key, then review and manage your key.
* To change the name or the scopes for your key, click **Edit** in the top right.
* To monitor or review recent requests, click **View logs** in the top right.
If your key is lost or otherwise compromised, you can rotate it. A new service key will be created and the original one will expire.
* Next to your service key, click **Rotate**:
* If your key is compromised and you want to immediately revoke access, click **Rotate and expire now**.
* If there's no imminent threat to your key, it's still recommended that you rotate it every six months. If you're ready to initiate a regular rotation of your key, click **Rotate and expire later**, which will trigger an expiration of the key in 7 days.
* If your key is ready to transition earlier, you can click **Expire now**.
* If you decide you need more time, you can click **Cancel rotation**, which will cancel the expiration of the original key and revoke the new service key.
To delete the service key:
* At the bottom of the page, click **Delete**.
* In the dialog box, type the name of your key to confirm its deletion, then click **Delete**.
# OAuth Quickstart Guide
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/oauth/oauth-quickstart-guide
Learn how to set up OAuth for your app using this quickstart Guide and sample Node.js app.
## Before you get started
Before you can start using OAuth with HubSpot, you'll need to have:
* [An app](/docs/apps/developer-platform/overview). Consult the [app creation guide](/docs/apps/developer-platform/build-apps/create-an-app) to create and customize a new app, or you can get up and running with a boilerplate app using the [quickstart guide](/docs/getting-started/quickstart).
* A HubSpot account to install your app in (you can use an existing account or [create a test account](/docs/getting-started/account-types))
You must be a [Super Admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) to install an app in a HubSpot account.
### How it works
HubSpot supports the [OAuth 2.0 Authorization Code grant type](https://developer.okta.com/blog/2018/04/10/oauth-authorization-code-grant-type), which can be broken down into four basic steps:
1. Your app opens a browser window to send the user to the HubSpot OAuth 2.0 server
2. The user reviews the requested permissions and grants the app access
3. The user is redirected back to the app with an authorization code in the query string
4. The app sends a request to the OAuth 2.0 server to exchange the authorization code for an access token
### In this guide
* [Quickstart App](#quickstart-app): A Node.js demo app that authenticates with HubSpot's OAuth 2.0 server
* [Getting OAuth tokens](#getting-oauth-tokens): How to authorize your app with users
* [Using OAuth tokens](#using-oauth-tokens): How to make queries with a token
* [Refreshing OAuth tokens](#refreshing-oauth-tokens): How to use the refresh token provided by HubSpot
All code examples in this guide are written in JavaScript (Node.js)
## Quickstart app
If this is your first time using OAuth authentication with HubSpot's APIs, it's strongly recommended that you check out the [OAuth 2.0 Quickstart App](https://github.com/HubSpot/oauth-quickstart-nodejs), written in Node.js. This sample app is designed to get you started using OAuth 2.0 as quickly as possible by demonstrating all the steps outlined below in the [getting OAuth tokens section](#getting-oauth-tokens) below.
## Getting OAuth tokens
### 1. Create the authorization URL and direct the user to HubSpot's OAuth 2.0 server
When sending a user to HubSpot's OAuth 2.0 server, the first step is creating the authorization URL. This will identify your app and define the resources (scopes) it's requesting access to on behalf of the user. The query parameters you can pass as part of an authorization URL are shown in the table below. For more detailed information on this step, read the [reference doc](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth).
Fields marked with \* are required.
| Parameter | Description | Example |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `client_id`\* | The client ID identifies your app. Find it on your app's settings page. | `7fff1e36-2d40-4ae1-bbb1-5266d59564fb` |
| `scope`\* | The [scopes](/docs/apps/developer-platform/build-apps/authentication/scopes#list-of-available-scopes) your application is requesting, separated by URL-encoded spaces (`%20`). | `oauth%20crm.objects.contacts.read` |
| `redirect_uri`\* | The URL that the user will be redirected to after they authorize your app for the requested scopes. **For production applications, `https` is required.** | `https://www.example.com/auth-callback` |
| `optional_scope` | The scopes that are optional for your app, and will be dropped if the selected HubSpot portal does not have access to those products | `automation` |
| `state` | A unique string value that can be used to maintain the user's state when they're redirected back to your app. | `WeHH_yy2irpl8UYAvv-my` |
Once you've created your URL, start the OAuth connection process by sending the user to it.
The code blocks below provide examples of using different redirect types:
**Using a server-side redirect:**
```js theme={null}
// Build the auth URL
const authUrl =
"https://app.hubspot.com/oauth/authorize" +
`?client_id=${encodeURIComponent(CLIENT_ID)}` +
`&scope=${encodeURIComponent(SCOPES)}` +
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
`&state=${encodeURIComponent(STATE)}`;
// Redirect the user
return res.redirect(authUrl);
```
**Using an HTML link:**
```html theme={null}
Install
```
**Encoding an additional redirect user state:**
Some apps may need to redirect the user to different locations. For example, an app may wish to redirect users to different subdomains of their integration (e.g. `userA.integration.com` and `userB.integration.com`). To do so, use the `state` parameter to encode more information about the user state:
1\. Generate and store a nonce value for the state parameter.
2\. Store the user's state in a local datastore using the nonce as its key.
3\. Include the nonce value as the state parameter in the authorization URL.
4\. When the user authenticates and is redirected to your redirect URL, validate the state parameter and use it as the key to retrieve the user state that was stored.
5\. From there, redirect the user as needed (e.g. redirecting again to a user specific URL).
### 2. HubSpot prompts user for consent
HubSpot displays a consent window to the user showing the name of your app and a short description of the HubSpot API services it's requesting permission to access. The user can then grant access to your app.
**Note:** The user installing the app must have access to all requested scopes. If they don't have the required access, the installation will fail and they will be directed to an error page. If a user sees this permissions error page, they'll need to have a Super Admin install the app.
Your application doesn't do anything at this stage. Once access is granted, the HubSpot OAuth 2.0 server will send a request to the callback URI defined in the authorization URL.
### 3. Handle the OAuth server response
When the user has completed the consent prompt from Step 2, the OAuth 2.0 server sends a `GET` request to the redirect URI specified in your authentication URL. If there are no issues and the user approves the access request, the request to the redirect URI will be returned with a `code` query parameter attached. If the user doesn't grant access, no request will be sent.
**Example:**
```js theme={null}
app.get("/oauth-callback", async (req, res) => {
if (req.query.code) {
// Handle the received code
}
});
```
### 4. Exchange authorization code for tokens
After your app receives an authorization code from the OAuth 2.0 server, it can exchange that code for an access and refresh token by sending a URL-form encoded `POST` request to `https://api.hubapi.com/oauth/v3/token` with the values shown below. For more detailed information on this step, consult the [API guide](/docs/api-reference/legacy/authentication/manage-oauth-tokens).
| Parameter | Description | Example |
| --------------- | --------------------------------------------------------- | --------------------------------------- |
| `grant_type` | Must be `authorization_code` | `authorization_code` |
| `client_id` | Your app's client ID | `7fff1e36-2d40-4ae1-bbb1-5266d59564fb` |
| `client_secret` | Your app's client secret | `7c3ce02c-0f0c-4c9f-9700-92440c9bdf2d` |
| `redirect_uri` | The redirect URI from when the user authorized your app | `https://www.example.com/auth-callback` |
| `code` | The authorization code received from the OAuth 2.0 server | `5771f587-2fe7-40e8-8784-042fb4bc2c31` |
**Example:**
```js theme={null}
const formData = {
grant_type: 'authorization_code',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
code: req.query.code
};
request.post('https://api.hubapi.com/oauth/v3/token', { form: formData }, (err, data) => {
// Handle the returned tokens (e.g., save tokens in a backend database)
});
```
The body of the token response will include the following properties:
```json theme={null}
{
"token_type": "bearer",
"refresh_token": "na1-aaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"hub_id": 1234567,
"scopes": [
"oauth",
"crm.objects.contacts.write",
"crm.objects.contacts.read"
],
"expires_in": 1800
}
```
**Please note:**
The access token will expire after the number of seconds given in the `expires_in` field of the response, currently 30 minutes. For details on getting a new access token, see the [refreshing OAuth tokens](#refreshing-oauth-tokens) section below.
## Using OAuth tokens
Once the authorization code flow is completed, your app is authorized to make requests on behalf of the user. To do this, provide the `access_token` that's returned from the `/oauth/v3/token` endpoint as a bearer token in the `Authorization` HTTP header. Specific details can be found in the [reference doc](/docs/api-reference/legacy/authentication/manage-oauth-tokens).
**Example:**
```js theme={null}
request.get(
"https://api.hubapi.com/crm/v3/objects/contacts",
{
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
},
(err, data) => {
// Handle the API response
}
);
```
**Please note:**
Access tokens reflect the scopes requested from the app and do not reflect the permissions or limitations of what a user can do in their HubSpot account. For example, if a user has permissions to view only owned contacts but authorizes a request for the `crm.objects.contacts.read` scope, the resulting access token can view all contacts in the account and not only those owned by the authorizing user.
## Refreshing OAuth tokens
OAuth access tokens expire periodically. This is to make sure that if they're compromised, attackers will only have access for a short time. The token's lifespan in seconds is specified in the `expires_in` field when an authorization code is exchanged for an access token.
Your app can exchange the received refresh token for a new access token by sending a URL-form encoded `POST` request to `https://api.hubapi.com/oauth/v3/token` and provide the values in the table below within the body of your request. For more detailed information on this step, check out the [API guide](/docs/api-reference/legacy/authentication/manage-oauth-tokens#refresh-an-access-token).
| Parameter | Description | Example |
| --------------- | ------------------------------------------------------------ | --------------------------------------- |
| `grant_type` | Must be `refresh_token` | `refresh_token` |
| `client_id` | Your app's client ID | `7fff1e36-2d40-4ae1-bbb1-5266d59564fb` |
| `client_secret` | Your app's client secret | `7c3ce02c-0f0c-4c9f-9700-92440c9bdf2d` |
| `redirect_uri` | The redirect URI from when the user authorized your app | `https://www.example.com/auth-callback` |
| `refresh_token` | The refresh token received when the user authorized your app | `b9443019-30fe-4df1-a67e-3d75cbd0f726` |
**Example:**
```js theme={null}
const formData = {
grant_type: 'refresh_token',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
refresh_token: REFRESH_TOKEN
};
request.post('https://api.hubapi.com/oauth/v3/token', { form: formData }, (err, data) => {
// Handle the returned tokens
});
```
The body of the token response will resemble the following:
```json theme={null}
{
"token_type": "bearer",
"refresh_token": "na1-aaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"hub_id": 1234567,
"scopes": [
"oauth",
"crm.objects.contacts.write",
"crm.objects.contacts.read"
],
"expires_in": 1800
}
```
The new access token can then be used to make calls on behalf of the user. When the new token expires, you can follow the same steps again to retrieve a new one.
## Related articles
* [Working with OAuth](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth)
* [OAuth v3 API guide](/docs/api-reference/legacy/authentication/manage-oauth-tokens)
# Working with OAuth
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth
OAuth is a secure means of authentication for your app. It uses authorization tokens rather than a password to connect your app to a user account.
OAuth is a secure means of authentication that uses authorization tokens rather than a password to connect your app to a user account. Initiating OAuth access is the first step towards allowing users to install your app in their HubSpot accounts.
**Please note:**
* Any app designed for installation by multiple HubSpot accounts or listing on the HubSpot Marketplace must use OAuth.
* Users installing apps in their HubSpot account must either be a [Super Admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) or have [HubSpot Marketplace Access](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#settings) permissions.
## Recommended resources
The [OAuth Quickstart Guide](/docs/apps/developer-platform/build-apps/authentication/oauth/oauth-quickstart-guide) will get you up and running with a working example app.
You can also check out the blog post linked below for a full walkthrough of how OAuth works and guidance on setting up a backend service for storing OAuth tokens:
## Set up OAuth authentication
To set up OAuth authentication for your app:
1. [Create a new app](/docs/apps/developer-platform/build-apps/create-an-app). After creating the app, you'll be able to find the app's client ID and client secret on the *Auth* page of your [app settings](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#manage-authentication-for-your-app).
* Use the client ID along with the [query parameters](#query-parameters) and [scopes](#scopes) outlined below in steps 2 and 3, to build your authorization URL.
* Your app's client secret will be used in step 4 below, after the user is redirected back to your app and you're ready to generate the initial access and refresh tokens.
2. Send users installing your app to the authorization URL, where they'll be presented with a screen that allows them to select their account and grant access to your integration. You can set the authorization URL to be for a specific HubSpot account by adding the account ID between `/oauth/` and `/authorize`, as shown below.
3. After granting access, they'll be redirected back to your application via a `redirect_uri`, which will have a `code` query parameter appended to it.
* **Example authorization URLs**
* **Any account:** `https://app.hubspot.com/oauth/authorize?client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&scope=contacts%20automation&redirect_uri=https://www.example.com/`
* **Specific account (ID 123456):** `https://app.hubspot.com/oauth/123456/authorize?client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&scope=contacts%20automation&redirect_uri=https://www.example.com/`
* **Example redirect URL:** `https://example.com/?code=xxxx`
* **Example error:** `https://www.example.com/?error=error_code&error_description=Human%20readable%20description%20of%20the%20error`
4. You'll then make an API request to `/oauth/v3/token` with a request body that includes the `code`, the `redirect_uri`, your app's `client_id` and `client_secret`, and a `grant_type` of `authorization_code` to get an [access\_token and refresh\_token](/docs/api-reference/legacy/authentication/manage-oauth-tokens#generate-initial-access-and-refresh-tokens) from HubSpot.
5. Use the `access_token` to authenticate any API calls made for that HubSpot account.
6. Once the `access_token` expires, use the `refresh_token` to generate a new `access_token`. Learn more about how to [refresh an access token](/docs/api-reference/legacy/authentication/manage-oauth-tokens#refresh-an-access-token).
**Please note:**
* Your app will not appear as a *Connected App* in a user's account unless you generate the refresh token and initial access token.
* Access tokens reflect the scopes requested from the app and do not reflect the permissions or limitations of what a user can do in their HubSpot account. For example, if a user has permissions to view only owned contacts but authorizes a request for the `crm.objects.contacts.read` scope, the resulting access token can view all contacts in the account and not only those owned by the authorizing user.
## Query parameters
The following query parameters are required when building an authorization URL for your app:
| **Parameter** | **Description** | **How to use** |
| -------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_id` | An ID that serves as a unique identifier for your app. | Get this from your app's Auth settings page (as described above). |
| `redirect_uri` | The URL visitors will be redirected to after granting access to your app. | You'll also designate this on your app's Auth settings page. **Note:** For security reasons, this URL must use `https` in production. (When testing using `localhost`, `http` can be used.) You also must use a domain, as IP addresses are not supported. |
| `scope` | A space-separated set of permissions that your app needs access to. | Any scopes that you've checked off in your app's *Auth* settings will be treated as required, and you'll need to include them in this parameter or the authorization page will display an error. Additionally, users will get an error if they try to install your app in an account that doesn't have access to an included scope. Consult the [scopes reference documentation](/docs/apps/developer-platform/build-apps/authentication/scopes#list-of-available-scopes) for more details about which endpoints can be accessed by specific scopes. |
The following parameters are optional:
| **Parameter** | **How to use** | **Description** |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `optional_scope` | A space-separated set of optional permissions for your app. | Optional scopes will be automatically dropped from the authorization request if the user selects a HubSpot account that doesn't have access to that tool (e.g., authorizing a ***Content Hub** Enterprise* scope in a HubSpot free account). If you're using optional scopes, you will need to check the access token or refresh token to see which ones were granted. Check out the [reference documentation on scopes](/docs/apps/developer-platform/build-apps/authentication/scopes) for more details. |
| `state` | If this parameter is included in the authorization URL, the value will be included in a state query parameter when the user is directed to the `redirect_uri`. | A string value that can be used to maintain the user's state when they're redirected back to your app. |
## Configure scopes
OAuth requires you to set scopes, or permissions, for your app. Each scope provides access to a set of HubSpot API endpoints and allows users to grant your app access to specific tools in their HubSpot account.
You can review your app's current scopes in the development settings in your HubSpot account:
1. In your HubSpot account, navigate to **Development**.
2. In the left sidebar menu, click **Projects**.
3. Click the **name** of the project configured with OAuth authentication.
4. Under *Project Components*, click the `UID` of your app.
5. Click the *Auth* tab, then review your app's required, conditionally required, and optional scopes in the *Scopes* section.
To update your app's scopes, edit the corresponding scopes within the `auth` property of your app's `app-hsmeta.json` [file](/docs/apps/developer-platform/build-apps/app-configuration#scopes) locally, then run the `hs project upload` [command](/docs/developer-tooling/local-development/hubspot-cli/project-commands#upload-to-hubspot) to upload the changes to your account.
```json highlight={12-14} theme={null}
{
"uid": "oauth-sample-app",
"type": "app",
"config": {
"description": "An example OAuth app.",
"name": "my first app",
"logo": "/app/app-logo.png",
"distribution": "marketplace",
"auth": {
"type": "oauth",
"redirectUrls": ["http://localhost:3000/oauth-callback"],
"requiredScopes": ["crm.objects.contacts.read", "crm.objects.contacts.write"],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
"permittedUrls": {
"fetch": ["https://api.hubapi.com"],
"iframe": [],
"img": []
},
"support": {
"supportEmail": "support@example.com",
"documentationUrl": "https://example.com/docs",
"supportUrl": "https://example.com/support",
"supportPhone": "+18005555555"
}
}
}
```
A full list of scopes is available [here](/docs/apps/developer-platform/build-apps/authentication/scopes).
## Monitor OAuth installation logs
After you've set up the OAuth authentication flow for your app, you can review installation attempts, successes, and failures in your development settings:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Monitoring** > **Logs**.
* Click the **dropdown menu** at the top of the page and select the **name** of your OAuth app.
* Click the **OAuth** tab.
* Review all installation events for your app. The following event types are logged:
* **AUTHORIZATION\_REQUEST:** logged when a user tries to load the app's authorization URL in their account. This event will be logged as an error if the app is misconfigured or the user doesn't have sufficient permissions to install the app.
* **AUTHORIZATION\_GRANT:** logged when a user clicks the *Connect app* button to start the installation process. This will result in HubSpot generating a `code` query parameter.
* **AUTHORIZATION\_CODE\_EXPIRY:** an error that occurs when the auth code expired before the token was exchanged.
* **TOKEN\_EXCHANGE:** your OAuth service successfully exchanged the `code` to get an access token and refresh token. At this point, the app is installed in the user's account.
* To view more details about a specific event, click the **ellipses** icon under the *Actions* column, then select an option:
* **Open details:** open a side panel with more information, including the *Account ID*, *Log ID*, *Trace ID*, and the full error messages
* **Open tracing:** navigate to an error details page, which also provides the sequence of related OAuth events triggered by the same account.
* You can use the top dropdown menus to search for specific events by *Account ID* or *Log ID*, or filter events by date range.
* To export OAuth event data, click **Export** in the top right. In the dialog box, confirm the date range, then click **Export**.
## Related articles
# Authentication overview
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/overview
Learn how to manage authentication for your apps and when developing locally.
While building apps on the developer platform, you can configure authentication based on how you plan to install your app. HubSpot also provides local authentication tooling via the HubSpot CLI.
## App authentication
There are two authentication types available based on how you plan to distribute your app: OAuth is required for multiple accounts, while static auth access tokens are used for installing in a single account at a time.
### OAuth
If you plan to distribute your app to multiple accounts (either through listing on the HubSpot Marketplace or by managing specific authorized accounts), your app must be built using OAuth authentication. You'll need to set up and host an OAuth backend service (e.g., hosted as a Docker instance using a cloud service provider) to initiate the OAuth process and manage token data for users installing your app in their account.
HubSpot provides a Node.js quickstart guide [here](/docs/apps/developer-platform/build-apps/authentication/oauth/oauth-quickstart-guide), which includes code you can run in a Docker instance with full OAuth support. Authentication configuration details for your app are available on the app details page in the [developer overview](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot) of your HubSpot account.
Once you've set up an OAuth backend, you can make API requests using the OAuth access token that corresponds to a user who installed your app. This access token is provided using the `Bearer` HTTP authorization header. For example, if you wanted to retrieve contacts for the account with an access token of `00000000-aaaa-xxx-yyyy-zzzzzzzzzzzz`, your request would resemble the following:
```shell theme={null}
curl --request GET \
--header "Authorization: Bearer 00000000-aaaa-xxx-yyyy-zzzzzzzzzzzz" \
--url "https://api.hubapi.com/crm/objects/2026-03/contacts?limit=10&archived=false"
```
Configure your app to use OAuth by setting the `type` subproperty within the `auth` field of your app's `app-hsmeta.json` config to `oauth`. You'll also need to set the `distribution` property to `marketplace` or `private` based on how you plan to distribute your app:
* `marketplace`: used if you want the app to be eligible for listing on the HubSpot Marketplace.
* `private`: used if you only want to install your app in a specific set of allowlisted accounts. If you choose this option, you can install your app in a maximum of 10 accounts at a time.
Learn more in the [app configuration guide](/docs/apps/developer-platform/build-apps/app-configuration).
### Static auth
If you want to limit distribution of your app to a single authorized account, you'll use a static auth access token. This token can be found in your [app settings](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot). An example request is provided below using a placeholder static auth access token.
```shell theme={null}
curl --request GET \
--header "Authorization: Bearer ***-***-*********-****-****-****-************" \
--url "https://api.hubapi.com/crm/objects/2026-03/contacts?limit=10&archived=false"
```
Configure your app to use static auth by setting the `type` subproperty (within the `auth` field) in your app's `app-hsmeta.json` config to `static`, and set the `distribution` field to `private`.
Learn more in the [app configuration guide](/docs/apps/developer-platform/build-apps/app-configuration).
## Developer API keys
Some app features and settings require a developer API key, which is available in the developer overview of your HubSpot account.
If a feature or endpoint requires a developer API key, it'll be documented in the associated guide or reference article. You'll provide the key value in the `hapikey` query parameter, usually with the `appId` query parameter which corresponds to the app you're making changes to. For example, the `cURL` snippet below provides an example of using the [custom channel registration](/docs/api-reference/latest/conversations/guide) endpoint:
```shell theme={null}
curl --request POST \
--url "https://api.hubapi.com/conversations/custom-channels/2026-03?hapikey={YOUR_DEVELOPER_API_KEY}&appId={appId}"
```
## Client credentials
Similar to HubSpot developer API keys, HubSpot uses client credential tokens to take an action on behalf of your app. These tokens are OAuth 2.0 tokens with short-term expiry windows that must be refreshed after a certain amount of time.
Unlike the OAuth tokens used for app authentication, client credential tokens aren't used to act on behalf of users who install your app. Instead, they're used to manage your app's global configuration for specific features.
Currently, the only feature using client credential tokens is the [webhooks journal API](/docs/api-reference/latest/webhooks-journal/guide). For example, to generate a client credentials token with all available webhook journal and management permissions, you'd make the following API call:
```shell theme={null}
curl --location 'https://api.hubapi.com/oauth/2026-03/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' \
--data-urlencode 'client_secret=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' \
--data-urlencode 'scope=developer.webhooks_journal.read developer.webhooks_journal.subscriptions.read developer.webhooks_journal.subscriptions.write developer.webhooks_journal.snapshots.read developer.webhooks_journal.snapshots.write'
```
## Scopes
Based on the HubSpot data and functionality that your app will require access to, you'll provide a list of scopes in your app's `app-hsmeta.json` authentication config. For example, if your app required access to create contacts, you'd need to include the `crm.objects.contacts.write`.
Learn more about [scopes](/docs/apps/developer-platform/build-apps/authentication/scopes).
## Local authentication
While you develop your app locally using the HubSpot CLI, you can use the `hs accounts auth` command. If you're configuring local authentication for the first time, you can also use the `hs init` command. After authenticating, a [personal access key](/docs/developer-tooling/local-development/hubspot-cli/personal-access-key) will be associated with your account that will be used to authenticate CLI commands.
Learn more about [installing the HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). A full list of CLI commands is provided [here](/docs/developer-tooling/local-development/hubspot-cli/reference).
## Client secret rotation
A client secret is a confidential value specific to your app, which is used for managing [OAuth tokens](/docs/api-reference/latest/authentication/manage-oauth-tokens), validating [requests from HubSpot](/docs/apps/developer-platform/build-apps/authentication/request-validation), and generating a [client credentials token](#client-credentials).
If this secret is compromised (e.g., you mistakenly committed the secret to a git repository), you can rotate the secret to ensure the old one is invalidated and cannot be used.
Client secret rotation is supported for [legacy public apps](/docs/apps/legacy-apps/public-apps/overview), project-based apps on the [developer platform](/docs/apps/developer-platform/overview), and [MCP auth apps](/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server).
Learn how to rotate a [client secret](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#rotate-client-secret) in a project-based app.
# Validating Requests
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/request-validation
An overview on validating requests originating from HubSpot to your backend service.
To ensure that the requests that your integration is receiving from HubSpot are actually coming from HubSpot, several headers are populated in the request. You can use these headers, along with fields of the incoming request, to verify the signature of the request.
The method used to verify the signature depends on the version of the signature:
* To validate a request using the latest version of the HubSpot signature, use the `X-HubSpot-Signature-V3` header and follow the [associated instructions for validating the v3 version of the signature](#validate-the-v3-request-signature).
* For backwards compatibility, requests from HubSpot also include older versions of the signature. To validate an older version of the signature, check the `X-HubSpot-Signature-Version` header, then follow the associated instructions below based on whether the version is [v2](#validate-requests-using-the-v2-request-signature) or [v1](#validate-requests-using-the-v1-request-signature).
In the instructions below, learn how to derive a hash value from your app's client secret and the fields of an incoming request. Once you compute the hash value, compare it to the signature. If the two are equal, then the request has passed validation. Otherwise, the request may have been tampered with in transit or someone may be spoofing requests to your endpoint.
If you're [building an app](/docs/apps/developer-platform/build-apps/create-an-app) with OAuth authentication, check out the blog post below for guidance on how to validate requests from HubSpot:
## Validate the v3 request signature
The `X-HubSpot-Signature-v3` header will be an HMAC SHA-256 hash built using the client secret of your app combined with details of the request. It will also include a `X-HubSpot-Request-Timestamp` header.
When validating a request using the X-HubSpot-Signature-v3 header, you'll need to
* Reject the request if the timestamp is older than 5 minutes.
* In the request URI, decode any of the URL-encoded characters listed in the table below. You do not need to decode the question mark that denotes the beginning of the query string.
| **Encoded value** | **Decoded value** |
| ----------------- | ----------------- |
| `%3A` | `:` |
| `%2F` | `/` |
| `%3F` | `?` |
| `%40` | `@` |
| `%21` | `!` |
| `%24` | `$` |
| `%27` | `'` |
| `%28` | `(` |
| `%29` | `)` |
| `%2A` | `*` |
| `%2C` | `,` |
| `%3B` | `;` |
* Create a utf-8 encoded string that concatenates together the following: `requestMethod` + `requestUri` + `requestBody` + timestamp. The timestamp is provided by the `X-HubSpot-Request-Timestamp` header.
* Create an HMAC SHA-256 hash of the resulting string using the application secret as the secret for the HMAC SHA-256 function.
* Base64 encode the result of the HMAC function.
* Compare the hash value to the signature. If they're equal then this request has been verified as originating from HubSpot. It's recommended that you use constant-time string comparison to guard against timing attacks.
The code snippets in the section below detail how you could incorporate v3 request validation for a `POST` request if you were running a backend service to handle incoming requests.
Keep in mind that the code blocks below omit certain dependencies you might need to run a fully-featured backend service. Confirm that you're running the latest stable and secure libraries when implementing request validation for your specific service.
### v3 request signature examples
The code blocks in the tabs below provide examples of validating the v3 request signature using Node.js, Java, and PHP.
```js theme={null}
// Introduce any dependencies. Only several dependencies related to this example are included below:
require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const crypto = require("crypto");
const app = express();
const port = process.env.PORT || 4000;
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.post("/webhook-test", (request, response) => {
response.status(200).send("Received webhook subscription trigger");
const { url, method, body, headers, hostname } = request;
// Parse headers needed to validate signature
const signatureHeader = headers["x-hubspot-signature-v3"];
const timestampHeader = headers["x-hubspot-request-timestamp"];
// Validate timestamp
const MAX_ALLOWED_TIMESTAMP = 300000; // 5 minutes in milliseconds
const currentTime = Date.now();
if (currentTime - timestampHeader > MAX_ALLOWED_TIMESTAMP) {
console.log("Timestamp is invalid, reject request");
// Add any rejection logic here
}
// Concatenate request method, URI, body, and header timestamp
let uri = `https://${hostname}${url}`;
// Drop fragment
uri = uri.split('#')[0];
// Decode only the query string portion
const queryPos = uri.indexOf('?');
if (queryPos !== -1) {
const decodeMap = {
'%3A': ':', '%2F': '/', '%3F': '?', '%40': '@',
'%21': '!', '%24': '$', '%27': "'", '%28': '(',
'%29': ')', '%2A': '*', '%2C': ',', '%3B': ';',
};
const path = uri.slice(0, queryPos + 1);
const query = uri.slice(queryPos + 1).replace(
/%3A|%2F|%3F|%40|%21|%24|%27|%28|%29|%2A|%2C|%3B/g,
m => decodeMap[m]
);
uri = path + query;
}
const rawString = `${method}${uri}${JSON.stringify(body)}${timestampHeader}`;
// Create HMAC SHA-256 hash from resulting string above, then base64-encode it
const hashedString = crypto.createHmac("sha256", process.env.CLIENT_SECRET).update(rawString).digest("base64");
// Validate signature: compare computed signature vs. signature in header
if (crypto.timingSafeEqual(Buffer.from(hashedString), Buffer.from(signatureHeader))) {
console.log("Signature matches! Request is valid.");
// Proceed with any request processing as needed.
} else {
console.log("Signature does not match: request is invalid");
// Add any rejection logic here.
}
});
```
```java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class HubSpotV3SignatureValidator {
// 5 minutes in milliseconds
private static final long MAX_ALLOWED_TIMESTAMP = 300000;
public static boolean validateV3Signature(String clientSecret, String method,
String uri, String requestBody,
long timestamp, String receivedSignature) {
try {
// Validate timestamp (reject if older than 5 minutes)
long currentTime = System.currentTimeMillis();
if (currentTime - timestamp > MAX_ALLOWED_TIMESTAMP) {
System.out.println("Timestamp is invalid, rejecting request");
return false;
}
// Drop fragment
uri = uri.split("#")[0];
// Decode only the query string portion
int queryPos = uri.indexOf('?');
if (queryPos != -1) {
String path = uri.substring(0, queryPos + 1);
String query = uri.substring(queryPos + 1)
.replace("%3A", ":").replace("%2F", "/")
.replace("%3F", "?").replace("%40", "@")
.replace("%21", "!").replace("%24", "$")
.replace("%27", "'").replace("%28", "(")
.replace("%29", ")").replace("%2A", "*")
.replace("%2C", ",").replace("%3B", ";");
uri = path + query;
}
// Create concatenated string: method + uri + body + timestamp
String rawString = method + uri + requestBody + timestamp;
System.out.println("Raw string: " + rawString);
// Create HMAC SHA-256 hash
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmacSha256.init(secretKey);
byte[] hash = hmacSha256.doFinal(rawString.getBytes(StandardCharsets.UTF_8));
// Base64 encode the result
String expectedSignature = Base64.getEncoder().encodeToString(hash);
// Compare signatures using constant-time comparison
return MessageDigest.isEqual(expectedSignature.getBytes(), receivedSignature.getBytes());
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Error creating HMAC SHA-256 hash", e);
}
}
// Example usage
public static void main(String[] args) {
String clientSecret = "cfc68c0b-4b4e-4ef8-b764-95350e4ea479";
String method = "POST";
String uri = "https://webhook.site/335453f5-94b3-49d9-b684-a55354d4b8df";
String requestBody = "[{\"eventId\":531833541,\"subscriptionId\":3923621,\"portalId\":48807704,\"appId\":16111050,\"occurredAt\":1752613920733,\"subscriptionType\":\"contact.creation\",\"attemptNumber\":0,\"objectId\":138017612137,\"changeFlag\":\"CREATED\",\"changeSource\":\"CRM_UI\",\"sourceId\":\"userId:76023669\"}]";
long timestamp = 1752613922216L; // Example timestamp in milliseconds
// This would typically come from the X-HubSpot-Signature-v3 header
String signatureFromHeader = "gbj1XPRvUt0noT7i7fXfTzOD4sLzQmf0VT28ZYq0EYg=";
boolean isValid = validateV3Signature(clientSecret, method, uri, requestBody, timestamp, signatureFromHeader);
if(isValid) {
System.out.println("Signature is valid! Proceed with request processing.");
}
// Proceed with any request processing as needed.
else {
System.out.println("Signature is invalid! Reject the request.");
// Add any rejection logic here, e.g., throw 400 Bad Request
}
}
}
```
```php theme={null}
$signature = $_SERVER['HTTP_X_HUBSPOT_SIGNATURE_V3'] ?? null;
$timestamp = $_SERVER['HTTP_X_HUBSPOT_REQUEST_TIMESTAMP'] ?? null;
if (!$signature || !$timestamp) {
http_response_code(403);
exit('Missing signature or timestamp.');
}
// Validate timestamp (within 5 minutes)
$maxSkew = 300;
if (abs(time() - ((int)($timestamp / 1000))) > $maxSkew) {
http_response_code(403);
exit('Expired timestamp.');
}
$method = $_SERVER['REQUEST_METHOD'];
$domain = 'https://' . $_SERVER['HTTP_HOST'];
$uri = $_SERVER['REQUEST_URI'];
// Decode URL-encoded characters
$decodeMap = [
'%3A' => ':', '%2F' => '/', '%3F' => '?',
'%40' => '@', '%21' => '!', '%24' => '$',
'%27' => "'", '%28' => '(', '%29' => ')',
'%2A' => '*', '%2C' => ',', '%3B' => ';',
];
// Drop fragment
$uri = preg_replace('/#.*$/', '', $uri);
// Decode only the query string portion
$queryPos = strpos($uri, '?');
if ($queryPos !== false) {
$path = substr($uri, 0, $queryPos + 1);
$query = substr($uri, $queryPos + 1);
$query = strtr($query, $decodeMap);
$uri = $path . $query;
}
// Final URI string used in the signature
$fullUri = $domain . $uri;
$body = file_get_contents('php://input'); // Raw JSON string
$clientSecret = 'YOUR_CLIENT_SECRET'; // Replace with the client secret from your app
$rawString = $method . $fullUri . $body . $timestamp;
$expectedSignature = base64_encode(
hash_hmac('sha256', $rawString, $clientSecret, true)
);
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(403);
exit('Invalid signature.');
}
http_response_code(200);
echo 'Valid webhook';
```
## Validate requests using the v2 request signature
If your app is handling data from a [webhook action in a workflow](https://knowledge.hubspot.com/workflows/how-do-i-use-webhooks-with-hubspot-workflows), or if you're returning data for an [app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview), the request from HubSpot is sent with the `X-HubSpot-Signature-Version` header set to `v2`. The `X-HubSpot-Signature` header will be an SHA-256 hash built using the client secret of your app combined with details of the request.
To verify this signature, perform the following steps:
* Create a string that concatenates together the following: `Client secret` + `http method` + `URI` + `request body` (if present)
* Create a SHA-256 hash of the resulting string.
* Compare the hash value to the signature.
* If they're equal then this request has passed validation.
* If these values do not match, then this request may have been tampered with in-transit or someone may be spoofing requests to your endpoint.
**Please note:**
* The URI used to build the source string must exactly match the original request, including the protocol. If you're having trouble validating the signature, ensure that any query parameters are in the exact same order they were listed in the original request.
* The source string should be UTF-8 encoded before calculating the SHA-256 hash.
### Example for a GET request
For a `GET` request, you'd need your app's client secret and specific fields from the metadata of your request. These fields are listed below with placeholder values included:
* **Client secret:** `yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy`
* **HTTP method:** `GET`
* **URI:** `https://www.example.com/webhook_uri`
* **Request body: `""`**
The resulting concatenated string would be: `yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyGEThttps://www.example.com/webhook_uri`
After calculating a SHA-256 hash of the concatenated string above, the resulting signature you'd expect to match to the one in the header would be: `eee2dddcc73c94d699f5e395f4b9d454a069a6855fbfa152e91e88823087200e`
### Example for a POST request
For a `POST` request, you'd need your app's client secret, specific fields from the metadata of your request, and a string representation of the body of the request (e.g., using `JSON.stringify(request.body)` for a Node.js service). These fields are listed below with placeholder values included:
* **Client secret:** `yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy`
* **HTTP method:** `POST`
* **URI:** `https://www.example.com/webhook_uri`
* **Request body:** `{"example_field":"example_value"}`
The resulting concatenated string would be: `yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyPOSThttps://www.example.com/webhook_uri{"example_field":"example_value"}`
After calculating a SHA-256 hash of the concatenated string above, the resulting signature you'd expect to match to the one in the header would be:`9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900`
After \[SHA-ing] the signature, you could then compare the resulting expected signature to the one provided in the x-hubspot-signature header of the request:
The code snippets below details how you could incorporate `v2` request validation for a `GET` request if you were running a backend service to handle incoming requests. Keep in mind that the code block below is an example and omits certain dependencies you might need to run a fully-featured backend service. Confirm that you're running the latest stable and secure libraries when implementing request validation for your specific service.
### v2 request signature examples
The code blocks in the tabs below provide examples of validating the v2 request signature using Node.js or Java.
```js theme={null}
// Introduce any dependencies. Only several dependencies related to this example are included below:
const express = require("express");
const bodyParser = require("body-parser");
const crypto = require("crypto");
const app = express();
// Add any custom handling or setup code for your Node.js service here.
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// Example Node.js request validation code.
app.get("/example-service", (request, response, next) => {
const { url, method, headers, hostname } = request;
const requestSignature = headers["x-hubspot-signature"];
// Compute expected signature
const uri = `https://${hostname}${url}`;
const encodedString = Buffer.from(`${process.env.CLIENT_SECRET}${method}${uri}`, "ascii").toString("utf-8");
const expectedSignature = crypto.createHash("sha256").update(encodedString).digest("hex");
console.log("Expected signature: %s", requestSignature);
console.log("Request signature: %s", expectedSignature);
// Add your custom handling to compare request signature to expected signature
if (requestSignature !== expectedSignature) {
console.log("Request of signature does NOT match!");
response.status(400).send("Bad request");
} else {
console.log("Request of signature matches!");
response.status(200).send();
}
});
```
```java theme={null}
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
public class HubSpotV2SignatureValidator {
public static boolean validateV2Signature(String clientSecret, String method,
String uri,
String receivedSignature) {
try {
// Create concatenated string: client_secret + method + uri + body
String sourceString = clientSecret + method + uri;
System.out.println("Source string: " + sourceString);
// Create SHA-256 hash
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(sourceString.getBytes(StandardCharsets.UTF_8));
// Convert to hex string (Java 17+)
String expectedSignature = java.util.HexFormat.of().formatHex(hash);
// Compare signatures using constant-time comparison
return MessageDigest.isEqual(expectedSignature.getBytes(), receivedSignature.getBytes());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not available", e);
}
}
// Example usage
public static void main(String[] args) {
String clientSecret = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
String method = "GET";
String uri = "https://www.example.com/webhook_uri";
// Expected signature: 9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900
String expectedSignature = "9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900";
boolean isValid = validateV2Signature(clientSecret, method, uri, expectedSignature);
if (isValid) {
System.out.println("Signature is valid!");
}
// Proceed with any request processing as needed.
else {
System.out.println("Signature is invalid!");
// Add any rejection logic here. e.g, throw 400
}
}
}
```
## Validate requests using the v1 request signature
Some requests from HubSpot's legacy APIs or older webhook formats will be sent with the `X-HubSpot-Signature-Version` header set to `v1`. The `X-HubSpot-Signature` header will be an SHA-256 hash built using the client secret of your app combined with details of the request.
To verify this version of the signature, perform the following steps:
* Create a string that concatenates together the following: `Client secret` + `request body` (if present).
* Create a SHA-256 hash of the resulting string.
* Compare the hash value to the value of the `X-HubSpot-Signature` header:
* If they're equal then this request has passed validation.
* If these values do not match, then this request may have been tampered with in-transit or someone may be spoofing requests to your endpoint.
**Example for a request with a body:**
```json theme={null}
// Client secret : yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
// Request body:
[
{
"eventId": 1,
"subscriptionId": 12345,
"portalId": 62515,
"occurredAt": 1564113600000,
"subscriptionType": "contact.creation",
"attemptNumber": 0,
"objectId": 123,
"changeSource": "CRM",
"changeFlag": "NEW",
"appId": 54321
}
]
```
### v1 request signature examples
The code blocks in the tabs below provide examples of validating the v2 request signature using Node.js, Java, Python, and Ruby.
```js theme={null}
NOTE: This is only an example for generating the expected hash.
You will need to compare this expected hash with the actual hash in the
X-HubSpot-Signature header.
> const crypto = require('crypto')
undefined
> client_secret = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
> request_body = '[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
'[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
> source_string = client_secret + request_body
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
> hash = crypto.createHash('sha256').update(source_string).digest('hex')
'232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de'
```
```java theme={null}
// NOTE: This is only an example for generating the expected hash.
// You will need to compare this expected hash with the actual hash in the
// X-HubSpot-Signature header.
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.io.UnsupportedEncodingException;
public class HubSpotSignatureValidator {
public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException {
String clientSecret = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
String requestBody = "[{\"eventId\":1,\"subscriptionId\":12345,\"portalId\":62515,\"occurredAt\":1564113600000,\"subscriptionType\":\"contact.creation\",\"attemptNumber\":0,\"objectId\":123,\"changeSource\":\"CRM\",\"changeFlag\":\"NEW\",\"appId\":54321}]";
String sourceString = clientSecret + requestBody;
System.out.println("Source string: " + sourceString);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(sourceString.getBytes("UTF-8"));
// Convert to hex string (Java 17+)
String hexString = java.util.HexFormat.of().formatHex(hash);
System.out.println("Hash: " + hexString);
// Output: 232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de
}
}
```
```py theme={null}
NOTE: This is only an example for generating the expected hash.
You will need to compare this expected hash with the actual hash in the
X-HubSpot-Signature header.
>>> import hashlib
>>> client_secret = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
>>> request_body = '[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
>>> source_string = client_secret + request_body
>>> source_string
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
>>> hashlib.sha256(source_string).hexdigest()
'232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de'
```
```ruby theme={null}
NOTE: This is only an example for generating the expected hash.
You will need to compare this expected hash with the actual hash in the
X-HubSpot-Signature header.
irb(main):003:0> require 'digest'
=> true
irb(main):004:0> client_secret = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
=> "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
irb(main):005:0> request_body = '[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
=> "[{\"eventId\":1,\"subscriptionId\":12345,\"portalId\":62515,\"occurredAt\":1564113600000,\"subscriptionType\":\"contact.creation\",\"attemptNumber\":0,\"objectId\":123,\"changeSource\":\"CRM\",\"changeFlag\":\"NEW\",\"appId\":54321}]"
irb(main):006:0> source_string = client_secret + request_body
=> "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy[{\"eventId\":1,\"subscriptionId\":12345,\"portalId\":62515,\"occurredAt\":1564113600000,\"subscriptionType\":\"contact.creation\",\"attemptNumber\":0,\"objectId\":123,\"changeSource\":\"CRM\",\"changeFlag\":\"NEW\",\"appId\":54321}]"
irb(main):007:0> Digest::SHA256.hexdigest source_string
=> "232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de"
```
For each of the examples above, the resulting hash would be: `232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de`
# Scopes
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/scopes
Learn about the different scopes available for apps in HubSpot.
Scopes provide access to a specific set of HubSpot API endpoints and the associated data from a HubSpot account.
## Find required scopes for an endpoint
Any scopes required to make a request to a specific endpoint will be listed under the *Required Scopes* section in a reference article or the expandable *Scope requirements* section of the corresponding API guide. These API resources can be accessed by navigating to the [APIs section](/docs/api-reference/latest/overview) of HubSpot's developer documentation, then drilling down into the specific API you need (e.g., the [contacts API guide](/docs/api-reference/latest/crm/objects/contacts/guide) or the [retrieve contacts reference page](/docs/api-reference/latest/crm/objects/contacts/get-contact)).
## App scope types
When you specify the scopes within the `auth` property of your `app-hsmeta.json` [configuration file](/docs/apps/developer-platform/build-apps/app-configuration#authentication), there are three different scope types available for you to configure. You must specify the scopes your app will require for installation, but you can also specify two other scope types: conditionally required scopes and optional scopes.
* **Required scopes:** scopes that must be authorized by the user and must be present in the `scope` query parameter in your app's install URL for successful installation. Apps configured with [static auth](/docs/apps/developer-platform/build-apps/authentication/overview#static-auth) can only define required scopes.
If your app uses [OAuth authentication](/docs/apps/developer-platform/build-apps/authentication/overview#oauth), you can also specify conditionally required scopes and optional scopes, which provide more flexibility and control for the permissions that users grant to your app.
* **Conditionally required scopes:** scopes that must be authorized by the user only if they're present in the `scope` query parameter in your app's install URL for successful installation.
* This scope type allows you to be flexible and provide a separate install URL for tiered features or scopes that are only required when users enable certain features in your app. For example, you could offer two install URLs to your users: one install URL could include the conditionally required scope in the `scope` query parameter for users with access to a feature, while another install URL omits that scope in the `scope` query parameter for users without access.
* If a conditionally required scope is present in your app install URL and a user without access to the associated feature attempts to install your app using that URL, the installation will fail.
* **Optional scopes:** scopes that are not required to successfully install your app. These scopes are included in the `optional_scope` query parameter in your app's install URL. For example, if you want your app to be able to fetch [custom object](/docs/api-reference/latest/crm/objects/custom-objects/guide) data (which is only available to *Enterprise* HubSpot accounts), you could add the `crm.objects.custom.read` scope as an optional scope. Then, when a user attempts to install your app, they'll be prompted with the optional scopes you specified, and can customize and confirm each optional permission they grant to your app.
## List of available scopes
Access to specific APIs or endpoints depends on HubSpot account tier. You can find a full list of available scopes and accessible endpoints in the table below.
| **Scope** | **Description** |
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `account-info.security.read` | Includes access to [account activity logs](/docs/api-reference/latest/account/audit-logs/guide) and other account security information.
Available to all accounts. |
| `analytics.behavioral_events.send` | Includes access to send [custom event occurrences](/docs/api-reference/latest/events/send-event-data/guide).
Available to *Professional* or *Enterprise* accounts only. |
| `automation` | Grants access to create and retrieve [custom workflow actions](/docs/api-reference/latest/automation/workflow-actions/custom-action-builder), and usage of the [v4 workflow APIs](/docs/api-reference/legacy/automation/workflows/guide).
Available to *Professional* or *Enterprise* accounts only. |
| `automation.sequences.enrollments.write` | Enroll contacts in a [sequence](/docs/api-reference/latest/automation/sequences/guide#enroll-a-contact-in-a-sequence).
Available to ***Sales Hub*** or ***Service Hub*** *Professional* or *Enterprise* accounts only. |
| `automation.sequences.read` | View details about [sequences](/docs/api-reference/latest/automation/sequences/guide).
Available to ***Sales Hub*** or ***Service Hub*** *Professional* or *Enterprise* accounts only. |
| `behavioral_events.event_definitions.read_write` | Create, read, update, or delete [custom events](/docs/api-reference/latest/events/define-events/guide). This includes behavioral event properties.
***Marketing Hub*** *Enterprise* accounts only. |
| `business_units_view.read` | View [brand data](/docs/api-reference/latest/account/brands/guide), including logo information. Note that the [brands functionality](https://knowledge.hubspot.com/branding/manage-your-brands-with-hubspot-brands) is the successor to business units.
Available to accounts with the [Brands Add-on](https://www.hubspot.com/brands) only. |
| `business-intelligence` | Grants access to the [legacy v2 reporting endpoints](/docs/api-reference/legacy/deprecated-apis).
Available to all accounts. |
| `cms.domains.read` | List [connected domains](/docs/api-reference/latest/cms/domains/guide) in an account.
Available to all accounts. |
| `cms.domains.write` | Create, update, and delete [connected domains](/docs/api-reference/latest/cms/domains/guide).
Available to all accounts. |
| `cms.functions.read` | View all [Content Hub serverless functions](/docs/cms/start-building/features/serverless-functions/overview), any related secrets, and function execution results.
Available to ***Content Hub** Enterprise* accounts only. |
| `cms.functions.write` | Grants access to write [Content Hub serverless functions](/docs/cms/start-building/features/serverless-functions/overview) and secrets.
Available to ***Content Hub** Enterprise* accounts only. |
| `cms.knowledge_base.articles.read` | View details about knowledge articles using the [GraphQL API](/docs/cms/start-building/features/data-driven-content/graphql/query-hubspot-data-using-graphql#use-a-graphql-query-in-an-api-request).
Available to ***Service Hub*** *Professional* or *Enterprise* accounts only. |
| `cms.membership.access_groups.read` | View [membership access groups](/docs/cms/start-building/features/memberships/overview) and their definitions.
Available to ***Service Hub*** or ***Content Hub** Professional* or *Enterprise* accounts only. |
| `cms.membership.access_groups.write` | Create, edit, and delete [membership access groups](/docs/cms/start-building/features/memberships/overview).
Available to ***Service Hub*** or ***Content Hub** Professional* or *Enterprise* accounts only. |
| `collector.graphql_query.execute` | Query data from your HubSpot account using the [GraphQL API endpoint](/docs/cms/start-building/features/data-driven-content/graphql/query-hubspot-data-using-graphql#use-a-graphql-query-in-an-api-request)
Available to ***CMS Hub*** *Professional* or *Enterprise* accounts only. |
| `collector.graphql_schema.read` | Perform [introspection queries](/docs/cms/start-building/features/data-driven-content/graphql/query-hubspot-data-using-graphql#use-a-graphql-query-in-an-api-request) via GraphQL application clients such as [GraphiQL](/docs/cms/start-building/features/data-driven-content/graphql/query-hubspot-data-using-graphql#test-and-run-queries-interactively-using-graphiql).
Available to ***CMS Hub** Professional* or *Enterprise* accounts only. |
| `communication_preferences.read` | View details of your contacts' [subscription preferences](/docs/api-reference/legacy/communication-preferences/guide).
Available to all accounts. |
| `communication_preferences.read_write` | Provides access to subscribe or unsubscribe contacts to your [subscription types](/docs/api-reference/legacy/communication-preferences/guide), as well as retrieve subscription preferences for your contacts.
Available to all accounts. |
| `communication_preferences.statuses.batch.read` | Allows you to [batch retrieve contacts](/docs/api-reference/legacy/communication-preferences/guide#using-batch-subscription-endpoints) based on their subscription status.
Available to ***Marketing Hub*** *Enterprise* accounts only. |
| `communication_preferences.statuses.batch.write` | Allows you to [batch update](/docs/api-reference/legacy/communication-preferences/guide#using-batch-subscription-endpoints) the subscription status of multiple contacts.
Available to ***Marketing Hub*** *Enterprise* accounts only. |
| `communication_preferences.write` | [Subscribe or unsubscribe](/docs/api-reference/legacy/communication-preferences/guide#update-subscription-preferences-for-a-specific-contact) contacts to your subscription types.
Available to all accounts. |
| `content` | Grants access to content APIs, including [website pages](/docs/api-reference/latest/cms/pages/website-pages/get-website-pages), [landing pages](/docs/api-reference/latest/cms/pages/landing-pages/get-landing-pages), [marketing email](/docs/api-reference/latest/marketing/marketing-emails/guide), and [blog](/docs/api-reference/latest/cms/blogs/posts/guide) APIs.
Available to ***CMS Hub*** *Professional* or *Enterprise*, or ***Marketing Hub*** *Professional* or *Enterprise* accounts only. |
| `conversations.read` | View details about [actors](/docs/api-reference/latest/conversations/guide#get-actors), [messages](/docs/api-reference/latest/conversations/guide#retrieve-messages), and [threads](/docs/api-reference/latest/conversations/guide#retrieve-threads) in help desk and the conversations inbox.
Available to all accounts. |
| `conversations.visitor_identification.tokens.create` | Fetch [identification tokens](/docs/api-reference/legacy/conversations/visitor-identification/guide) for authenticated website visitors interacting with the HubSpot chat widget.
Available to *Professional* or *Enterprise* accounts only. |
| `conversations.write` | Create and manage [threads and messages](/docs/api-reference/latest/conversations/guide#threads-%26-messages) in the conversations inbox.
Available to all accounts. |
| `conversations.custom_channels.read` | View details about [custom channels](/docs/api-reference/latest/conversations/guide) for connected inboxes and help desk.
Available to ***Sales Hub*** or ***Service Hub*** *Enterprise* accounts only. |
| `conversations.custom_channels.write` | Manage [custom channels](/docs/api-reference/latest/conversations/guide) for connected inboxes and help desk.
Available to ***Sales Hub*** or ***Service Hub*** *Enterprise* accounts only. |
| `crm.export` | [Export records](/docs/api-reference/latest/crm/exports/guide) from your CRM for all CRM data types.
Available to all accounts. |
| `crm.import` | Allows you to [import records](/docs/api-reference/latest/crm/imports/guide#scope-requirements) into your CRM. This includes creating new records or modifying any of your existing records for all CRM data types (contacts, companies, deals, tickets, etc).
Available to all accounts. |
| `crm.dealsplits.read_write` | Create or retrieve [deal splits](/docs/api-reference/latest/crm/objects/deal-splits/guide) on a deal.
Available to ***Sales Hub*** *Enterprise* accounts only. |
| `crm.lists.read` | View details about contact [lists](/docs/api-reference/latest/crm/lists/guide#retrieve-lists).
Available to all accounts. |
| `crm.lists.write` | Create, delete, or make changes to contact [lists](/docs/api-reference/latest/crm/lists/guide#create-a-list).
Available to all accounts. |
| `crm.objects.appointments.read` | View properties and other details about [appointments](/docs/api-reference/latest/crm/objects/appointments/get-appointments).
Available to all accounts. |
| `crm.objects.appointments.sensitive.read` | View [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties for appointments.
Available to *Enterprise* accounts only. |
| `crm.objects.appointments.sensitive.write` | Edit [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties and values for appointments.
Available to *Enterprise* accounts only. |
| `crm.objects.appointments.write` | Create, delete, or make changes to [appointments](/docs/api-reference/latest/crm/objects/appointments/create-appointment).
Available to all accounts. |
| `crm.objects.carts.read` | View properties and other details about [carts](/docs/api-reference/latest/crm/objects/carts/guide#retrieve-carts).
Available to all accounts. |
| `crm.objects.carts.write` | Create, delete, or make changes to [carts](/docs/api-reference/latest/crm/objects/carts/guide#create-carts).
Available to all accounts. |
| `crm.objects.commercepayments.read` | View details about commerce [payments](/docs/api-reference/latest/crm/objects/commerce-payments/guide).
Available to *Starter* accounts only. |
| `crm.objects.companies.highly_sensitive.read` | View [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties for companies.
Available to *Enterprise* accounts only. |
| `crm.objects.companies.highly_sensitive.write` | Edit [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties and values for companies.
Available to *Enterprise* accounts only. |
| `crm.objects.companies.read` | View properties and other details about [companies](/docs/api-reference/latest/crm/objects/companies/guide#retrieve-companies).
Available to all accounts. |
| `crm.objects.companies.sensitive.read` | View [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties for companies.
Available to *Enterprise* accounts only. |
| `crm.objects.companies.sensitive.write` | Edit [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties and values for companies.
Available to *Enterprise* accounts only. |
| `crm.objects.companies.write` | View properties and create, delete, or make changes to [companies](/docs/api-reference/latest/crm/objects/companies/guide#create-companies).
Available to all accounts. |
| `crm.objects.contacts.highly_sensitive.read` | View [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties for contacts.
Available to *Enterprise* accounts only. |
| `crm.objects.contacts.highly_sensitive.write` | Edit [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties and values for contacts.
Available to *Enterprise* accounts only. |
| `crm.objects.contacts.read` | View properties and other details about [contacts](/docs/api-reference/latest/crm/objects/contacts/guide#retrieve-contacts).
Available to all accounts. |
| `crm.objects.contacts.sensitive.read` | View [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties for contacts.
Available to *Enterprise* accounts only. |
| `crm.objects.contacts.sensitive.write` | Edit [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties and values for contacts.
Available to *Enterprise* accounts only. |
| `crm.objects.contacts.write` | Create, delete, and make changes to [contacts](/docs/api-reference/latest/crm/objects/contacts/guide#create-contacts).
Available to all accounts. |
| `crm.objects.courses.read` | View details about [courses](/docs/api-reference/latest/crm/objects/courses/get-courses).
Available to all accounts. |
| `crm.objects.courses.write` | Create, delete, or make changes to [courses](/docs/api-reference/latest/crm/objects/courses/create-course).
Available to all accounts. |
| `crm.objects.custom.highly_sensitive.read` | View [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties for custom objects.
Available to *Enterprise* accounts only. |
| `crm.objects.custom.highly_sensitive.write` | Edit [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties and values for custom objects.
Available to *Enterprise* accounts only. |
| `crm.objects.custom.read` | View details about [custom objects](/docs/api-reference/latest/crm/objects/custom-objects/guide#retrieve-custom-object-records).
Available to *Enterprise* accounts only. |
| `crm.objects.custom.sensitive.read` | View [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties for custom objects.
Available to *Enterprise* accounts only. |
| `crm.objects.custom.sensitive.write` | Edit [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties and values for custom objects.
Available to *Enterprise* accounts only. |
| `crm.objects.custom.write` | Create, delete, or make changes to [custom objects](/docs/api-reference/latest/crm/objects/custom-objects/guide#create-a-custom-object).
Available to *Enterprise* accounts only. |
| `crm.objects.deals.highly_sensitive.read` | View [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties for deals.
Available to *Enterprise* accounts only. |
| `crm.objects.deals.highly_sensitive.write` | Edit [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties and values for deals.
Available to *Enterprise* accounts only. |
| `crm.objects.deals.read` | View properties and other details about [deals](/docs/api-reference/latest/crm/objects/deals/guide#retrieve-deals).
Available to all accounts. |
| `crm.objects.deals.sensitive.read` | View [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties for deals.
Available to *Enterprise* accounts only. |
| `crm.objects.deals.sensitive.write` | Edit [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties and values for deals.
Available to *Enterprise* accounts only. |
| `crm.objects.deals.write` | Create, delete, or make changes to [deals](/docs/api-reference/latest/crm/objects/deals/guide#create-deals).
Available to all accounts. |
| `crm.objects.feedback_submission.read` | View details about submissions to any of your [feedback surveys](/docs/api-reference/latest/crm/objects/feedback-submissions/guide#retrieve-survey-responses).
Available to ***Service Hub*** *Professional* or *Enterprise* accounts only. |
| `crm.objects.goals.read` | View all [goals](/docs/api-reference/latest/crm/objects/goals/guide#retrieve-goals).
Available to ***Sales Hub*** *Starter*, *Professional*, or *Enterprise* accounts only. |
| `crm.objects.invoices.read` | View details about [invoices](/docs/api-reference/latest/crm/objects/invoices/guide#retrieve-invoices).
Available to all accounts. |
| `crm.objects.leads.read` | View properties and other details about [leads](/docs/api-reference/latest/crm/objects/leads/guide#retrieve-leads).
Available to ***Sales Hub** Professional* or *Enterprise* accounts only. |
| `crm.objects.leads.write` | Create, delete, or make changes to [leads](/docs/api-reference/latest/crm/objects/leads/guide#create-leads).
Available to ***Sales Hub*** *Professional* or *Enterprise* accounts only. |
| `crm.objects.line_items.read` | View properties and other details about [line items](/docs/api-reference/latest/crm/objects/line-items/guide#retrieve-a-line-item).
Available to all accounts. |
| `crm.objects.line_items.write` | Create, delete, or make changes to [line items](/docs/api-reference/latest/crm/objects/line-items/guide#create-a-line-item).
Available to all accounts. |
| `crm.objects.listings.read` | View properties and other details about [listings](/docs/api-reference/latest/crm/objects/listings/get-listings).
Available to all accounts. |
| `crm.objects.listings.write` | Create, delete, or make changes to [listings](/docs/api-reference/latest/crm/objects/listings/create-listing).
Available to all accounts. |
| `crm.objects.marketing_events.read` | View details about [marketing events](/docs/api-reference/latest/marketing/marketing-events/guide#get-event-details).
Available to all accounts. |
| `crm.objects.marketing_events.write` | Create, delete, or make changes to [marketing events](/docs/api-reference/latest/marketing/marketing-events/guide#create-an-event).
Available to all accounts. |
| `crm.objects.orders.read` | View properties and other details about [orders](/docs/api-reference/latest/crm/objects/orders/guide).
Available to all accounts. |
| `crm.objects.orders.write` | Create, delete, or make changes to [orders](/docs/api-reference/latest/crm/objects/orders/guide).
Available to all accounts. |
| `crm.objects.owners.read` | View details about users [assigned to a CRM record](/docs/api-reference/latest/crm/owners/guide#retrieve-a-list-of-owners).
Available to all accounts. |
| `crm.objects.partner-clients.read` | View details about [partner clients](/docs/api-reference/latest/crm/objects/partner-clients/get-partner-clients) objects.
Available to all accounts. |
| `crm.objects.partner-clients.write` | Create, delete, or make changes to [partner clients](/docs/api-reference/latest/crm/objects/partner-clients/update-partner-client) objects.
Available to all accounts. |
| `crm.objects.partner-services.read` | View details about [partner service](/docs/api-reference/latest/crm/objects/partner-services/get-partner-services) objects.
Available to all accounts. |
| `crm.objects.partner-services.write` | Create, delete, or make changes to [partner service](/docs/api-reference/latest/crm/objects/partner-services/update-partner-service) objects.
Available to all accounts. |
| `crm.objects.quotes.read` | View properties and other details about [quotes and quote templates](/docs/api-reference/latest/crm/objects/quotes/get-quotes).
Available to all accounts. |
| `crm.objects.quotes.write` | Create, delete, or make changes to [quotes](/docs/api-reference/latest/crm/objects/quotes/guide) (including [legacy quotes](/docs/api-reference/legacy/crm/objects/quotes/guide)).
Available to all accounts. |
| `crm.objects.services.read` | View properties and other details about [services](/docs/api-reference/latest/crm/objects/services/get-services).
Available to all accounts. |
| `crm.objects.services.write` | Create, delete, or make changes to [services](/docs/api-reference/latest/crm/objects/services/create-service).
Available to all accounts. |
| `crm.objects.subscriptions.read` | View properties and other details about [commerce subscriptions](/docs/api-reference/latest/crm/objects/commerce-subscriptions/guide).
Available to all accounts. |
| `crm.objects.users.read` | View properties and other details about [users](/docs/api-reference/latest/crm/objects/users/guide#retrieve-users).
Available to all accounts. |
| `crm.objects.users.write` | Create, delete, or make changes to [users](/docs/api-reference/latest/crm/objects/users/guide#update-users).
Available to all accounts. |
| `crm.pipelines.orders.read` | View details about [order pipelines](/docs/api-reference/latest/crm/pipelines/guide).
Available to all accounts. |
| `crm.pipelines.orders.write` | Create, delete, or make changes to [order pipelines](/docs/api-reference/latest/crm/pipelines/guide).
Available to all accounts. |
| `crm.schemas.appointments.read` | View details about property settings for [appointments](/docs/api-reference/latest/crm/objects/appointments/get-appointments).
Available to all accounts. |
| `crm.schemas.appointments.write` | Create, delete, or make changes to property settings for [appointments](/docs/api-reference/latest/crm/objects/appointments/create-appointment)
Available to all accounts. |
| `crm.schemas.carts.read` | View details about property settings for [carts](/docs/api-reference/latest/crm/objects/carts/guide#retrieve-carts).
Available to all accounts. |
| `crm.schemas.carts.write` | Create, delete, or make changes to property settings for [carts](/docs/api-reference/latest/crm/objects/carts/guide#create-carts).
Available to all accounts. |
| `crm.schemas.courses.read` | View details about property settings for [courses](/docs/api-reference/latest/crm/objects/courses/get-courses).
Available to all accounts. |
| `crm.schemas.courses.write` | Create, delete, or make changes to property settings for [courses](/docs/api-reference/latest/crm/objects/courses/create-course).
Available to all accounts. |
| `crm.schemas.commercepayments.read` | View details about property settings for [commerce payments](/docs/api-reference/latest/crm/objects/commerce-payments/guide#properties).
Available to *Starter* accounts only. |
| `crm.schemas.companies.read` | View details about property settings for [companies](/docs/api-reference/latest/crm/objects/companies/guide)
Available to all accounts. |
| `crm.schemas.companies.write` | Create, delete, or make changes to property settings for [companies](/docs/api-reference/latest/crm/objects/companies/guide).
Available to all accounts. |
| `crm.schemas.contacts.read` | View details about property settings for [contacts](/docs/api-reference/latest/crm/objects/contacts/guide).
Available to all accounts. |
| `crm.schemas.contacts.write` | Create, delete, or make changes to property settings for [contacts](/docs/api-reference/latest/crm/objects/contacts/guide).
Available to all accounts. |
| `crm.schemas.custom.read` | View details about [custom object definitions](/docs/api-reference/latest/crm/objects/custom-objects/guide#properties) in the HubSpot CRM.
Available to *Enterprise* accounts only. |
| `crm.schemas.deals.read` | View details about property settings for [deals](/docs/api-reference/latest/crm/objects/deals/guide).
Available to all accounts. |
| `crm.schemas.deals.write` | Create, delete, or make changes to property settings for [deals](/docs/api-reference/latest/crm/objects/deals/guide).
Available to all accounts. |
| `crm.schemas.invoices.read` | View details about property settings for [invoices](/docs/api-reference/latest/crm/objects/invoices/guide).
Available to all accounts. |
| `crm.schemas.invoices.write` | Create, delete, or make changes to property settings for [invoices](/docs/api-reference/latest/crm/objects/invoices/guide#common-properties)
Available to all accounts. |
| `crm.schemas.line_items.read` | View details about [line items properties](/docs/api-reference/latest/crm/objects/line-items/guide#line-item-properties).
Available to all accounts. |
| `crm.schemas.listings.read` | View details about property settings for [listings](/docs/api-reference/latest/crm/objects/listings/get-listings)
Available to all accounts. |
| `crm.schemas.listings.write` | Create, delete, or make changes to property settings for [listings](/docs/api-reference/latest/crm/objects/listings/create-listing)
Available to all accounts. |
| `crm.schemas.orders.read` | View details about property settings for [orders](/docs/api-reference/latest/crm/objects/orders/guide#order-properties)
Available to all accounts. |
| `crm.schemas.orders.write` | Create, manage, or make changes to property settings for [orders](/docs/api-reference/latest/crm/objects/orders/guide#order-properties)
Available to all accounts. |
| `crm.schemas.quotes.read` | View details about [quotes](/docs/api-reference/latest/crm/objects/quotes/guide) and [quotes templates](/docs/api-reference/latest/crm/objects/quotes/guide#quote-templates).
Available to all accounts. |
| `crm.schemas.quotes.write` | Create, manage, or make changes to property settings for [quotes](/docs/api-reference/latest/crm/objects/quotes/guide#quote-properties)
Available to all accounts. |
| `crm.schemas.services.read` | View details about property settings for [services](/docs/api-reference/latest/crm/objects/services/get-services)
Available to all accounts. |
| `crm.schemas.services.write` | Create, manage, or make changes to property settings for [services](/docs/api-reference/latest/crm/objects/services/create-service)
Available to all accounts. |
| `crm.schemas.subscriptions.read` | View details about property settings for [commerce subscriptions](/docs/api-reference/latest/crm/objects/commerce-subscriptions/guide).
Available to all accounts. |
| `crm.schemas.subscriptions.write` | Create, manage, or make changes to property settings for [commerce subscriptions](/docs/api-reference/latest/crm/objects/commerce-subscriptions/guide).
Available to all accounts. |
| `external_integrations.forms.access` | Includes the ability to rename, delete, and clone existing forms when using the [HubSpot WordPress plugin](https://knowledge.hubspot.com/integrations/install-the-hubspot-wordpress-plugin).
Available to all accounts. |
| `files` | Access, manage, and upload [files](/docs/api-reference/latest/files/guide) in the HubSpot file manager.
Available to all accounts. |
| `files.ui_hidden.read` | Access hidden or deleted [files](/docs/api-reference/latest/files/guide) uploaded to the HubSpot file manager.
Available to all accounts. |
| `forms` | Grants access to the [legacy](/docs/api-reference/legacy/marketing/forms/guide) and v3 [forms APIs](/docs/api-reference/legacy/marketing/forms/guide)
Available to all accounts. |
| `forms-uploaded-files` | Grants access to the legacy [v1 uploaded form files API](/docs/api-reference/legacy/marketing/forms/guide)
Available to all accounts. |
| `hubdb` | Retrieve and manage [HubDB data](/docs/api-reference/legacy/cms/hubdb/guide).
Available to ***CMS Hub*** *Professional* or *Enterprise*, or ***Marketing Hub*** *Professional* or *Enterprise* accounts only. |
| `marketing.campaigns.read` | View details about [marketing campaigns](/docs/api-reference/latest/marketing/campaigns/guide) and their associated assets.
Available to ***Marketing Hub*** *Professional* or *Enterprise* accounts only. |
| `marketing.campaigns.revenue.read` | View revenue details and deal amounts attributed to a [marketing campaign](/docs/api-reference/latest/marketing/campaigns/guide).
Available to ***Marketing Hub*** *Professional* or *Enterprise* accounts only. |
| `marketing.campaigns.write` | Create, update, and delete [marketing campaigns](/docs/api-reference/latest/marketing/campaigns/guide).
Available to ***Marketing Hub*** *Professional* or *Enterprise* accounts only. |
| `marketing-email` | Grants access to retrieve and send [marketing emails](/docs/api-reference/latest/marketing/marketing-emails/guide). Publishing marketing emails using this API requires ***Marketing Hub*** *Enterprise*.
Available to all accounts. |
| `media_bridge.read` | Grants access to events and objects from the [media bridge API](/docs/api-reference/latest/cms/media-bridge/guide).
Available to all accounts. |
| `media_bridge.write` | Grants access to create and update events and objects from the [media bridge API](/docs/api-reference/latest/cms/media-bridge/guide).
Available to all accounts. |
| `oauth` | Basic scope required for OAuth. This scope is added by default to all [apps](/docs/apps/developer-platform/build-apps/create-an-app).
Available to all accounts. |
| `sales-email-read` | Grants access to read and manage [one-to-one email engagements](/docs/api-reference/latest/crm/activities/emails/guide)
Available to all accounts. |
| `scheduler.meetings.meeting-link.read` | Read metadata and booking availability for [meeting links](/docs/api-reference/latest/scheduler/guide)
Available to *Professional* accounts only. |
| `settings.billing.write` | Make changes to your [account's billing settings](/docs/api-reference/latest/account/account-information/get-account-details). This includes managing and assigning paid seats for users.
Available to all accounts. |
| `settings.currencies.read` | Reads existing exchange rates along with the [current company currency](/docs/api-reference/latest/account/settings/multicurrency/guide#retrieve-account-currencies-and-exchange-rates) associated with your HubSpot account.
Available to all accounts. |
| `settings.currencies.write` | Create, update and delete exchange rates along with updating the [company currency](/docs/api-reference/latest/account/settings/multicurrency/guide#add-account-currencies-and-set-exchange-rates) associated with your HubSpot account.
Available to all accounts. |
| `settings.users.read` | View details about [account users](/docs/api-reference/latest/account/settings/user-provisioning/users/get-users) and their permissions.
Available to all accounts. |
| `settings.users.write` | Manage [users and user permissions](/docs/api-reference/latest/account/settings/user-provisioning/users/create-user) on your HubSpot account. This includes creating new users, assigning permissions and roles, and deleting existing users.
Available to all accounts. |
| `settings.users.teams.read` | See details about the [teams in an account](/docs/api-reference/latest/account/settings/user-provisioning/teams/get-teams).
Available to all accounts. |
| `settings.users.teams.write` | Assign users to [teams on your HubSpot account](/docs/api-reference/latest/account/settings/user-provisioning/users/update-user).
Available to all accounts. |
| `tax_rates.read` | View details about [tax rates](/docs/api-reference/latest/crm/objects/line-items/guide#retrieve-tax-rates) configured in your account.
Available to all accounts. |
| `tickets` | Retrieve, manage, or create [tickets](/docs/api-reference/latest/crm/objects/tickets/guide).
Available to all accounts. |
| `tickets.highly_sensitive` | Grants access to view and edit [Highly Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#highly-sensitive-scopes) properties and values for tickets.
Available to *Enterprise* accounts only. |
| `tickets.sensitive` | Grants access to view and edit [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data#sensitive-scopes) properties and values for tickets.
Available to *Enterprise* accounts only. |
| `timeline` | Grants access to manage [legacy timeline events](/docs/api-reference/legacy/crm/extensions/timeline/v1/get-integrations-v1-application-id-timeline-event-event-typeid-event-id) on HubSpot CRM records.
Available to all accounts. |
| `transactional-email` | Access and manage [transactional emails](/docs/api-reference/latest/marketing/transactional-emails/guide).
Available to ***Marketing Hub*** *Professional* or *Enterprise* accounts with [Transactional Email Add-on](https://www.hubspot.com/products/marketing/transactional-email) only. |
**Please note:** the scopes listed below may appear on certain app configuration pages but do not currently have a corresponding API available:
* `cms.knowledge_base.articles.write`
* `cms.knowledge_base.articles.publish`
* `cms.knowledge_base.settings.read`
* `cms.knowledge_base.settings.write`
* `ctas.read`
## Deprecated scopes
The following scopes are associated with deprecated APIs and should not be used for new app development:
* `accounting`
* `actions`
* `e-commerce`
* `integration-sync`
* `social`
# Configure user-level access for an app (BETA)
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/user-level-apps
Learn about how to manage user API access in your app using user-level access.
User-level access is an alternative, more granular option to the account-wide access apps grant to users in your HubSpot account.
## User-level vs. account-level access
Account-level access provides a uniform level of access for all users in your HubSpot account, while user-level access links your app's features and API access to the associated per-user permissions in your account.
When user-level access is set up, each user that meets your app's configured [scopes](/docs/apps/developer-platform/build-apps/authentication/scopes) that correspond to their [in-app permissions](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide) can install the app.
If your app should behave differently based on who is using it, then you should opt for user-level access. Otherwise, account-level access remains the right choice for other use-cases such as account-wide automation, or syncing data across many tools in your account.
## Set up user-level access for your app
To turn on user-level access for your app, add `isUserLevel: true` to the `config` property in your app's top-level `app-hsmeta.json` [file](/docs/apps/developer-platform/build-apps/app-configuration#app-schema):
```json highlight={7} theme={null}
{
"uid": "your-app-uid",
"type": "app",
"config": {
"name": "Your App Name",
"distribution": "marketplace",
"isUserLevel": true,
"auth": {
"type": "oauth",
"redirectUrls": [
"http://localhost:4000/oauth2/hubspot-callback"
]
}
}
}
```
If `isUserLevel` is set to `false` or left unspecified, your app will use account-level access.
Once you finish updating your app's configuration and upload your project, any user who installs your app will have their actions annotated with `[User] via [app name]` in HubSpot's app audit logs.
**Please note:** existing apps using account-level access cannot currently be migrated to user-level access. You'll need to create a new app with user-level access to leverage this functionality.
## Supported platform features
The sections below outline the platform feature support for user-level access.
### Platform features
| Feature | Account-level | User-level |
| ----------------------------------------------------------------------------------------------------------- | ------------- | -------------------------------------------- |
| [APIs](/docs/api-reference/latest/overview) | Yes | Yes ([partial](#api-support)) |
| [MCP](/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server) | No | Yes |
| [App pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview) | Yes | Yes ([partial](#ui-extension-point-support)) |
| [App cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview) | Yes | No |
| [App settings](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page) | Yes | Yes ([partial](#ui-extension-point-support)) |
| [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data#hubspot-fetch) | Yes | Yes |
| [UIE hooks](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks) | Yes | Yes |
| [Serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/overview) | Yes | Yes |
| [Webhooks](/docs/apps/developer-platform/add-features/configure-webhooks) (v3, v4) | Yes | No |
| [Custom workflow actions](/docs/apps/developer-platform/add-features/custom-workflow-actions) | Yes | No |
| [Agent tools](/docs/apps/developer-platform/add-features/agent-tools/overview) | Yes | No |
| [App events](/docs/apps/developer-platform/add-features/app-events/overview) | Yes | No |
| [App objects](/docs/apps/developer-platform/add-features/app-objects/overview) | Yes | No |
| [CMS](/docs/cms/start-building/introduction/overview) | Yes | No |
| [SCIM](/docs/apps/developer-platform/add-features/scim) | Yes | No |
All [distribution options](/docs/apps/developer-platform/build-apps/app-configuration#distribution) and [authentication methods](/docs/apps/developer-platform/build-apps/app-configuration#authentication) support user-level access.
### UI extension point support
For [app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview) and [app settings](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page), you'll need to set up and manage a separate backend service yourself to provide user-specific functionality and ensure you're not sharing data between individual user installs.
### API support
The following REST APIs are currently supported with user-level apps:
* [CRM objects](/docs/api-reference/latest/crm/understanding-the-crm)
* [CRM properties and property groups](/docs/api-reference/latest/crm/properties/guide)
* [CRM object schemas](/docs/api-reference/latest/crm/objects/schemas/guide)
* [Scheduler](/docs/api-reference/latest/scheduler/guide)
* [Sales email templates (2026-09-beta)](/docs/api-reference/2026-09-beta/sales/templates/guide)
* [Sequences (2026-09-beta)](/docs/api-reference/2026-09-beta/automation/sequences/guide)
# Create a new app using the CLI
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/create-an-app
Learn how to create and customize a new app using the HubSpot CLI
Apps on the new developer platform (versions `2025.2` and `2026.03`) are initialized using the HubSpot CLI, via a series of streamlined commands. The app's configuration (name, authentication type, etc.) and any features are specified using individual configuration files, which are bundled into a project.
The steps below walk you through the process of creating a new app using the CLI, uploading the associated project to your HubSpot account, which you can install and test in a developer test account.
This article provides a full setup guide to customize and deploy a new app using the `hs project create` command.
If you're new to building apps on HubSpot, check out the [quickstart guide](/docs/getting-started/quickstart) that will get you up and running with a demo app using the streamlined `hs get-started` command.
## Prerequisites
* To create an app on the latest version of the developer platform, you'll need to [install the HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli) and authenticate it with your account using the `hs account auth` command. Make sure you're using v7.6.0 of the HubSpot CLI before proceeding. If you've already installed the CLI, you can update to the latest version of the CLI by running `npm install -g @hubspot/cli@latest`.
* You may want to [create a configurable test account](/docs/developer-tooling/local-development/configurable-test-accounts) so that you can build and test in an isolated environment.
## Create a new boilerplate project
* Run the command below in your terminal to create a new project with a boilerplate template to get you started.
```shell theme={null}
hs project create
```
* Follow the commands to set up your project. When prompted to select the base contents of your project, select **App**.
* Continue to follow the CLI prompts to configure your app details, including:
* **\[--distribution]:** select whether you plan to distribute your app on the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps) or if want to restrict installation to specific HubSpot accounts.
* **\[--auth]:** select whether you want to use OAuth for the ability to authenticate multiple accounts, or opt for a static token to limit installation to a specific account.
* **\[--features]:** select which app features to include, which will create a directory for each feature, along with the respective config files you'll need to get started. Press **spacebar** to select a feature, the **a** key to toggle all features, the **i** key to invert your current selection, and the **enter** key to proceed. The following app features are available:
* **Card:** an [app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) that will appear on a CRM record page.
* **App Function:** add support for [serverless functions](/docs/cms/start-building/features/serverless-functions/overview).
* **Settings:** add an [app settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page).
* **Webhooks:** add a configuration file to specify [webhook subscriptions](/docs/apps/developer-platform/add-features/configure-webhooks).
* **Custom Workflow Action:** add a [custom workflow action](/docs/apps/developer-platform/add-features/custom-workflow-actions).
After selecting your app features, the HubSpot CLI will create a top-level project directory, as well as subdirectories for the app features you chose.
Next, you'll customize the configuration for the app and any of its associated features.
You can add a boilerplate feature to your app at any time by running the `hs project add` command in the root directory of the project.
## Configure the newly created project and upload it to your developer account
The projects framework moves app features that were previously configured in the UI or via the API over to source code files, typically defined as `-hsmeta.json` configuration files.
App features are then created using a combination of subfolders from the main `/src/app` directory and other configuration files as needed. Learn more about your app's project structure and schema options in the [app configuration reference documentation](/docs/apps/developer-platform/build-apps/app-configuration).
### Configure UIDs and initial app features
Update the [UIDs](/docs/apps/developer-platform/build-apps/app-configuration#specifying-uids) of your app and any features:
* Change the `uid` property of the app in the top-level `app-hsmeta.json` file and give a unique name to represent your new app.
* If you opted for `static` [authentication](/docs/apps/developer-platform/build-apps/app-configuration#authentication) for a [privately distributed](/docs/apps/developer-platform/build-apps/app-configuration#distribution) app, remove the `redirectUrls` sub-property within the `auth` field of your `app-hsmeta.json` configuration (see line 10 in the example code block below).
```json lines Highlight={2} title="Example app-hsmeta.json" theme={null}
{
"uid": "new_developer_platform_app",
"type": "app",
"config": {
"description": "An example to demonstrate how to build an app with developer projects.",
"name": "my first app",
"distribution": "marketplace",
"auth": {
"type": "oauth",
"redirectUrls": ["http://localhost:3000/oauth-callback"],
"requiredScopes": [
"crm.objects.contacts.read",
"crm.objects.contacts.write"
],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
"permittedUrls": {
"fetch": ["https://api.hubapi.com"],
"iframe": [],
"img": []
},
"support": {
"supportEmail": "support@example.com",
"documentationUrl": "https://example.com/docs",
"supportUrl": "https://example.com/support",
"supportPhone": "+18005555555"
}
}
}
```
* For any features you want to include (e.g., app cards), update the UID within any associated `*-hsmeta.json` configuration files in your project.
Keep in mind that UIDs are used as a unique identifier for all your project's components and features. Once your app or any of its features has been uploaded with a specific UID, changing it in subsequent deployments will force the platform to recognize it as different from previous builds, which may not be intended.
### Set up OAuth (if applicable)
If you plan on distributing your app to multiple accounts (either with a specific set of allowlisted accounts or via the HubSpot app marketplace), you'll need to set up OAuth for your app by following the steps below. Otherwise, you can skip this step and proceed to [upload your project](#upload-your-project).
* Add one or more valid redirect URLs to the `app-hsmeta.json` file based on your local (or another non-production) OAuth server configuration.
* If you don't have a backend service set up already, you can get started by using the [sample OAuth Node.js example](http://github.com/hubspot/oauth-quickstart-nodejs) and run it locally. It's already set up to work with `https://localhost:3000/oauth-callback` as the redirect URL configured in the boilerplate example code from the `hs project create` command you ran in the previous step.
```json expandable Highlight={7-10} title="Example app-hsmeta.json" theme={null}
{
"uid": "new_developer_platform_app",
"type": "app",
"config": {
"description": "An example to demonstrate how to build an app with developer projects.",
"name": "my first app",
"distribution": "marketplace",
"auth": {
"type": "oauth",
"redirectUrls": ["http://localhost:3000/oauth-callback"],
"requiredScopes": [
"crm.objects.contacts.read",
"crm.objects.contacts.write"
],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
"permittedUrls": {
"fetch": ["https://api.hubapi.com"],
"iframe": [],
"img": []
},
"support": {
"supportEmail": "support@example.com",
"documentationUrl": "https://example.com/docs",
"supportUrl": "https://example.com/support",
"supportPhone": "+18005555555"
}
}
}
```
### Upload your project
After you've updated your app and feature schemas, run the `hs project upload` CLI command to upload your project to your HubSpot account and automatically trigger a new build.
If your app is configured to use OAuth authentication, proceed to the next step to retrieve the app's authentication details. Otherwise, you can proceed to [app installation](#install-your-app).
## Add the client ID and client secret of your app to your app
If you configured the [authentication type](/docs/apps/developer-platform/build-apps/app-configuration#authentication) to use `oauth`, you'll need to set up your backend OAuth server to use your app's client ID and secret, which you can find in HubSpot:
* In the terminal, run `hs project open` from within your local project directory to open the project details page in HubSpot.
* Under *Project Components*, click the **name** of your app.
* Click the **Auth** tab.
* Under *Client credentials*, copy the *Client ID* and *Client secret* from your new app and paste them into the corresponding locations in your OAuth server's configuration, then restart your OAuth server.
Your app is now ready to test with an installed account.
## Install your app
If you're still planning on testing your app out before getting it ready for a production setting, it's recommended you start by installing it in a [developer test account](#install-in-a-developer-test-account). Otherwise, users with the [required user permissions](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#permission-requirements) can install the app directly in their [standard HubSpot account](#install-in-a-standard-hubspot-account).
### Install in a developer test account
If you don't already have a [test account](/docs/getting-started/account-types#developer-test-accounts), you can create one in HubSpot:
* Navigate to **Test accounts** in the *Development* navigation menu, then click **Create developer test account**. Follow the prompts to create your new test account.
* In the left sidebar menu, navigate to **Projects**, click the **name** of your new project, then click the **UID** of your app in the component list.
* On the *Distribution* tab, next to *Test installs*, click **Add test install(s)**.
* In the right panel, click **Install** next to the test account you created.
* Review the app permissions, select the **checkbox** to authorize installing an unverified app, then click **Connect app**.
### Install in a standard account
If you have the [required user permissions](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#permission-requirements), you can also install your app directly in your [standard account](/docs/getting-started/account-types#standard-hubspot-accounts):
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Projects**, click the **name** of your new project, then click the **UID** of your app in the component list.
* On the *Distribution* tab, under *Standard install*, click **Install now**.
After initiating the install, you'll be prompted to review the app permissions.
* Select the **checkbox** to authorize installing an unverified app, then click **Connect app**.
* Once successful, click **View installed app details** to navigate to the *Connected Apps* page of the account where you installed your app.
Learn more about [distributing your app](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#distribute-your-app).
## Local development and previews
Once you've successfully installed the app into the test account, you can run `hs project dev` to start developing your app locally.
* When running this command, you'll see a link to view your project status and source code within your primary developer account as well as a link to access a local development homepage in your test account.
* This homepage will provide you details about the active local development session, including which components are being developed locally and how you can preview those components to test your changes in real time.
## Next steps
Check out the documentation for guidance on [configuring an app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) and [creating a settings page for your app](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page).
# Integrate AI tools with the HubSpot MCP server
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server
Learn how to configure an app to authenticate requests to the HubSpot MCP server for interacting with HubSpot's APIs.
The HubSpot Model Context Protocol (MCP) server enables AI assistants and Large Language Models to securely interact with your HubSpot CRM data through natural conversation. By implementing the [Model Context Protocol](https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro) standard, this remote server acts as a bridge between AI systems and HubSpot's APIs, allowing for intelligent automation and insights without requiring users to understand complex API structures.
For developers, the remote HubSpot MCP server provides secure, granular access to HubSpot CRM data through AI agents and third-party applications, and allows teams to build custom workflows, automate reporting, and integrate HubSpot context into third-party applications. For end-users, it creates a more streamlined, simpler way to query their HubSpot data using natural language.
The HubSpot MCP server documented on this page is separate from the [developer MCP server](/docs/developer-tooling/local-development/developer-mcp/setup). The developer MCP server helps developers build apps and CMS content assets locally on HubSpot's developer platform. In contrast, the HubSpot MCP server is intended for making requests to an account's CRM data, similar to using HubSpot's APIs.
## Implementation overview
At a high level, integrating with the HubSpot MCP server involves:
1. Creating an MCP auth app in your HubSpot account.
2. Configuring your MCP client to connect to the HubSpot MCP server at `https://mcp.hubspot.com` using your app's OAuth credentials.
3. Prompting your MCP client using natural language to query your HubSpot data.
Once you've created your MCP auth app, you can use it with any MCP client that supports OAuth authentication with [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key for Code Exchange). PKCE is required for authenticating with HubSpot's MCP server.
Some MCP clients, like the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), handle PKCE automatically. If you're building a custom integration, ensure your OAuth implementation includes PKCE support. Learn more about [OAuth 2.1 authorization in the MCP specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization).
## Supported data and permissions
The following data is accessible via the remote MCP server.
**Read access:**
* **CRM records:** contacts, companies, deals, tickets, users, carts, invoices, orders, line items, products, quotes, subscriptions, and segments (lists)
* **Activities:** calls, emails, meetings, notes, and tasks
* **Content and marketing:** blog posts, landing pages, site pages, campaigns, and marketing events
* **Conversations:** messages and threads from live chat, team email, WhatsApp, SMS, Facebook Messenger, and custom channels connected to help desk or conversations inbox
Conversations access has additional restrictions based on your HubSpot inbox configuration.
* Help desk conversations are visible to all users.
* For the conversations inbox, if access has been restricted to specific users or teams, only those users can query those conversations through the MCP server.
* **Marketing emails:** draft emails and previews, email analytics data including aggregate send statistics (sends, opens, clicks, bounces, unsubscribes, and deliveries), account email health diagnostics, and per-contact delivery and engagement details
**Write access:**
* **CRM records:** contacts, companies, deals, tickets, line items, and products
* **Activities:** calls, emails, meetings, notes, and tasks
* **Marketing emails:** create and update email drafts
All actions respect your existing HubSpot user permissions. Users can only view and modify records they have access to in HubSpot.
When creating an MCP auth app, it's important to note that you don't explicitly define the app's scopes. Instead, available scopes are automatically determined by two factors:
* The tools available in the MCP server at the time of installation.
* The permissions that the user chooses to grant during installation.
As the MCP server's tools are updated, the available scopes may change. In the event of scope updates, users who have already installed the app will need to re-install to grant any new scopes.
Behind the scenes, the HubSpot MCP server is based on the [CRM search API](/docs/api-reference/latest/crm/search-the-crm), which currently doesn't include vector search capabilities.
**Please note:** if your HubSpot account has [Sensitive Data](/docs/api-reference/latest/crm/properties/sensitive-data) turned on, activity objects (such as calls, emails, meetings, notes, and tasks) and conversation data will be blocked from access through the MCP server. This restriction is specific to the MCP server and does not apply to the standard CRM APIs.
### Available tools
Below is a list of the tools provided by the remote MCP server.
Returns the authenticated user's information, account details, and per-object access (read and write).
Search and filter CRM records using filter groups, text queries, sorting, and pagination. Based on the [CRM search API](/docs/api-reference/latest/crm/search-the-crm). Supports up to five filter groups with up to six filters each. Filters within a group use AND logic, and groups are combined using OR logic. Maximum 200 results per page.
Fetch one or more CRM objects by their IDs in a single request. Maximum 100 object IDs per request.
Create or update CRM records or activities.
Find property definitions for an object type using keyword search. Returns property names, labels, and descriptions. Maximum 5 keywords per request.
Get full property definitions, including data types and enumeration values. Responses can be large, so it's recommended to fetch specific properties by name when possible.
Find CRM record owners by name or email, or look up owners by ID. Maximum 100 results.
Fetch paginated contact IDs for a campaign filtered by attribution type.
Get campaign analytics (metrics or revenue attribution) for one or more campaigns.
List the asset type names available as campaign assets (e.g., landing pages or blog posts).
Get metrics and properties for specific CRM objects associated with a campaign.
Search conversations and messages from HubSpot inboxes. Supports lookup by CRM object, time range, or keyword, and aggregation by channel, inbox, or status.
List the inboxes, channels, and channel instances available in the portal. Call this before using search\_conversations to filter by channel, inbox, or instance IDs.
Get analytics data for marketing emails based on sends within a given date range. Supports three modes: account-level aggregate statistics (sends, deliveries, opens, clicks, bounces, and unsubscribes), account email health diagnostics by MX group, and per-contact delivery and engagement details for a specific email send.
Manage marketing email settings and content. Supports:
* Listing available templates, subscription types, and from addresses
* Creating, updating, and cloning email drafts
* Retrieving, editing, and previewing email content
* Managing A/B variants.
Creating and updating email drafts requires the following app permissions:
* View properties about the installing account including primary domain and user emails
* See details about this account's teams
* Create and edit marketing email content
Send feedback about the MCP server experience to HubSpot.
## Create an MCP auth app
Create an MCP auth app [in your HubSpot account](https://app.hubspot.com/l/mcp-auth-apps/).
* In the main navigation bar of your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **MCP Auth Apps**.
* In the upper right, click **Create MCP auth app**.
* In the dialog box, enter your app details, which you can update later as needed:
* **App name:** the name of your app.
* **Description:** an optional description of your app.
* **Redirect URL:** the URL to use for OAuth authentication. If you'll be testing with the MCP inspector, you'll need to include `http://localhost:6274/oauth/callback/debug` as a redirect URL.
* **Icon:** an optional icon for your app.
* Click **Create**.
HubSpot will then generate an app configured with OAuth authentication using the details you provided. You'll then be redirected to the app's details page, where you can view its client credentials, redirect URLs, and more. To edit your app details, you can click **Edit info** in the upper right.
If you're including multiple redirect URLs, the first redirect URL will be used as the default redirect.
You can then proceed to connect your MCP client to the HubSpot MCP server, using your app to authenticate requests.
For quick testing, you can use the MCP inspector tool, as described in the next section. Otherwise, you can continue to the [General MCP client connection](#general-mcp-client-connection-instructions) section for general instructions.
## Test with the MCP inspector
The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a debugging tool that handles OAuth with PKCE automatically, making it a quick way to test your connection to HubSpot's MCP server without building a full integration.
To connect using MCP Inspector:
* Run the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) tool locally.
* In the browser window that loads the MCP Inspector, configure the environment fields in the left sidebar as follows:
* **Transport Type:** Streamable HTTP
* **URL:** `https://mcp.hubspot.com/`
* **Client ID:** your app's client ID, as displayed in HubSpot.
* **Client secret:** your app's client secret, as displayed in HubSpot.
* With your initial details configured, click **Open Auth Settings**.
* In the *OAuth Authentication* section, click **Guided OAuth Flow**.
* In the *OAuth Flow Progress* section, begin the OAuth flow by clicking **Continue** below the progress steps.
* Click **Continue** after each step to proceed to the next.
* As you progress, the MCP inspector will provide expandable sections with details about the results of each step. This can be helpful for debugging issues with the OAuth flow, such as confirming the authorization URL and token response.
* During the *Preparing Authorization* step, an authorization URL will be provided. Click the **link icon** next to the URL to open it in a new tab and proceed with installing the app in your HubSpot account.
* At the end of the HubSpot account installation process, an authorization code will be provided, which you'll need to copy into the *Authorization Code* field in the MCP inspector.
* After completing all steps of the OAuth flow, the MCP inspector will display an *Authentication successful!* message. Your generated token will automatically be used to authenticate requests to the HubSpot MCP server. As you continue to test with the MCP inspector, you can use the **Guided Token Refresh**, **Quick Refresh**, and **Clear OAuth State** buttons to manage your authentication state.
* With authentication complete, click **Back to Connect** in the top right to return to the MCP inspector's main panel.
* In the bottom of the left sidebar, click **Connect**.
The status will then update to *Connected* and the main panel will display a set of options.
* In the top bar, select **Tools**.
* Click **List Tools**.
* In the tools list, scroll and select **get\_user\_details**. The right panel will display details about the tool.
* Under the tool details in the right panel, click **Run tool**.
* After the tool runs, it will return a success message along with information about your user, the account you installed the app in, and will show object and tool data availability, based on the app's scopes and your user permissions.
## General MCP client connection instructions
After creating your MCP auth app, you can connect your MCP client to `https://mcp.hubspot.com` using your app's credentials. Your MCP client will need to handle the OAuth flow, including PKCE, to authenticate with HubSpot.
To configure your MCP client, you'll need the following from your app's details page:
* Client ID
* Client secret
* Redirect URL (must match what's configured in your MCP client)
During the OAuth flow, you will:
1. Select the HubSpot account to connect.
2. Grant permissions to the app. These permissions are based on the user's permissions in HubSpot and determine what data the app can access.
3. Authorize the connection.
Once authorized, your MCP client can make requests to the HubSpot MCP server on behalf of the authenticated user.
Not sure where to start with MCP clients? Check out [this list](https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro) of applications that support MCP integrations.
## Troubleshooting
### PKCE-related authentication failures
HubSpot's MCP server requires [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key for Code Exchange) for all OAuth authentication flows. If your MCP client doesn't handle PKCE automatically, you'll need to implement it yourself:
* Generate a random `code_verifier` (43-128 characters).
* Derive the `code_challenge` from the verifier using the S256 method.
* Include the `code_challenge` and `code_challenge_method=S256` in the authorization request.
* Include the `code_verifier` in the token exchange request.
If you see authentication errors during the OAuth flow, verify that your client is correctly generating and sending the PKCE parameters. Some MCP clients, like the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), handle PKCE automatically.
### Token refresh failures
OAuth access tokens expire after a set period. If your MCP client stops working after a period of inactivity:
* Ensure your client is using the `refresh_token` returned during the initial OAuth flow to request a new access token.
* If the refresh token has also expired or been invalidated, you'll need to re-run the full OAuth authorization flow.
* Check that your redirect URL in the MCP client matches the redirect URL configured in your MCP auth app in HubSpot.
# Manage apps in HubSpot
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot
Learn how to manage authentication, installation, and other settings for your app on the new developer platform
After you upload your app to HubSpot using the [app creation guide](/docs/apps/developer-platform/build-apps/create-an-app), migrating it from a legacy app, or manually running the command `hs project upload`, you can manage your app on the projects index page in your HubSpot account. On the details page for your app, you can view its build and deploy history, confirm active app features, and manage authentication.
## View your app on the project details page
To review your app details:
* In your HubSpot account, navigate to **Development**.
* On the **Projects** page, click the **name** of your project. On the project details page, review and manage your app:
* In the left sidebar menu, you can click **Details** to jump straight to recent build and deploy history, or click an app feature to check the configuration as detailed in its associated `*-hsmeta.json` file.
* Click the **Settings** tab to toggle your auto-deploy settings, or if you need to delete your app.
* Under *Project Components*, click your app's \`name (as specified in your app's [top-level schema](/docs/apps/developer-platform/build-apps/app-configuration#app-schema)) to review the most recently deployed app schema, the currently configured app features, and manage authentication settings.
## Locate your app's client credentials and redirect URLs
If you're setting up [OAuth authentication](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth) or [request validation](/docs/apps/developer-platform/build-apps/authentication/request-validation) for your app, you can find the required credentials such as your app's client ID, client secret, and redirect URLs within the authentication details page:
* On the *Overview* tab of the project details page, under *Project Components*, click the top-level app name as defined in the `name` field in your app's [top-level schema file](/docs/apps/developer-platform/build-apps/app-configuration#app-schema).
* Click the **Auth** tab.
* Your app's *Client ID* and *Client secret* will appear in the *Client credentials* section.
* Scroll down to the bottom of the page to locate your app's currently configured *Redirect URL*, used in the initial [authorization](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth#set-up-oauth-authentication) step when a user installs your app via OAuth. You can update these URLs by updating the `redirectUrls` field in your app's top-level `app-hsmeta.json` [file](/docs/apps/developer-platform/build-apps/app-configuration#app-schema), then running `hs project upload`.
### Rotate client secret
Your app's client secret is a confidential value used for managing OAuth tokens, validating requests, and generating a client credentials token.
If this secret is compromised, you can rotate the secret to ensure the old one is invalidated and cannot be used.
To rotate your app's client secret:
* In your HubSpot account, navigate to **Development**.
* On the **Projects** page, click the **name** of your project.
* On the *Overview* tab of the project details page, under *Project Components*, click the top-level app name as defined in the `name` field in your app's [top-level schema file](/docs/apps/developer-platform/build-apps/app-configuration#app-schema).
* Click the **Auth** tab.
* Next to your client secret, click **Rotate**.
* In the dialog box, confirm you're ready to rotate your secret, then enter the name of your app and click **Rotate secret**.
## Distribute your app
Based on the [authentication](/docs/apps/developer-platform/build-apps/app-configuration#authentication) and [distribution](/docs/apps/developer-platform/build-apps/app-configuration#distribution) you configure in your app's `app-hsmeta.json` file of your project, you can install your app in a single account, multiple allowlisted accounts, or prepare to [list the app](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) on the HubSpot Marketplace.
### Installation limits
App installation is subject to the following limits based on your app's configuration:
* **Static token apps:** can only be installed in 1 [standard HubSpot account](/docs/getting-started/account-types#standard-hubspot-accounts) at a time, and up to 10 [developer test accounts](/docs/getting-started/account-types#developer-test-accounts).
* **Privately distributed OAuth apps:** can be installed in up to 10 allowlisted accounts (not including developer test accounts).
* **Marketplace OAuth app prior to listing:** can be installed in up to 25 accounts (not including developer test accounts).
* **Marketplace OAuth app after being listed:** can be installed in unlimited accounts.
### Permission requirements
The following users can install an app in a standard HubSpot account:
* [Super admins](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin)
* Users with the *HubSpot Marketplace access* [permission](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#settings-access) along with any scope groups requested by the app (e.g., if your app requires the `crm.import` scope, the user must have the *Import* permission in the account to install the app)
### Navigate to your app distribution settings
After you [upload your app's project](/docs/developer-tooling/local-development/hubspot-cli/project-commands#upload-to-hubspot) or [deploy a new build](/docs/developer-tooling/local-development/hubspot-cli/project-commands#deploy-to-hubspot) to HubSpot, navigate to the distribution settings of your app.
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, click **Projects**, then click the *name* of the project associated with your app.
* On the *Overview* tab of the project details page, under *Project Components*, click the top-level app name as defined in the `name` field in your app's [top-level schema file](/docs/apps/developer-platform/build-apps/app-configuration#app-schema).
* Click the **Distribution** tab.
Then, consult the corresponding section below to install your app based on your app's configuration.
### Install an app with a static token
Apps built with a static auth token can be installed in a single [standard account](/docs/getting-started/account-types#standard-hubspot-accounts) at a time.
**Please note:** you can also use your static token to install your app in up to 10 [developer test accounts](/docs/getting-started/account-types#developer-test-accounts).
To install an app with a static token:
* Under *Manage distribution* on the *Distribution* tab, your current HubSpot account will appear. Click **Install now**.
* You'll be prompted to confirm the installation. Review the data that the app is requesting access to, which should align with the scopes you configured. Then click **Connect app**.
* If installation is successful, the app's *Distribution* tab within your project will display an access token. Click **Show** to reveal the full token, which you can then copy and use to authenticate your app's API requests to HubSpot.
**Please note:**
* To view or rotate your access token, you must be a super admin or have a developer seat for the associated account.
* It's recommended you rotate your token every 6 months for security purposes.
* If you need to install the app on a different account, the app must first be uninstalled from the first account.
* If you update the scopes for your app, you'll need to reinstall the static token app using the *Reinstall URL* on the *Distribution tab* to apply the changes.
### Install a privately distributed OAuth app
To install an OAuth app in a set of allowlisted accounts, make sure your OAuth server is fully set up, then proceed to the following steps:
* On the *Distribution* tab of your app settings, a summary of currently installed accounts will appear at the top of the page. For apps with a `private` distribution, you can install your app in a maximum of 10 HubSpot accounts.
* To add allowlisted accounts, click **Add approved account(s)**.
* In the right panel, under *Other production accounts*, review the list of production accounts that your HubSpot user currently has access to.
* Select the **checkbox** next to any account you want to add to the allowlisted accounts that can install your app.
* When you're done, click **Save changes**.
* In the list of approved accounts, you can hover over an account to copy its install link, or remove it from the list of allowlisted accounts.
* To install the app, copy the allowlisted account's install URL, then paste into the URL bar of a new browser window.
* You'll be prompted to confirm the installation. Review the data that the app is requesting access to, which should align with the scopes you [configured](/docs/apps/developer-platform/build-apps/app-configuration#scopes). You can also clear any of the checkboxes under *Optional scopes* for any permissions that you don't want to grant to the app. After confirming app permissions, click **Connect app**.
### Install an OAuth marketplace app
To install an OAuth marketplace app, make sure your OAuth server is fully set up, then proceed to the following steps:
* On the *Distribution* tab of your app settings, click **Begin publishing**.
* Review and sign the Acceptable Use Policy (AUP). Until the AUP is signed, you won't be able to install your app in accounts other than developer test accounts.
* If you're not ready to create a marketplace listing for your app, you can close the right panel to return to the app management page. You can return to the panel at any time by clicking **Continue publishing**.
* You can use the sample install URL to install your app in any account. OAuth marketplace apps do not use an allowlist to manage installed accounts.
Until your app is reviewed by the HubSpot Ecosystem Quality team and listed on the Marketplace, you'll be limited to 25 installs. Once your app is published, you can install your app in an unlimited number of accounts.
To install your app in an account, navigate to the install URL in a browser window:
* Select the **account** where you want to install your app.
* After choosing an account, you'll be presented with a list of scopes to authorize based on what you specified in the top-level [app configuration](/docs/apps/developer-platform/build-apps/app-configuration#scopes) file. You can also clear any of the checkboxes under *Optional scopes* for any permissions that you don't want to grant to the app. After confirming app permissions, click **Connect app**.
* If your app has not yet been verified by the HubSpot Ecosystem Quality team, a warning banner will appear to alert the user that the app is unverified:
* Carefully review all scopes being requested by the app to confirm they match what you expect.
* Click **Connect app**.
* In the dialog box, double-check all details of the app to confirm that the unverified app matches what you expect. Then type **I accept the risk**, and click **Connect** to authorize the app.
Learn more about [listing your app](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) on the HubSpot Marketplace.
## Manage keys
Beyond the authentication methods available for your app (e.g., OAuth or static auth access tokens), two other keys are available in the developer overview of your account: a developer API key and a personal access key.
To review your keys or create a new key in your developer account:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, click **Keys**, then click **Personal access key** or **Developer API key**.
### Personal access keys
Personal access keys provide a form of authentication that's specific to one HubSpot user and one HubSpot account, based on permissions selected when the key is first generated. Once generated, the key is used to authenticate when developing locally using the HubSpot CLI, such as when you run `hs init` or `hs account auth` commands.
Learn more about using auth-related CLI commands in the [CLI reference documentation](/docs/developer-tooling/local-development/hubspot-cli/reference#authentication).
### Developer API keys
Some APIs and features, such as the [custom channels API](/docs/api-reference/legacy/conversations/guide), require a developer API key to authenticate the request. Each key is specific to a HubSpot account, not an individual user, and only one key is allowed at a time. You can deactivate your API key and generate a new one at any time.
For the applicable APIs, this key is provided as the `hapikey` query parameter in your request, often accompanied by the associated `appId` query parameter that corresponds to the app you want to make changes for. For example, the `cURL` snippet below provides an example of using the [custom channel registration](/docs/api-reference/legacy/conversations/guide) endpoint:
```shell theme={null}
curl --request POST \
--url "https://api.hubapi.com/conversations/v3/custom-channels?hapikey={YOUR_DEVELOPER_API_KEY}&appId={appId}
```
## Verified domains
When a HubSpot user installs an app, they consent to grant the app developer access to their account data, subject to [scopes](/docs/apps/developer-platform/build-apps/authentication/scopes) that the app requests, such as retrieving or updating contacts in their CRM. The developer's identity and reputation each play an important role in a user's decision to proceed to install the app in their account.
### Verification status during app installation
To ensure full user consent when installing an app, HubSpot will display a message on the app installation page based on the app's level of verification and if the app is listed on the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps):
* When an app doesn't have a verified domain, a banner will appear that indicates the app hasn't been verified.
* When an app does have a verified domain but isn't officially listed on the HubSpot Marketplace, HubSpot will display the verified domain, along with a banner to indicate that the app hasn't been reviewed or explicitly approved by HubSpot.
* When an app has been submitted for certification and officially passes HubSpot's app review process, HubSpot will not display any warning banner. You are not required to verify the domain if your app has been listed on the HubSpot Marketplace, since the app has been fully vetted by the HubSpot Ecosystem Quality team.
### Add a verified domain
To add a verified domain for your apps, add the domain within the developer overview page of your account, then set up a TXT record in your domain provider's DNS settings:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, click **Domain**.
* Click **Verify a domain**.
* In the right panel, enter your **domain**, then click **Next**.
* In the DNS settings of your domain provider, you'll need to set up a TXT record with the provided *Host (name)* and *Value* fields with the values provided. Once configured in your domain provider, click **Next**.
* The verification process usually completes within an hour, but you may need to wait up to 48 hours for the new DNS information to propagate. Once verified, you'll see the domain appear with a success state on the *Domain* details page in the developer overview.
### Additional notes
Keep the following caveats regarding domain verification in mind:
* To ensure continued ownership of the domain, HubSpot will continue to verify that the TXT record is present on a regular basis. The install warning will return if the TXT record is removed or modified.
* You can only have one verified domain per developer account. All apps in an account share the verified domain. The domain on the install page will link to your root domain.
* If you delete your verified domain, any user who installs your app will be prompted with the [verification warning noted above](#verification-status-during-app-installation). You can verify another domain, but the process will take a couple hours.
# Migrate a legacy CRM card to an app card
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-legacy-crm-cards-to-app-cards
Learn how to migrate your card from a legacy CRM card to an app card.
Legacy CRM cards will be [sunset on Oct 31, 2026](https://developers.hubspot.com/changelog/deprecating-support-for-classic-crm-cards). This means if your app currently contains a legacy CRM card, it must be migrated to an app card before the sunset date to avoid interruptions for your end users. App cards built with React can do everything legacy CRM cards can do, but offer more advanced customization options.
This guide walks you through how to build a replacement app card and replace your legacy CRM card with an app card in user accounts. Once your card is migrated, your users will not need to take action to adopt the replacement app card.
## Prerequisites
* Ensure you've installed the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli).
* Ensure your app is on the projects framework, using [version `2025.2`](/docs/developer-tooling/platform/versioning) or newer. Learn how to [migrate a public app to the projects framework](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-an-existing-public-app).
* It's recommended to [create a configurable test account](/docs/developer-tooling/local-development/configurable-test-accounts) so that you can build and test in an isolated environment.
## Create an app card to replace the legacy CRM card
To start the migration process, create an app card that will replace your legacy card.
**Please note:** it's highly recommended to build your app card in a [separate testing environment](/docs/developer-tooling/local-development/configurable-test-accounts) prior to adding it to your app.
When creating your replacement app card, you can either:
* **[Start from scratch](#redesign-by-creating-a-new-app-card)**: create a new app card. Choose this option if you want to use new functionality and design elements to redesign your card.
* **[Use HubSpot's converter app card as an example](#convert-a-legacy-crm-card-into-an-app-card)**: use the [Legacy CRM Card Converter GitHub sample code](https://github.com/HubSpot/ui-extensions-examples/tree/main/legacy-card-converter) to create an app card based on the design and functionality of a legacy CRM card. Choose this option if you want to quickly add a replacement that works and looks as close as possible to your legacy CRM card.
### Redesign by creating a new app card
To start from scratch, follow the instructions to [create a new app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card#create-an-app-card).
React-based app cards can include more design elements than your existing legacy card, such as [images](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image), [forms](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form), and [tables](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/table). With more UI components and data fetching capabilities, app cards give you the opportunity to design dynamic experiences embedded within HubSpot to meet your users where they work.
Use the [Figma design kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit#figma-design-kit-ui-extensions) to plan your designs, or refer to the [components reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) to get started. During beta testing, you can enable the app card for a subset of your users by using [feature flags](/docs/api-reference/latest/app-management/feature-flags/guide#gradually-roll-out-app-cards).
Once your new app card is ready, you can [add it to the app with the legacy CRM card](#add-the-app-card-to-the-app-with-the-legacy-crm-card) to begin the migration process.
### Convert a legacy CRM card into an app card
If you want to quickly convert your legacy CRM card to an app card, refer to the [Legacy CRM Card Converter GitHub sample](https://github.com/HubSpot/ui-extensions-examples/tree/main/legacy-card-converter) for guidance. This sample provides an app card that works like a legacy CRM card to help you test and develop a new app card before migrating. The converter fetches data from an API endpoint to display tiles, expandable content, and action buttons that work almost identically to your legacy CRM card. Refer to the [README.md](https://github.com/HubSpot/ui-extensions-examples/tree/main/legacy-card-converter#readme) for instructions and more details, including the [differences between the legacy CRM card and the converter app card](https://github.com/HubSpot/ui-extensions-examples/tree/main/legacy-card-converter#differences-from-legacy-crm-cards).
Once your new app card is ready, you can [add it to the app with the legacy CRM card](#add-the-app-card-to-the-app-with-the-legacy-crm-card) to begin the migration process. If you want to redesign the card later, you can [update the app card design](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference) as needed.
## Add the app card to the app with the legacy CRM card
Once you've set up the replacement app card, add it to the project with the legacy CRM card.
1. Copy the contents of the `cards` folder of the development project.
2. Paste the card content in the `cards` folder of the app with the legacy CRM card.
3. Upload the project (`hs project upload`). The `uid` of the replacement card will be shown as processing during build and deploy.
4. Verify the app card displays in installs with scopes by [adding it to views](https://knowledge.hubspot.com/integrations/install-and-manage-app-cards).
**Please note:** if your migrated app is listed on the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps), the `hs-release-app-cards` flag is initialized during migration, keeping your app cards hidden by default. Use the [feature flag API](/docs/api-reference/latest/app-management/feature-flags/guide#gradually-roll-out-app-cards) to make the app card available for select users to add to their views for beta testing. Enabling the flag makes the card available to add to views — it does not replace the legacy CRM card in existing views. Once you're ready to replace the legacy card in all user views, delete the feature flag, then use the migration described below. The feature flag must be deleted prior to starting the migration.
If you've used the `hs-hide-crm-cards` flag, it must also be deleted prior to migration. Use the [feature flag API](/docs/api-reference/latest/app-management/feature-flags/flags/delete-feature-flag) to delete the flags from your app.
## Replace the legacy CRM card with the app card in user accounts
Once the [new app card is included in your app](#add-the-app-card-to-the-app-with-the-legacy-crm-card), migrate the legacy card to the new app card.
* **During the migration**: users will continue to see the legacy CRM card in their views. Some users may temporarily see both the legacy CRM card and the app card in some views, but they will not lose access to the cards in your app.
* **When a migration is complete**: the legacy CRM card will be replaced by the app card in all user views. The legacy CRM card will be hidden and users will not be able to add it to views.
**Please note:** once the card view migration has started, it cannot be reversed. Be sure the app card works as intended through beta testing before replacing the card in all user views.
**Please note:** if your legacy CRM card supports tickets, an app card must be enabled for the `helpdesk.sidebar` location for it to appear in the [help desk workspace](https://knowledge.hubspot.com/help-desk/overview-of-the-help-desk-workspace). This means you need two app cards to support existing legacy CRM cards for tickets: one for the `crm.record.sidebar` location and one for the `helpdesk.sidebar` location. They will need different titles, and you need both app card IDs to undertake migration.
### Replace a card in the projects UI
To migrate from within HubSpot:
1. In your HubSpot account, navigate to **Development**. Then, click the **name** of your project.
2. If your project has legacy CRM cards, a migration banner is displayed with a list of the cards to migrate. At the bottom of the banner, click **Review & migrate**.
3. In the right panel, review the migration details, then click **Next** to begin a card migration.
4. Select the **legacy CRM card** to migrate.
5. Select the **replacement app card** to which your legacy card will be migrated. App cards are only shown as options if they are compatible replacements for the legacy card.
* If your legacy CRM card supports tickets, select a **Helpdesk sidebar card** to replace the card displayed in the [help desk workspace](https://knowledge.hubspot.com/help-desk/overview-of-the-help-desk-workspace).
* If you've selected a replacement card used in a previous migration, select the **Allow consolidating multiple legacy CRM cards into one app card** checkbox to confirm that you're consolidating multiple legacy cards into one new app card.
6. Click **Migrate**, then in the dialog box, click **Migrate** to confirm. Once the migration has started, it cannot be stopped or reversed.
7. The migration process will begin and progress is displayed in the panel. If your app has many installs, it may take a few minutes.
8. When the migration is finished, the panel will show the status *Migration Complete*.
* To delete the legacy CRM card, click **Delete legacy card**. In the dialog box, click **Delete** to confirm. You can [delete at a later time](#delete-unused-legacy-crm-cards) or the card will be automatically deleted when [legacy CRM cards are deprecated](https://developers.hubspot.com/changelog/deprecating-support-for-classic-crm-cards).
* To begin migrating another legacy CRM card, click **Migrate another card**.
### Replace a card with the migrate views API
To use the migrate views API endpoint to replace legacy CRM cards:
1. Copy the values for the following fields: `appId`, `appCardId`, `legacyCrmCardId`, and if your legacy CRM card supports tickets, `helpdeskAppCardId`. [Retrieve cards](/docs/api-reference/legacy/crm/extensions/crm-cards/get-crm-cards) to get the legacy card ID and [view projects in HubSpot](/docs/cms/start-building/introduction/react-plus-hubl/project-structure) to get the app, app card, and help desk app card IDs.
2. Make the following call to the migration endpoint (`/crm/v3/extensions/cards-dev/{appId}/views/migrate`).
```shell wrap theme={null}
curl -X POST 'https://api.hubapi.com/crm/v3/extensions/cards-dev/{appId}/views/migrate?hapikey={developerApiKey}' \
-H 'Content-Type: application/json' \
-d '{"legacyCrmCardId": "{legacyCrmCardId}", "appCardId": "{appCardId}", "helpdeskAppCardId": "{helpdeskCardId}"}'
```
By default, the migration endpoint blocks requests if the target `appCardId` is already associated with another legacy CRM card migration. This prevents multiple legacy CRM cards from being migrated into a single app card. To consolidate multiple legacy CRM cards into a single app card, set `allowDuplicateAppCardIds` in the request body to `true`.
3. The migration process will begin and the API will return a message summarizing the progress. View updates are processed asynchronously. You can safely call the endpoint again with the same request body to check progress. Retrying the endpoint will not duplicate work or cause adverse effects.
```shell wrap theme={null}
Migration Underway: X of X installs still processing. This process takes longer for high install count apps, or for customers with unique view setups. Re-hit this endpoint with the same body to see progress.
```
The migration should complete within a few minutes. If the migration takes longer than expected, the endpoint will return an extended status message.
4. Once the migration is complete, the following message will be returned.
```shell wrap theme={null}
Migration Complete: All customer views have been migrated to App Card X. Legacy CRM Card X has been hidden from all customers and is ready to be deleted. Delete the card through the API: `https://developers.hubspot.com/docs/api-reference/legacy/crm/extensions/crm-cards/delete-crm-card`.
```
Once the card is replaced and users are unable to add the legacy CRM card to views, the [legacy CRM card can be deleted](#delete-unused-legacy-crm-cards).
## Delete unused legacy CRM cards
It is safe to delete a legacy CRM card once a migration is complete. This process will have no impact on end users and they'll automatically have access to the new app card. If not deleted, the legacy CRM card will continue to exist in the backend until it is automatically deleted after the [deprecation of legacy CRM cards on Oct 31, 2026](https://developers.hubspot.com/changelog/deprecating-support-for-classic-crm-cards).
### Delete legacy CRM cards in the projects UI
To delete a migrated card from within HubSpot:
1. In your HubSpot account, navigate to **Development**. Then, click the **name** of your project.
2. At the bottom of the migration banner, click **Review & migrate**.
3. In the right panel, click **Next**.
4. Click to expand the **Previously migrated cards** section.
5. Next to the card's name, click **Delete**. In the dialog box, click **Delete** to confirm.
### Delete legacy CRM cards via API
To [delete a replaced legacy CRM card](/docs/api-reference/legacy/crm/extensions/crm-cards/delete-crm-card), make a `DELETE` request to `/crm/v3/extensions/cards-dev/{appId}/{cardId}?hapikey={developerApiKey}`. Include the `appId` and `cardId` values of the legacy CRM card.
Once the legacy CRM card is deleted, there will no longer be warnings about the legacy CRM card existing in your app.
# Migrate an existing app to the latest version of the developer platform (2026.03)
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-to-the-latest-platform-version
Learn how to migrate an existing app to the latest version of the developer platform.
The latest version of the developer platform, `2026.03`, provides serverless function support for apps with static auth, as well as all of the previous functionality released on version `2025.2`. This includes apps managed via projects, which use a file-based build-and-deploy framework. This framework contains an app's configuration, assets, and other source code.
This guide walks you through how to migrate an existing app to `2026.03`.
Migrating a legacy non-project-based [private app](/docs/apps/legacy-apps/private-apps/overview) to `2026.03` is not currently supported.
## Prerequisites
* Before proceeding with the steps below, confirm that you're on the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli#install-the-latest-version-of-the-hubspot-cli). Version `8.4.0` or above is recommended.
* Review the migration overview for more details on previous app versions, legacy apps, and other important context.
## Determine your migration path
After you've installed the latest version of the CLI and reviewed the migration overview to check your existing app's [version](/docs/apps/developer-platform/build-apps/migrate-an-app/overview#check-your-app-type-and-project-version) follow one of the sections below based on the existing app you want to migrate:
* If you're on version `2025.2` of the developer platform, follow the steps in the [first section](#migrate-from-2025-2) below.
* If you're on version `2023.1`, `2023.2`, or `2025.1`, follow the steps in the [second section below](#migrate-from-an-older-project). This section also covers project-based apps that predate platform versioning.
* If you're migrating a legacy public app that's not managed via an existing project, follow the steps in the [third section below](#migrate-a-legacy-public-app-non-project-based).
## Migrate from 2025.2
If your app is already on version 2025.2 of the developer platform, you'll first need to update the `platformVersion` property in the top-level `hsproject.json` file of your project from `2025.2` to `2026.03`:
```json highlight={4} theme={null}
{
"name": "my_project",
"srcDir": "src",
"platformVersion": "2025.2"
}
```
Once you've updated the `platformVersion` to `2026.03`, run the following command to re-upload the project to your HubSpot account:
```shell theme={null}
hs project upload
```
## Migrate from an older project version
If your app is on version `2023.1`, `2023.2`, `2025.1`, you can automatically migrate to the latest version of the developer platform by running the following command in the working directory of your project:
```shell theme={null}
hs project migrate
```
Follow the prompts to confirm the features that will be migrated from your existing app. After confirming, all components of your project should automatically be migrated to `2026.03`.
If your previous legacy app included serverless functions, and defined environment variables via the `environment` property in your project's `serverless.json` file, these variables will not be automatically migrated over to your new project.
You'll need to redefine the variables as secrets using the `hs secret add` [command](/docs/apps/developer-platform/add-features/serverless-functions/reference#managing-and-referencing-secrets).
## Migrate a legacy public app (non-project-based)
If you have an existing [legacy public app](/docs/apps/legacy-apps/public-apps/overview) that's not managed via a project, you can migrate it automatically by following the instructions below. This process will preserve the original authentication credentials, app features, and installs for the app. No changes are required in your app's backend.
To get started, open a terminal, navigate to the directory where you plan on managing your HubSpot projects, then run the following command:
```shell theme={null}
hs app migrate
```
* You'll be prompted to choose the app you want to migrate. This will create a new project in your local filesystem containing an app, along with its features that represent its currently configured state.
* After selecting the app, you'll be prompted for confirmation of the components that will be migrated. Make sure you fully understand the migration process before confirming to the next step.
* Next, you'll be prompted to provide a project name, a local path to the new project, and any required UIDs.
UIDs should be used as a unique identifier for all of your app’s features. Once a UID is defined for a specific feature, changing or modifying it in subsequent builds will force the platform to recognize it as “new” or different from previous builds, which might not be intended.
* Once you confirm the details for your project, the migration process will automatically perform the following actions as a single operation:
* Create a project for your app in your account's *Projects* page, which includes an app component and any compatible features.
* Convert any existing features of the app to source code files, which you can update to revise their configuration in the future.
* Build and deploy the new project (which will be labeled as *Build #1*), which completes the association between the app and its project. This will preserve the original auth credentials, app features, and installs.
* Download the new project source files to the local directory you specified earlier when prompted in the terminal.
* If an existing project was migrated, move the files from the original `src/` directory to `archive/` and populate `/src/` with the new project source code.
*Build #1* of the new project captures the app's configuration state for all supported features as baseline you can return to. If needed, you can safely revert any future changes to this project (e.g., adding React-based app cards) by redeploying **build #1**.
After migration, features represented in project source files must be managed locally through the project. These features are no longer editable through the app management UI within HubSpot or previous developer APIs. App features that aren't represented in project source files should continue to be managed through their existing APIs.
## Next steps
After you've successfully migrated your app, check out the resources below to start building on version `2026.03`:
* Review the [supported features](/docs/apps/developer-platform/overview#features) on the developer platform.
* Learn about using [serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/overview).
* Review the [app configuration](/docs/apps/developer-platform/build-apps/app-configuration) reference.
* Create an [app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview), [settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page), or [app page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages) using UI extensions.
# Determine your migration path to 2026.03
Source: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/migrate-an-app/overview
Learn how to determine the right migration path for your legacy app to the latest version of the developer platform.
Multiple migration options are available to upgrade an older version of an app to [version 2026.03](/docs/apps/developer-platform/build-apps/create-an-app) of the developer platform, based on the type of your app, whether the app was already using the projects framework, as well as the existing functionality your app is leveraging.
The sections below provide guidance on auditing your app's configuration, along with how to use that information to determine the best option available to migrate your app to the latest version of the developer platform.
## Audit your existing app configuration
Your potential migration path depends on several factors:
* Which version of the projects framework your app is on (if any)
* Whether your app is private or public
* Whether your app is using serverless functions
* The number of HubSpot accounts your app is installed in (if applicable)
### Check your app type and project version
If you're not sure whether your legacy app is public or private, you can check its type in your development settings:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, click **Legacy apps**.
* Locate your app in the list, using the search bar or the **Filter by app type** dropdown menu to filter apps in the list.
* Confirm your app's *Type* and whether it's already on the projects framework.
If your app is using the projects framework, you can also identify the version of the framework your app is using by checking the `platformVersion` field in your project's `hsproject.json` file.
```shell highlight={2} theme={null}
my-project-folder/
└── hsproject.json
...
```
```json highlight={4} theme={null}
{
"name": "my_project",
"srcDir": "src",
"platformVersion": "2026.03"
}
```
* If you're already on version `2026.03`, your app is on the current version and no migration is necessary.
* Apps on version `2025.2` are supported through March 2027, but migrating to `2026.03` allows you to use [serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/overview).
* Versions `2025.1`, `2023.2`, and `2023.1` have all been sunset, but you can still migrate these older apps to `2026.03`.
If your public app doesn't have a `hsproject.json` file, it's not on the projects framework and you can use the `hs app migrate` command, subject to a few considerations outlined in the [diagram below](#review-your-app-migration-options).
Learn more about [platform versioning](/docs/developer-tooling/platform/versioning).
### Confirm serverless function usage
If your app is on the projects framework and you're leveraging [serverless functions](/docs/apps/legacy-apps/private-apps/build-with-projects/serverless-functions), some restrictions currently apply to the migration process.
To check whether your app is actively using serverless functions, confirm both of the following:
* The `src/app/app.functions/` directory exists within your project.
* At least one entry exists in the `appFunctions` property in your `hsproject.json` file.
The tabs below compare the structure of projects using serverless functions before `2026.03` to the latest structure in `2026.03`, along with the corresponding configuration changes:
```json serverless.json theme={null}
{
"appFunctions": {
"functionName": {
"file": "example-function.js",
"secrets": [],
"endpoint": {
"path": "fetch-quote",
"method": ["GET"]
}
}
}
}
```
```json function-with-endpoint-hsmeta.json theme={null}
{
"uid": "app_function",
"type": "app-function",
"config": {
"entrypoint": "/app/functions/FunctionWithEndpoint.js",
"secretKeys": []
}
}
```
### Check public app installs
If your legacy public app isn't on the projects framework, you can migrate using the `hs app migrate` command, subject to whether your app is public and the number of active installs.
To check your public app's number of active installs:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, click **App listings**.
* Locate your app, then check the *Installs* column for the number of installs.
* If your public app is already listed on the marketplace, you can proceed with migration without any prerequisite actions required.
* If your public app isn't listed on the marketplace, but it's installed in at least 10 different HubSpot accounts, you'll need to list your app on the marketplace first, then proceed with migration.
## Review your app migration options
After confirming your app configuration details from the sections above, consult the paths detailed in the tabs below based on whether your app is already on the projects framework:
```mermaid actions={false} theme={null}
flowchart TB
A[Which version of the framework is your app using?] -- 2023.1, 2023.2, or 2025.1 --> B[Is your app using serverless functions?]
A -- 2025.2 --> C[Update platformVersion in hsproject.json to 2026.03 then run hs project upload]
B -- No --> E[Is your app using legacy CRM cards?]
B -- Yes --> G
E -- No --> G[Run hs project migrate]
E -- Yes --> H[Convert to app cards first]
H --> G
```
```mermaid actions={false} theme={null}
flowchart TB
A[Is your app private or public?] -- Private --> B[Migration is not currently supported]
A -- Public --> C[Is your app listed on the marketplace?]
C -- No --> D[How many accounts is your app installed in?]
C -- Yes --> F[Run hs app migrate]
D -- 10 or more --> E[List on marketplace first]
D -- Less than 10 --> F
E --> F
```
## Next steps
After you've reviewed your migration path options above, you can proceed to [migrate your app to 2026.03](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-to-the-latest-platform-version).
If you have an unlisted legacy public app with fewer than 10 installs, [list your app on the HubSpot Marketplace](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) first.
The previous migration guides to 2025.2 for legacy [private apps](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-an-existing-private-app) and [public apps](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-an-existing-public-app) are still available, but it's highly recommended you [migrate to 2026.03](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-to-the-latest-platform-version) to leverage the latest functionality, including support for serverless functions.
# Manage developer seats
Source: https://developers.hubspot.com/docs/apps/developer-platform/developer-seats
Learn how to manage developer platform access using developer seats
To streamline access and permissions to developers within accounts with seat-based pricing, you can assign developer seats to users in your account. This seat type provides dedicated access to the developer platform without needing to use one of your [paid Core seats](https://knowledge.hubspot.com/account-management/manage-seats#seat-types) or other seat types.
Note that if a user has already been assigned a seat, you can't assign a developer seat in tandem with the existing seat. If you want to keep the user's existing seat, you can always grant them the *Developer tools access* [permission](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#settings-access).
Learn more about seats on HubSpot's [Knowledge Base](https://knowledge.hubspot.com/account-management/manage-seats#seat-types).
**Please note:**
* If your account doesn't use seat-based pricing, you'll have to grant a user the *Developer tools access* [permission](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#settings-access) in your account settings, in order for them to access the developer platform.
* By default, all [super admins](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) and [partner admins](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#partner-admin) have access to the developer platform.
## Benefits of Developer seats
Developer seats provide an unlimited number of users access to the developer platform in accounts with seat-based pricing. This is particularly useful for accounts such as free [standard HubSpot accounts](/docs/getting-started/account-types#standard-hubspot-accounts), which are limited to two [Core seats](https://knowledge.hubspot.com/account-management/manage-seats#seat-types).
Developer seats offer the following key benefits:
* **Comprehensive access to the developer platform:** users who are assigned a Developer seat gain full access to the developer platform, including projects, apps, monitoring, and settings to manage HubSpot Marketplace listings.
* **No impact to CRM users:** Developer seats don't count towards your CRM user limitations, which allows you to add developers to your HubSpot account without affecting your total CRM user count or incur additional subscription costs.
## Using developer seats
To grant a user access to the developer platform, they must be assigned a Developer seat in your HubSpot account.
### Invite a new user with a Developer seat
If you're a [Super admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) in a HubSpot account that's opted into the new developer platform, you can invite new users with a developer seat:
* In your HubSpot account, click the **settings icon** in the main navigation bar.
* In the left sidebar menu, navigate to **Users & Teams**.
* Click **Add user**.
* Under *Assign a seat*, click the **Seat assignment** dropdown menu and select **Developer Seat**.
* Click **Next**, then click **Create user** to send the new user an invite email.
### Change an existing user's seat
To update an existing user to grant them a developer seat:
* In your HubSpot account, click the **settings icon** in the main navigation bar.
* In the left sidebar menu, navigate to **Users & Teams**.
* Click the **Seats** tab.
* Hover over a user, then click **Change seat**.
* In the right panel, click the **Seat** dropdown menu, then select **Developer Seat**.
* Click **Save**.
### Access the developer platform
Users with developer seats can navigate to the developer platform by clicking **Development** in the navigation menu in their HubSpot account. This section provides access to:
* **Overview:** review recent activity and onboarding content.
* **Projects:** access and manage your uploaded projects.
* **Legacy apps:** review any previously created [private](/docs/apps/legacy-apps/private-apps/overview) or [public](/docs/apps/legacy-apps/public-apps/overview) apps created before you opted into the new developer platform.
* **Monitoring:** track the performance and health of your apps.
* **Keys:** manage your Developer API keys and Personal Access keys.
* **Test accounts:** set up and manage test environments for your apps and projects.
* **App listings:** manage your app listing in the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps).
* **Documentation:** access technical documentation about the platform and its associated tools.
# Developer platform
Source: https://developers.hubspot.com/docs/apps/developer-platform/overview
Learn how to create apps on the HubSpot developer platform.
HubSpot's new developer platform, built and managed via projects, provides multiple advantages over legacy private and public apps, including a new file-based build-and-deploy framework. This framework contains an app's configuration, assets, and other source code.
## Continued support for legacy apps
Although building apps on the developer platform is recommended to take advantage of the latest app features, legacy [private apps](/docs/apps/legacy-apps/private-apps/overview) and [public apps](/docs/apps/legacy-apps/public-apps/overview) are still fully supported and actively maintained by HubSpot.
* All [REST APIs](/docs/api-reference/latest/overview) are available to query via a private app access token or public app OAuth token.
* Unless otherwise noted, any previous extensions you've built using legacy apps are also still supported.
Developer platform apps (v2025.2 and v2026.03) are created and deployed via the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). If you're less familiar with using the CLI and you only need an access token for a few HubSpot APIs, you may want to create a [legacy private app](/docs/apps/legacy-apps/private-apps/overview).
To review any previously created private or public apps in your HubSpot account, navigate to **Development**, then click **Legacy Apps**.
You can also [migrate](#migration-guides) an existing legacy app to the latest version of the developer platform.
## Create an app
The [app creation guide](/docs/apps/developer-platform/build-apps/create-an-app) walks you through how to get up and running with a proof-of-concept app that uses a boilerplate example project and schema definition. You can then upload the associated configuration files to a project in your developer account, and test out an install using a developer test account.
The app creation article linked above provides a full setup guide to customize and deploy a new app using the `hs project create` command.
If you're new to building apps on HubSpot, check out the [quickstart guide](/docs/getting-started/quickstart) that will get you up and running with a demo app using the streamlined `hs get-started` command.
## Migration guides
If you have an existing app built using a project on the old version of the developer platform, you can migrate the app to the new version of the platform by consulting the resources below:
* [Migration overview](/docs/apps/developer-platform/build-apps/migrate-an-app/overview)
* [Migrate an app to 2026.03](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-to-the-latest-platform-version)
## App configuration
Whether you're starting from scratch with the [quickstart guide](/docs/getting-started/quickstart), creating an app using the [full setup guide](/docs/apps/developer-platform/build-apps/create-an-app), or migrating an existing legacy app, you can consult the [app configuration](/docs/apps/developer-platform/build-apps/app-configuration) reference article for details on all available fields in your app's top-level `app-hsmeta.json` schema file.
## Features
Once you've got your new app up and running, you can add features such as app cards and webhooks. The features available to an app depend on the app's [distribution](/docs/apps/developer-platform/build-apps/app-configuration#distribution) and [authentication](/docs/apps/developer-platform/build-apps/app-configuration#authentication) configuration, as shown in the table below.
# HubSpot Account Types
Source: https://developers.hubspot.com/docs/getting-started/account-types
Learn about the different types of HubSpot accounts and what each one is used for.
There are several types of HubSpot accounts, each with a distinct purpose. Below, learn about each account type.
## Standard HubSpot accounts
A standard HubSpot account is the most common type of account. It’s where you’ll find all the tools, features, and settings included with your HubSpot plan. It can be free or paid, and is used as your production environment. It also provides a centralized [developer overview](/docs/getting-started/developer-overview) to manage your apps, developer tooling, test accounts, and more.
A standard HubSpot account will have access to all the tools and features included with your plan.
## Developer test accounts
You can create up to 10 test accounts to test apps and integrations without affecting any real HubSpot data. Developer test accounts are free HubSpot accounts with access to a 90-day trial of many enterprise features, with the following limitations:
### Limits
* **Marketing** **Hub:** you can only send marketing emails to addresses of users who you've added to your developer test account.
* **Content Hub:** the number of pages you can create are subject to the following limits:
* **Website pages:** 25
* **Blogging tools:** 1 blog with up to 100 posts
* **Landing pages:** 25
* **Workflows:** a maximum of 100,000 records can be enrolled per day in workflows created in a developer test account. If this daily limit is reached:
* Any additional attempted enrollment beyond this limit will be dropped.
* Users will be informed in app that they reached the daily record enrollment limit, as well as what time the limit will be refreshed.
You can create up to 10 test accounts per standard HubSpot account. Test accounts cannot sync data with other accounts.
### Create a developer test account
To create a developer test account in your HubSpot account:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Testing** > **Test Accounts**.
* In the upper right, click **Create developer test account**.
* Enter an **account name**, then click **Create**.
To access and manage your developer test accounts:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Testing** > **Test Accounts**.
* Click the **name** of the account to enter the account.
* To delete the account or renew its product trial periods, click **Actions**, then select **Renew trials** or **Delete**.
**Please note:**
Developer test accounts will expire after 90 days if no API calls are made to the account. You can either manually renew the account from the *Test accounts* page in HubSpot, or by making an API call to the account.
When renewing via API call, you must use an [OAuth token](/docs/api-reference/legacy/authentication/manage-oauth-tokens) generated from an application in the same developer account as the test account you want to renew. In addition, renewals must be done no more than 30 days before the test account's expiration date.
You can also create configurable test accounts via the [HubSpot CLI](/docs/developer-tooling/local-development/configurable-test-accounts).
## Sandbox accounts
Sandbox accounts allow you to test out changes without impacting your standard account. Learn more about the different types of sandbox accounts in the sections below.
### Standard sandbox accounts
If you have an *Enterprise* subscription, you can create a standard sandbox account that provides a safe and secure environment where you can test new workflows, integrations, website pages, and other important changes without impacting anything in your standard account. These sandboxes copy the structure of your standard account.
A banner will be displayed at the top of your sandbox to indicate that you are in a sandbox portal. You can use the account picker to return to your production account.
Learn more about standard sandbox accounts on [HubSpot's Knowledge Base](https://knowledge.hubspot.com/account-management/set-up-a-hubspot-standard-sandbox-account).
### CMS sandbox accounts
CMS sandboxes are free accounts intended for building and testing website changes without impacting your standard account or live website. Similar to developer accounts, CMS sandbox accounts are not connected to your standard HubSpot account.
You can [create a CMS sandbox account for free](https://offers.hubspot.com/free-cms-developer-sandbox).
CMS sandboxes don’t have a banner, but they only have access to HubSpot’s free tools and ***CMS Hub*** *Enterprise*, minus the ability to connect a domain.
### Development sandbox accounts
If you have an *Enterprise* subscription, you can create a development sandbox account through the CLI for local development.
A banner will appear at the top of your sandbox to indicate that you are in a sandbox account. You can use the account picker to return to your production account.
Development sandbox accounts can only be managed via the HubSpot CLI. Check out the available commands in [this reference article](/docs/developer-tooling/local-development/hubspot-cli/project-commands#create-and-use-development-sandboxes).
## Marketplace provider accounts
Marketplace Provider accounts are intended for creating and managing [Template Marketplace listings](/docs/cms/marketplace/template-guidelines) and transactions. To get started selling on the Template Marketplace, [create a Template Marketplace provider account](https://app.hubspot.com/signup-hubspot/asset-provider). If you're a [HubSpot Partner](https://www.hubspot.com/partners), you already have Marketplace Provider functionality in your Partner account.
A Marketplace Provider account can be identified by a **Marketplace** menu item in the main navigation bar.
## Legacy developer accounts
Developer accounts are a type of legacy account used for creating and managing legacy apps. As of August 28, 2025, all standard HubSpot accounts now have access to the same functionality in the [development overview](/docs/getting-started/developer-overview) section in the main navigation.
**Please note:** as of March 9, 2026, all legacy developer accounts were migrated to standard HubSpot accounts. None of your existing apps, developer test accounts, or keys were impacted by the change and are fully [accessible](/docs/getting-started/developer-overview#navigate-to-the-development-overview-in-your-account) after your account was converted to the new experience.
# Development overview
Source: https://developers.hubspot.com/docs/getting-started/developer-overview
Learn about the development overview page in HubSpot.
The development overview in your HubSpot account provides a centralized page to manage apps, authentication details, app monitoring, app marketplace listings, and more.
Super admins, partner admins, and users with either a [developer seat](/docs/apps/developer-platform/developer-seats) or the *Developer tools access* [permission](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#settings-access) can access the development overview page.
**Please note:** as of March 9, 2026, all legacy developer accounts were migrated to standard HubSpot accounts, which provides access to the latest development overview page. None of your existing apps, developer test accounts, or keys were impacted by the change and are fully accessible after your account was converted to the new experience.
## Navigate to the development overview in your account
To access the development overview directly in your HubSpot account:
* In your HubSpot account, navigate to **Development**.
* All your projects, app listings, and other details are available in the left sidebar menu:
* **Overview**: review API usage across your apps, recently updated projects, build & deploy statuses, and relevant product updates.
* **Projects**: drill down into your individual projects and their associated apps, including app [management](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot) tools.
* **Legacy apps**: review any legacy [private](/docs/apps/legacy-apps/private-apps/overview) or [public](/docs/apps/legacy-apps/public-apps/overview) apps in your account.
* **Design manager**: navigate directly to the [design manager](https://knowledge.hubspot.com/design-manager/a-quick-tour-of-the-design-manager) tool in HubSpot.
* **Monitoring**: check API usage and logging for your apps in your account.
* **Keys**: locate details for your [personal access key](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#personal-access-keys) and [developer API key](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#developer-api-keys).
* **Domain**: add or review the [verified domain](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#verified-domains) for your apps.
* **App listings**: create or review existing [app listings](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) on the HubSpot Marketplace.
To create a new app on the latest version of the developer platform, check out the [quickstart guide](/docs/getting-started/quickstart), or learn more about the new developer platform [here](/docs/apps/developer-platform/overview).
# Introduction to HubSpot's developer platform
Source: https://developers.hubspot.com/docs/getting-started/introduction
Learn how to get started developing on HubSpot.
Whether you’re a new HubSpot developer or looking to expand your skills, this page provides the different development routes available, as well as the specific tooling and accounts you’ll need to achieve your goals.
## The advantages of a connected platform
The foundation of every HubSpot account is the [CRM](/docs/guides/crm/understanding-the-crm) (Customer Relationship Management) platform, a database of business relationships and processes. HubSpot offers different routes for external developers and partners to work with the CRM so they can create extra value for HubSpot end users. This includes:
* Creating [apps](/docs/apps/developer-platform/overview) with HubSpot’s [APIs](/docs/api-reference/latest/overview) to sync data between HubSpot and external platforms.
* Customizing the CRM with [UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview). Developers can use React to build flexible custom cards that integrate seamlessly with HubSpot.
* Building custom websites using the [HubSpot CMS](/docs/cms/start-building/introduction/overview) (Content Management System). In addition to a full suite of content management tools, the CMS fully integrates with the CRM. This makes campaign launches, capturing leads and ongoing customer communications easier to manage.
* Leveraging powerful [developer tooling](/docs/developer-tooling/overview) to maximize your productivity while building on HubSpot.
Along with HubSpot's developer documentation, learn more about how the corresponding in-app tools work by checking out the [HubSpot Knowledge Base](https://knowledge.hubspot.com).
The sections below provide links to help you start building, as well as other resources to check out if you're still exploring HubSpot.
## Start Building
Take a tour of developer features in your account and jump into creating your first app.
Manage your apps and other developer features in your HubSpot account.
Create your first app using the HubSpot CLI.
Review the various account types available to test and install your apps.
Authenticate API requests to retrieve and manage HubSpot data.
Discover the features available on HubSpot's developer platform.
Install HubSpot's local development MCP server for AI-assisted coding in your IDE.
## Other resources
Explore supplemental developer resources in the HubSpot developer ecosystem.
Watch demos and discussions of the latest features on the HubSpot Developers YouTube channel.
Read the latest announcements on HubSpot Developer Blog.
Join the HubSpot developer community.
# Quickstart
Source: https://developers.hubspot.com/docs/getting-started/quickstart
Get up and running on the latest version of HubSpot's developer platform.
Get started with app development by building and deploying a simple boilerplate app.
If you don't have a HubSpot account yet, [create an account](https://app.hubspot.com/signup-hubspot/crm?intent=developer) before proceeding with the steps below.
## Set up your local environment
Before getting started, install the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). In a terminal window, run the following command:
```shell theme={null}
npm install -g @hubspot/cli
```
After installing the latest version of the HubSpot CLI, it's recommended you run `hs account auth` to authenticate your HubSpot account:
* Follow the prompts to generate a Personal Access Key in your account. Your personal access key will then be passed to the terminal.
* In the terminal, you'll then be asked whether you want to set the account as your default. Setting an account as default means CLI commands will automatically target it without needing the `--account` flag. You can change your default at any time with the [`hs account use` command](/docs/developer-tooling/local-development/hubspot-cli/commands/account-commands#set-default-account).
## Create and upload a project
With the CLI installed, run `hs get-started` to initialize your project.
```shell theme={null}
hs get-started
```
You'll be greeted with a welcome message, then you can begin setting up your project and app.
* Following the prompts, select **App**.
* Give your project a **name**.
* Set your project's **local directory** (by default this will be your current working directory).
* Upload your project to HubSpot to initialize the first build and deploy.
## Install the app and set up the card
After your project builds and deploys, you'll be prompted to navigate to your HubSpot account to walk through installing and previewing your app.
* In the terminal, press **Enter** to confirm that you want to install the app in your account. A browser window will open to the app installation page.
The "unverified app" warning on the installation page is expected. Apps that haven't been submitted to and reviewed through HubSpot's app marketplace verification process display this warning to protect users from installing apps from unknown developers. Since this is an app you built, it's safe to proceed.
* Select the **checkbox** to confirm that you want to install the app, then click **Connect App**.
* On the installation success page, click **Continue to manage App Card view**.
You'll then be redirected to the app card settings page, where you'll walk through configuring the card's display location.
* Click **Manage locations**.
* In the right sidebar, select the **checkbox** next to *Get Started App Card* to add the card to the middle column of the default contact record view.
* Click **Save** to save your changes and close the sidebar.
* In the *Get Started App Card* section, click **Complete card setup** to finish the walkthrough.
## Run the local development server
With the app installed and card location set, use the command below to switch back to the terminal to navigate into your project directory and start the local development server (`hs project dev`). The development server command will automatically check for dependencies and install them as needed.
```shell theme={null}
cd your-project-name
hs project dev
```
When prompted, select the account you want to use for the testing session. You can select an account you've already connected to the CLI, or create a new test or sandbox account by following the additional prompts. Learn more about [the different account types](/docs/getting-started/account-types).
Once the local development server starts, a browser window will open to the *Local Dev Panel* page in HubSpot. Along with the actions you can perform on this page, it also displays high-level information about the project, such as the number of the latest build and deployed build. The *last updated* time will update any time you make a change to your local project files.
* Click **Preview** next to *Get Started App Card*.
* On the contacts index page, click the **name** of a contact.
* In the middle column, scroll down in the *Overview* tab to see your app card. A *Developing locally* badge will display next to the app card name. This badge is only visible to you while the development server is running.
The local development server will automatically pick up any changes you make to frontend React/TypeScript files (e.g., `get-started-app-card.tsx`). Any changes made to other file types, such as `.json` configuration files, will need to be uploaded to HubSpot before they can be picked up. You can either do this by running the `hs project upload` command in the terminal, or by clicking the **Upload and build** button on the *Local Dev Panel* page.
To test this out, make a change to the app card's `.tsx` file, then save your change. For example, the code block below includes changes to the `EmptyState` component's title text, layout, and spacing.
```tsx highlight={27-29} theme={null}
import {
CrmContext,
EmptyState,
ExtensionPointApiActions,
Link,
List,
Text,
} from '@hubspot/ui-extensions';
import { hubspot } from '@hubspot/ui-extensions';
interface ExtensionProps {
context: CrmContext;
actions: ExtensionPointApiActions<'crm.record.tab'>;
}
hubspot.extend<'crm.record.tab'>(({ context, actions }: ExtensionProps) => (
));
const Extension = ({ context, actions }: ExtensionProps) => {
const appCardDocsLink =
'https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensibility/app-cards/overview';
return (
<>
Add a layer of UI customization to your app by including app cards
that can display data, allow users to perform actions, and more. Visit
the app card documentation for
more info, or check out the following links to get inspired:
📖 Explore our library of UI components
📖 Look at the Marketplace collection of apps that contain app cards
▶️ Connect with developers on #ui-extensions channel on developer
Slack community
>
);
};
```
You can learn more about each of the components used in the quickstart app card, including usage examples and available parameters, in the docs below:
* [EmptyState](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/empty-state)
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
* [List](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/list)
* [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link)
## Next steps
Now that you've successfully deployed your quickstart app, check out the following resources to continue building on HubSpot's developer platform:
* [App configuration](/docs/apps/developer-platform/build-apps/app-configuration)
* [Manage apps in HubSpot](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot)
* [Create app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card)
* [Fetching data for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data)
* [UI extension components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview)
# HubSpot Developer Documentation
Source: https://developers.hubspot.com/docs/index
Bring Productivity To Life Through Code
Search or ask...
Welcome to the HubSpot Developer Documentation. Build custom CRM and data-driven website experiences on HubSpot. Follow the steps below to get started, or jump directly into a quickstart that fits your build.
1. Install the HubSpot CLI globally
2. Authorize your HubSpot account
3. Start building
Create your first app or template in minutes
Get to know HubSpot's development platform and tools
Integrate with HubSpot's APIs
Learn about new changes and releases
Explore educational developer content
Join the HubSpot Community
# Add telemetry
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/add-telemetry
Learn how to set up telemetry for your app so you can send logging information to an external service.
You can set up telemetry for your app, which acts as a log sink to pipe out log data to an external observability provider. [Honeycomb](https://www.honeycomb.io/) and [Sentry](https://sentry.io/welcome/) are supported as third-party providers.
You can also specify a custom backend with an endpoint URL pointed at a specific backend provider of your choice. Note that using a custom backend provider for app telemetry is currently in beta, and is subject to HubSpot's Developer Terms and Developer Beta Terms.
## Create and set up telemetry component files
In your project's `src/app/` directory, create a `telemetry/` directory, then add a `telemetry-hsmeta.json` configuration file within it.
```shell theme={null}
└── src/
└── app/
└── telemetry/
└── telemetry-hsmeta.json
```
Edit the `telemetry-hsmeta.json` file to configure your provider, log level settings, and more. An example file is provided below, along with a table that details each of the available fields.
**Please note**: by default, all log types and levels will be synced with your provider via the `logTypes` and `logLevels` fields, which may result in a very high volume of data being sent. It's strongly recommended you start with configuring only the log types you're interested in, and setting the log levels to filter for errors only.
### telemetry-hsmeta.json
The code block below demonstrates an example `telemetry-hsmeta.json` file configured for Sentry:
```json theme={null}
{
"uid": "telemetry",
"type": "telemetry",
"config": {
"providerType": "SENTRY",
"datasetName": "my-app-telemetry",
"logTypes": [
"API_CALL",
"EXTENSION_LOG",
"EXTENSION_RENDER",
"DATA_FETCH",
"ENDPOINT_FUNCTION",
"APP_FUNCTION",
"WEBHOOKS",
"APP_SETTINGS",
"CRM_LEGACY_CARD"
],
"logLevels": ["ERROR", "WARN", "INFO"]
}
}
```
| Field | Type | Description |
| ------------------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid` | String | A unique identifier for your telemetry configuration. This can be set to any value, but it will appear in your project settings in your account, so it should be different from other `uid` values of other app components. |
| `type` | String | The type of component, which should be `telemetry` in this case. |
| `config` | Object | An object containing the configuration details. See the sub-properties listed in the rows below. |
| `providerType` | String | The name of your third-party telemetry provider, which can be `SENTRY`, `HONEYCOMB`, or you can set this value to `CUSTOM_BACKEND` to propagate data to a specific backend provider of your choice.
If you opt for a custom backend provider, you should also set the `endpointUrl` field detailed below. |
| `datasetName` | String | A label that will be associated with your log data, if your provider supports that option. |
| `logTypes` | Array | A list of log types sent to your external provider. By default, all logs are propagated to your provider. The available log types include:
`API_CALL`
`EXTENSION_LOG`
`EXTENSION_RENDER`
`DATA_FETCH`
`ENDPOINT_FUNCTION`
`APP_FUNCTION`
`WEBHOOKS`
`APP_SETTINGS`
`CRM_LEGACY_CARD`
. |
| `logLevels` | Array | A list of severity levels to filter logs by. Supported log levels are: `"ERROR"`, `"WARN"`, `"INFO"`, `"TRACE"`, and `"DEBUG"` |
| `endpointUrl` | String | If you specified a custom backend via the `CUSTOM_BACKEND` value as your `providerType`, use this field to provide the URL to your custom backend provider, following your provider's documentation to get the URL that's specific to your account.
Omit this field if you're using Sentry or Honeycomb as your telemetry provider. |
## Add external authentication as a secret via the CLI
In addition to creating the `telemetry-hsmeta.json` configuration file above, you'll also need to add a secret that corresponds to the authentication key for your provider:
* If you're using Sentry, you'll add the DSN (Data Source Name) as a secret.
* If you're using Honeycomb, you'll add an API key as a secret.
### Locate a Sentry DSN
If you're using Sentry, follow the steps below to get your DSN:
* Log into your [Sentry account](https://sentry.io/welcome/auth/login/).
* Navigate to your project's settings.
* Under the *Client Keys* or *DSN* section, you'll find a unique DSN for your project. It should resemble the following:
```
https://sentry-key@sentry-identifier.ingest.us.sentry.io/project-id
```
### Locate a Honeycomb API key
If you're using Honeycomb as your external observability provider, an API key is used to authenticate and forward data. This API key is associated with your specific Honeycomb account and project.
To generate a Honeycomb API key:
* Log into your [Honeycomb account](https://ui.honeycomb.io/login).
* Navigate to your project settings.
* Find the *API Keys* section and generate a new key.
* Copy the generated API key.
### Add a secret using the HubSpot CLI
Once you've obtained either your Sentry DSN or Honeycomb API key, run the following command to add the value as a secret. When prompted for the name of the secret, you must use `TELEMETRY_SECRET` for log data to be synced correctly.
```shell theme={null}
hs app secrets add
```
# Create an agent tool (BETA)
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/agent-tools/create-an-agent-tool
Follow this tutorial to build an agent tool, which is a custom workflow action tailored to AI agents.
In HubSpot, you can create an AI agent that will perform various actions based on your instructions. For example, you can create an agent to send an email to you every morning with notes about the day’s upcoming meetings.
To perform tasks, agents rely on agent tools. Tools are like functions in a programming language: they have parameters you pass into the tool, and the tool returns an output. Tools in HubSpot are similar to the concept of tools in MCP (Model Context Protocol). An agent tool will package API calls, LLM steps, and other supporting context to enable the AI to do the job. Tools are designed to perform specific, well-defined tasks, such as querying a database, performing CRUD (Create, Read, Update, Delete) operations, or using generative AI to summarize content.
In implementation terms, an agent tool is an enhanced version of a custom workflow action, which you’ll build using HubSpot’s developer project framework. This tutorial will guide you through how to start building an agent tool.
## Prerequisites
Before getting started, you'll need to:
* Install the latest version of the HubSpot CLI by running `npm install -g @hubspot/cli`. If you've already installed the CLI, you can update to the latest version by running `npm install -g @hubspot/cli@latest`.
* Create a [developer test account](/docs/getting-started/account-types#developer-test-accounts) from within the developer account that's opted in to the beta.
* Authenticate the test account with the CLI by running the [`hs account auth` command](/docs/developer-tooling/local-development/hubspot-cli/reference#authenticate-an-account) in your terminal.
## Build and deploy an agent tool
If you're starting from scratch, you'll first need to create a new project. Alternatively, if you'd like to use an existing project (project version `2025.2` or `2026.03` required), [skip to the next section](#add-an-agent-tool).
To create a new project:
1. In the terminal, run the command below to create a new project from one of the boilerplate quickstart templates.
```shell theme={null}
hs project create
```
2. Follow the terminal prompts to configure the project name and location, then select a template. Several template options are provided depending on how you plan to [distribute](/docs/apps/developer-platform/build-apps/app-configuration#distribution) your app. For the purposes of this tutorial, select the **Getting started project with marketplace app** template.
3. The project template will then be downloaded to your working directory where you can view its contents.
This project template includes all the files needed to upload a project with an app that includes a few example app features. These features include a boilerplate [app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card), [webhook](/docs/apps/developer-platform/add-features/configure-webhooks), and [workflow action](/docs/apps/developer-platform/add-features/custom-workflow-actions). For the purposes of this tutorial, these components aren’t needed, but may be helpful to review should you want to experiment with them later.
* If your project doesn't have one yet, create a `workflow-actions/` directory in `src/app/`.
* In the `workflow-actions` directory, create a new JSON file for the tool configuration. The file can have any name, but must end with `-hsmeta.json` (e.g., `my-agent-tool-hsmeta.json`).
* Build out the action configuration using the [agent tools reference documentation](/docs/apps/developer-platform/add-features/agent-tools/reference). Be sure to include the `supportedClients` array along with its `client`, `toolType`, and `llmConfig` fields, as shown in the example code below.
```json Highlight={7-15} theme={null}
{
"uid": "agent_tool_action",
"type": "workflow-action",
"config": {
"actionUrl": "https://example.com/api-endpoint",
"isPublished": false,
"supportedClients": [
{
"client": "AGENTS",
"toolType": "GET_DATA",
"llmConfig" : {
"actionDescription": "Use this tool to fetch data from an external API."
}
}
],
"inputFields": [
{
"typeDefinition": {
"name": "message",
"type": "string",
"fieldType": "textarea"
},
"supportedValueTypes": ["STATIC_VALUE"],
"isRequired": false
}
],
"labels": {
"en": {
"actionName": "My custom agent tool",
"actionDescription": "A description of the tool.",
"actionCardContent": "Send a notification",
"inputFieldLabels": {
"message": "Notification Message"
},
"inputFieldDescriptions": {
"message": "Enter the message to be sent in the notification"
}
}
},
"objectTypes": ["CONTACT"]
}
}
```
As you build your tool, keep the following in mind:
* When actively developing, you should not set your input fields to required, as required fields cannot be updated or removed once uploaded.
* Requests to public endpoints will be made as `POST` requests.
* Unlike custom workflow actions, you cannot include functions in agent tools.
* The agent does not have access to CRM data unless explicitly given tools to get that data.
* The agent can't request information from the user after it's invoked if given insufficient information to complete the task.
When you're ready to upload to your test account, save your files, then run `hs project upload`. If you started with a new project, you'll be prompted press `y` to create the project in the account.
## Testing agent tools in HubSpot
There are multiple aspects of your agent tool that you'll want to test before making it available to users. HubSpot provides two primary methods for testing: the workflows tool, and the developer tool testing agent.
### Test with workflows
It's recommended to start testing with a workflow to evaluate the core logic of your tool. Using workflows for testing is especially useful for testing input and output behavior, such as providing both correct and incorrect inputs to see what the result is.
This is the same type of testing you would perform for a custom workflow action, as tools are custom workflow actions with additional fields for agent configuration.
To test with workflows:
1. [Create a workflow](https://knowledge.hubspot.com/workflows/create-workflows).
2. Add your tool as a workflow action.
3. Ensure you're passing the inputs and outputs you want to pass, then execute the workflow.
4. Iterate on your back-end code to ensure the logic is properly handling the inputs and returning the expected outputs.
### Test with the developer tool testing agent
Using HubSpot's [Developer Tool Testing Agent](https://ecosystem.hubspot.com/marketplace/listing/developer-tool-tester-agent), you can test how your tool works in the context of an agent. Testing with this method will help you to better understand the LLM side of your agent tool and how it will behave for end-users prompting the agent.
To use the Developer Tool Testing Agent:
* Navigate to the [Developer Tool Testing Agent](https://ecosystem.hubspot.com/marketplace/listing/developer-tool-tester-agent) in the HubSpot Marketplace.
* Click **Sign in to add** to navigate to the install page in your account. Once in your account, click **Add** to install the agent.
* In the pop-up banner, click **Configure**. Alternatively, you can click **Open** to open the agent page, then click **Configure** in the top right.
* In the *What this agent can access* section, click **Add tool**.
* In the right sidebar, select your **agent tool**.
With your agent tool added, you can now begin testing your tool by adding a prompt to the *Your message* field, then clicking \*\*Test **agent**.
As needed, you can click **Add extra instructions** to add to the existing agent instructions. For example, you may want to instruct the agent to perform in specific ways or make it easier to repeatedly test something you're trying to improve.
The aspects you should test with this agent are:
* **Tool name and description clarity:** test whether the LLM correctly understands when to use your tool based on its name and description, especially when compared to other default or custom tools.
* **Direct vs. indirect prompts:** test both direct commands (e.g., "Use Tool X to do Y") and more abstract, goal-oriented prompts to see if the LLM can correctly infer the user's intent. Ideally the user doesn't need to tell the agent to use a tool, the agent can intuit what it needs to do and just execute.
* **Parameter extraction:** verify that the agent correctly identifies and extracts all required parameters from natural language prompts. The testing agent is designed to report exactly what parameters it passed to help with this verification.
* **Sequential operations:** test scenarios where your tool must work in a sequence with other tools. This includes checking if the agent can correctly pass the output from one tool as the input for the next.
With each run, the agent will return its reasoning and the data it's passing to the tools to help you troubleshoot. As you identify, issues, you can to update your `llmConfig` and your `labels` properties in your tool's `hsmeta.json` file to give the LLM more clarity and specificity into what your tool does, and how to provide inputs to it.
# Agents and tools overview (BETA)
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/agent-tools/overview
Learn how you can build agent tools to customize the HubSpot agent experience.
In HubSpot, you can create an AI agent that will perform various actions based on your instructions. For example, you can create an agent to send an email to you every morning with notes about the day’s upcoming meetings.
To perform tasks, agents rely on agent tools. Tools are like functions in a programming language: they have parameters you pass into the tool, and the tool returns an output. Tools in HubSpot are similar to the concept of tools in MCP (Model Context Protocol). An agent tool will package API calls, LLM steps, and other supporting context to enable the AI to do the job. Tools are designed to perform specific, well-defined tasks, such as querying a database, performing CRUD (Create, Read, Update, Delete) operations, or using generative AI to summarize content.
In implementation terms, an agent tool is an enhanced version of a custom workflow action, which you’ll build using HubSpot’s developer project framework.
## Key considerations for agent tools
When building agent tools, keep in mind that they serve two audiences:
* **AI agents** that need clear, unambiguous descriptions to understand when and how to use the tool
* **Human users** (when the tool is also supported in workflows, and also in the "add tool" interface when configuring an agent) who need intuitive labels and descriptions for manual configuration.
If your agent tool is also supported in workflows, human users will interact directly with your tool's labels and field descriptions when setting up workflow actions. Ensure these are clear and user-friendly for manual configuration.
Note the following limits at this time of the beta:
* The agent does not have access to CRM data unless explicitly given tools to get that data.
* The agent can't request information from the user after it's invoked if given insufficient information to complete the task.
## Building tools
To build a tool, check out the [Create an agent tool guide](/docs/apps/developer-platform/add-features/agent-tools/create-an-agent-tool) along with the [reference documentation](/docs/apps/developer-platform/add-features/agent-tools/reference), which includes information about configuration options, best practices, and request validation.
## Testing your agent tools
Agent tools should be tested in two phases:
1. **Core logic testing** using HubSpot workflows to validate the tool's functionality with correct and incorrect inputs
2. **LLM integration testing** using the Developer Tool Testing Agent to verify that the agent correctly understands when and how to use your tool
Learn more about testing strategies in the [Create an agent tool guide](/docs/apps/developer-platform/add-features/agent-tools/create-an-agent-tool#testing-your-agent-tool).
# Agent tools reference (BETA)
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/agent-tools/reference
Learn how to build agent tools, which are custom workflow actions that can be used by AI agents.
HubSpot agents are AI-powered assistants that users can chat with to perform tasks. Each agent includes a series of actions, called tools, that they'll use according to the user's instructions. As a developer, you can create custom agent tools to perform specific, well-defined tasks depending on the agent's intended use case.
Behind the scenes, tools are custom workflow actions that are configured to be available in the agent context. Tools can be used across multiple agents, and can be configured to work in both agents and workflows.
At a high level, creating an agent tool consists of:
* Adding a workflow action component to an app by including a `workflow-actions` [directory in the project](#project-setup), along with a `*-hsmeta.json` configuration file for the tool. Each tool and workflow action you create should have its own `*-hsmeta.json` configuration file.
* [Configuring the action](#agent-tool-definition) to be available in AI agents via the `supportedClients` field.
As you build your tools, you should also keep in mind a set of [best practices](#best-practices) to ensure higher quality performance.
Note the following limits at this time of the beta:
* The agent does not have access to CRM data unless explicitly given tools to get that data.
* The agent can't request information from the user after it's invoked if given insufficient information to complete the task.
## Project setup
To build tools, your `hsproject.json` must have its `platformVersion` set to `2025.2` or `2026.03`. This version is set automatically on all of the [boilerplate project types](/docs/apps/developer-platform/build-apps/create-an-app), but will need to be manually updated for projects on older versions.
Note that, when upgrading an older project to version `2025.2` or `2026.03` you'll need to adhere to the new `*-hsmeta.json` configuration file standards for your [app configuration](/docs/apps/developer-platform/build-apps/app-configuration) and its features.
Agent tools and custom workflow actions are both contained within the app's `workflow-actions` directory.
```shell theme={null}
myProject
└── src/
└── app/
└── workflow-actions/
└── agent-tool-hsmeta.json
```
## Agent tool configuration
Configuration options for agent tools are similar to [custom workflow actions](/docs/apps/developer-platform/add-features/custom-workflow-actions#custom-workflow-action-definition), with a few notable differences:
* The `supportedClients` field must include the `AGENTS` client, along with other agent-specific fields, such as `toolType`.
* Functions are not supported in agent tools.
```json theme={null}
{
"uid": "agent_tool_action",
"type": "workflow-action",
"config": {
"actionUrl": "https://example.com/api-endpoint",
"supportedClients": [
{
"client": "AGENTS",
"toolType": "TAKE_ACTION",
"llmConfig": {
"actionDescription": "Use this tool to fetch data from an external API."
}
}
],
"inputFields": [
{
"typeDefinition": {
"name": "message",
"type": "string",
"fieldType": "textarea"
},
"supportedValueTypes": ["STATIC_VALUE"],
"isRequired": true
},
{
"typeDefinition": {
"name": "priority",
"type": "enumeration",
"fieldType": "select",
"options": [
{
"value": "high",
"label": "High Priority"
},
{
"value": "normal",
"label": "Normal Priority"
},
{
"value": "low",
"label": "Low Priority"
}
]
},
"supportedValueTypes": ["STATIC_VALUE"],
"isRequired": true
}
],
"outputFields": [
{
"typeDefinition": {
"name": "errorCode",
"type": "string",
"externalOptions": false
}
},
{
"typeDefinition": {
"name": "apiResponse",
"type": "string",
"externalOptions": false
}
}
],
"labels": {
"en": {
"actionName": "My custom agent tool",
"actionDescription": "A description of the tool.",
"actionCardContent": "Send {{priority}} priority notification",
"inputFieldLabels": {
"message": "Notification Message",
"priority": "Priority Level"
},
"inputFieldDescriptions": {
"message": "Enter the message to be sent in the notification",
"priority": "Select the priority level for this notification"
},
"outputFieldLabels": {
"apiResponse": "API Response",
"errorCode": "Error Code"
}
}
},
"objectTypes": ["CONTACT"]
}
}
```
Fields marked with \* are required
| Field | Type | Description |
| ----------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uid`\* | String | An internal unique identifier for the agent tool. |
| `type`\* | String | The type of component, which should be `workflow-action` in this case. |
| `actionUrl`\* | String | The URL that the tool will make a `POST` request to. The URL must be a publicly accessible endpoint and cannot be a serverless function defined within the developer project. |
| `supportedClients`\* | Array | An array of objects that specifies the clients that support the action. Each object in the array should have a `client` key with a string value indicating the client type. Values include:
`WORKFLOWS`: enables the action for workflows.
`AGENTS`: enables the action for agents.
This array should also contain the `toolType` and `llmConfig.actionDescription` fields. |
| `toolType`\* | String | The category of tool functionality. Can be one of:
`GET_DATA`: retrieves information from HubSpot or external sources.
`GENERATE`: generates content, summaries, analyses, or suggestions based on the provided inputs.
`TAKE_ACTION`: performs an action, such as CRM actions like creating notes, assigning tasks to owners, or actions in external systems like creating tasks in an external project management system. By default, this type of action requires users to review the output before approving the tool execution. This setting can be changed in the Agent editor after the tool has been added to the agent.
|
| `llmConfig.actionDescription`\* | String | The `llmConfig` object contains the `actionDescription` field, which describes the tool to the AI agent. This allows the agent to determine how and when to invoke the tool, along with how to structure the input data. This description is only visible to the agent and will never be shown to users. Learn more about [writing effective tool descriptions](#writing-effective-tool-descriptions). |
| `isPublished` | Boolean | Determines whether the definition is visible in accounts that installed your app. By default, this is set to `false`. |
| `inputFields` | Array | The fields that will be sent to the external service via the `actionUrl`. |
| `labels.`\* | String | Locale key that maps to the locale definition. At a minimum, an english label (`en`) and its definition must be defined. |
| `labels..inputFieldDescriptions` | Object | An object that defines the details for the inputs for your action. In the example above, this object includes `message` and `priority` fields. |
| `labels..inputFieldOptionLabels` | Object | An object that's required if your input field(s) have options. Provides a map of input field option labels, keyed by the option's value or label. |
| `labels..outputFieldLabels` | Object | An object that maps the definitions from `outputFields` to the corresponding labels that appear in the agent UI. |
| `labels..actionName`\* | String | The action's name as displayed in the agent UI. |
| `labels..appDisplayName`\* | String | The name of the section in the tool selection panel where all tools appear. If `appDisplayName` is defined for multiple tools, the first one found will be used. |
| `labels..actionCardContent` | String | A summarized description shown in the action's card. |
| `labels..executionRules` | Object | An object that maps the definitions from your `executionRules` to messages that will appear in the agent UI. |
| `objectTypes` | Array | The available CRM object types that this action can be used with. If empty, the action will be available for all object types. |
| `outputFields` | Array | An array containing the fields and values that the tool will output. Response data must be formatted as comma separated string-string value pairs. Learn more about [output fields](#output-fields). |
| `executionRules` | Object | A list of definitions you can specify to surface errors from your service to the user in the agent UI. |
### Writing effective tool descriptions
In the `llmConfig.actionDescription` field, you can help agents understand how and when to invoke a tool, and how to structure input data. The description is only ever visible to the agent.
When writing the description:
* Describe when the tool should be used so that the agent understands what types of user requests or contexts should trigger the tool.
* Explain how to use the tool, including any technical guidance that the agent should consider when constructing inputs. For example, include details such as:
* Supported input types (e.g., "Accepts video URLs or uploaded .mp4 files.")
* Format constraints (e.g., ""Date must be in ISO 8601 format.")
* Default parameter behavior (e.g., If no contact is specified, the tool will not return any results.")
* Error-tolerant suggestions (e.g., "If company name is ambiguous, prompt user for clarification.")
* Highlight required fields, expected input formats, or default behaviors.
* Provide fallback logic or edge-case handling if applicable (e.g., "If no date range is provided, default to the last 30 days").
* Do not include branding, UI labels, or customer-facing copy, as this field is only for agent reasoning.
Below are two examples of effective agent tool descriptions:
1. **Generate meeting summary tool**
`actionDescription`: Use this tool when the user asks for a summary of one or more meetings. Provide a list of meeting IDs or transcript URLs as input. If no specific meeting is identified, use the most recent one. Summaries should include key topics, action items, and assigned responsibilities.
2. **Generate invoice PDF tool**
`actionDescription`: Use this tool when the user needs to create an invoice document. Inputs should include `recipient`, `items` (with description, quantity, and price), and `dueDate`. All currency values must be in USD unless otherwise specified. If `items` are missing, do not generate a document. Returns a downloadable PDF link.
## Output fields
In the agent tool schema, `outputFields` is an array that defines the fields that can contain values returned by the response when the tool is executed. Output field definitions are similar to input field definitions:
```json theme={null}
"outputFields": [
{
"typeDefinition": {
"name": "errorCode",
"type": "string",
"externalOptions": false
}
},
{
"typeDefinition": {
"name": "apiResponse",
"type": "string",
"externalOptions": false
}
}
]
```
To populate these output fields with data, the `outputFields` response sent to HubSpot must be structured as a JSON object with key-value pairs, where both keys and values are strings, as shown below. The keys should correspond with your defined output field names, with the values containing the actual returned data.
```json theme={null}
{
"outputFields": {
"apiResponse": "agentResponseData",
"systemStatus": "statusReport"
}
}
```
Including non-string values in the response will result in a failure to parse, and cause all outputs to be ignored.
```json theme={null}
// Non-working example (array is invalid)
{
"outputFields": {
"my_output_field": ["my", "output", "value"]
}
}
```
For tools where the `toolType` is set to `TAKE_ACTION`, you can include a follow-up CTA that lets users navigate to a HubSpot CRM record on click. To do so, add the following fields to the `outputFields` object in the response sent to HubSpot.

```json theme={null}
{
"outputFields": {
...
"ctaCrmObjectType": "contact",
"ctaCrmObjectId": "123456",
}
}
```
| Field | Type | Description |
| ------------------ | ------ | ------------------------------------------------------------------------------------------ |
| `ctaCrmObjectType` | String | The type of CRM record (e.g., contact) |
| `ctaCrmObjectId` | String | The ID of the CRM record to navigate to. |
| `ctaLabel` | String | Optionally, you can specify a label. By default, HubSpot will try to autogenerate a label. |
## Execution
When an agent request executes, a `POST` request is sent to the `actionUrl`. The request will include the `v2` `x-hubspot-signature`, which you can use to [validate the request](/docs/apps/legacy-apps/authentication/validating-requests#validate-requests-using-the-v2-request-signature).
The request body will include the input field values along with context about the account, user, and agent.
```json theme={null}
{
"callbackId": "68f5c9cjk1251j5-8363-4-1asdf0dja-4-1",
"origin": {
"portalId": 123456,
"userId": 987654,
"userEmail": "userEmail@website.com",
"actionDefinitionId": 8675309,
"actionDefinitionVersion": 4,
"actionExecutionIndexIdentifier": null,
"extensionDefinitionId": 8675309,
"extensionDefinitionVersionId": 4
},
"context": {
"agentId": 8997212,
"source": "AGENTS"
},
"fields": {
"input_field_name": "input_value"
},
"inputFields": {
"input_field_name": "input_value"
}
}
```
| Field | Type | Description |
| ------------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `callbackId` | String | A unique ID assigned to the execution. You can use this value for [execution blocking](#asynchronous-execution). |
| `origin` | Object | Metadata about the account, user, and tool associated with the request. |
| `context` | Object | Additional context about the agent and tool. |
| `inputFields` | Object | Input field data included in the request. |
### Execution state
You can manage execution state by returning the `hs_execution_state` field in your response to HubSpot. This field can be set to one of the following values:
* `SUCCESS`: the execution has completed successfully and can proceed.
* `FAIL_CONTINUE`: the execution has failed, but will proceed.
* `BLOCK`: the execution is temporarily blocked and will not proceed until it's updated via the automation API or when the block expires.
Blocking action execution enables you to prevent the agent from continuing to run until the state is updated. To put an execution block in place, configure your response's `outputFields` to include an `hs_execution_state` of `BLOCK`:
```json theme={null}
"outputFields": {
"hs_execution_state": "BLOCK",
"hs_expiration_duration": "P1WT1H"
}
```
| Prop | Type | Description |
| ------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `hs_execution_state` | String | Set to `BLOCK` to prevent the action from continuing to execute. |
| `hs_expiration_duration` | String | By default, actions are blocked for one week. Use this field to specify a different block expiration ([ISO 8601 duration format](https://en.wikipedia.org/wiki/ISO_8601#Durations)). |
To unblock the execution, make a `POST` request to `https://api.hubspot.com/callbacks/{callbackId}/complete`, where `callbackId` is the value provided in the original request body sent by HubSpot to your service.
In the request body, set the `hs_execution_state` to either `SUCCESS` or `FAIL_CONTINUE`, depending on the execution status.
```json theme={null}
{
"outputFields": {
"hs_execution_state": "SUCCESS"
}
}
```
## Best practices
When building agent tools, keep the following best practices checklist in mind:
* Start with optional fields, then set to required only when stable.
* Label and describe fields for both humans and AI.
* Test with fewer agent instructions at first to understand how the AI interprets a tool.
* Keep tools focused and be mindful of field quantity.
* Use field descriptions to control agent creativity.
Learn more about each best practice in the sections below.
### Develop with optional fields
Do not set action fields (`inputFields`) to be required during active development. Once a field has been set to required and the project is uploaded, you cannot remove or update the field. You should only set a field to required once you're confident in the field's details, such as its `name` and `type`. The reason for this limitation is that changing required fields would break any active workflows that include the action.
### Build for human and AI understanding
The `actionName`, `inputFields`, and `labels` should clearly communicate their usage and utility to both humans and the agent. These fields in particular are used by the agent to understand when to invoke the action and how to pass data to the tool. As you build your tools, keep in mind that LLMs may need more explicit descriptions than human users. For example, while a human might intuitively understand a field labeled `Date`, an LLM might prefer `Event start date (YYYY-MM-DD)`.
Ideally, tools should be built so that agents don’t require additional instructions to use it. However, there are cases where field details alone may not be sufficient for the agent. For example, it may not understand the intended order of operations for executing tools that are dependent on the output of other tools (e.g., a 'Send Email' tool might depend on a 'Get Contact Info' tool running first).
While building a tool, you should test it in the agent without adding to the agent's instructions first to better understand how it interprets the tool. Through testing, you'll be able to determine whether the reasoning engine performs correctly on its own or if it needs additional instructions.
### Be mindful of the number of input fields
Agents can handle a high number of input fields in each tool (more than 26 unique inputs). However, the more input fields there are in a tool, the clearer you need to be when assigning `actionName`, `inputField`, and `label` values. Tools are most effective and reliable when they're designed for specific tasks with a focused set of parameters. For complex operations, consider whether it would be more effective to create multiple, simpler tools versus a single tool with an excessive number of inputs.
**Please note:** it's possible that HubSpot will restrict the number of inputs in the future based on the feedback around the quality of agents handling large numbers of inputs.
### Control agent creativity and improvisation
In some scenarios, you might want an agent to be creative and improvisational. However, there may be scenarios where you don't want the agent to improvise. Experiment with instructing the LLM in your input field names, labels, and descriptions. If you require stricter guidance, add instructions to the agent to set clear expectations.
For example, let's say you give an agent the task of generating a blog post, with one of the fields being the blog post title. Depending on how much you want the agent to improvise, you could label the field permissively or more restrictively:
* **Permissive:** `"Blog title"`
* **Restrictive:** `"Blog title (must include the product name 'HubSpot CRM')"`
As another example, consider the following input field labels intended for social media post content:
* **Permissive:** `"Social post content"`
* **Moderate:** `"Social post content (keep under 280 characters)"`
* **Restrictive:** `"Social post content (must mention our Q4 sale, include #HubSpot, and stay under 280 characters)"`
## Testing and validation
Agent tools require two-phase testing to ensure both functional correctness and proper AI integration:
### Phase 1: Workflow testing
Test your tool's core logic using HubSpot workflows:
1. Create a test workflow and add your tool as a workflow action
2. Test with both correct and incorrect inputs
3. Verify expected outputs and error handling
4. Iterate on your backend code based on results
### Phase 2: Agent integration testing
Use the Developer Tool Testing Agent (available in the Agent Marketplace) to test AI-specific functionality:
**Tool Recognition Testing:**
* Test whether the agent correctly identifies when to use your tool based on its name and description
* Compare performance against other available tools to ensure clear differentiation
**Parameter Extraction Testing:**
* Test both direct commands ("Use Tool X with parameter Y") and indirect prompts ("Help me accomplish goal Z")
* Verify the agent correctly extracts all required parameters from natural language
* The testing agent reports exactly what parameters it passes to help with verification
**Sequential Operations Testing:**
* Test scenarios where your tool works in sequence with other tools
* Verify the agent can correctly pass outputs from one tool as inputs to another
**Based on testing results, refine:**
* `llmConfig.actionDescription` for clearer tool purpose and usage
* `labels` properties for better parameter extraction
* Field descriptions to guide agent behavior
## Verifying agent tool request origin
When an agent uses a tool to make a request, it makes a `POST` request to the tool's `actionUrl`. Agent tool invocation is authenticated by validating the `X-HubSpot-Signature` header sent with the request. This is the same system HubSpot uses for [validating webhook requests](/docs/apps/legacy-apps/authentication/validating-requests#validate-the-v3-request-signature).
**Please note:** you should not create input fields for secrets or API keys, as this is insecure and not the intended authentication pattern, especially because the LLM would need to be given the secret/API key in its instructions or receive it from another tool.
# Create and manage event types
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types
Learn how to define app event types so that you can send event occurrence data into HubSpot.
This feature is intended only for [technology partners](https://www.hubspot.com/partners/technology/join), and requires approval from HubSpot to use. To apply for app events access, or if you want to learn more about the functionality, please submit [this in-app form](https://app.hubspot.com/l/developer-overview/appObjectsEventsRequest).
To send app event data to HubSpot, you'll first create an event type using a developer project. The event type is a JSON schema that defines the structure, properties, and validation rules for the event occurrence data you'll be sending. This includes the event name, display label, target CRM object, and event properties. Event types also include definitions for display templates for CRM record timeline rendering.
The app events described in this documentation are intended for apps built for the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps) using developer projects. To build custom event reporting for other types of integrations, you can instead use [custom events](/docs/api-reference/latest/events/define-events/guide).
## Prerequisites
To include an app event type definition in your project:
* You must be using HubSpot CLI version `7.6.0` or later. You can check which version of the CLI you have by running `hs --version`, and update by running `npm install -g @hubspot/cli@latest`.
* Your project must be deployed before you can update it to include an app event component.
## Set up your project files
If you previously created a public app with a timeline event, learn more about [migrating it to the developer platform](#migrate-an-existing-timeline-event-type).
To create a new app event type definition in your project, update your [app configuration](/docs/apps/developer-platform/build-apps/app-configuration) `hsmeta.json` file to include the `timeline` scope in the `requiredScopes` field. This is required for app event data to appear on CRM record timelines. Installers must grant this scope for your event data to send successfully.
```json theme={null}
"requiredScopes": [
"timeline"
],
```
Note that you may need to add other [scopes](/docs/apps/legacy-apps/authentication/scopes#list-of-available-scopes), depending on the features your app includes.
Then, in the `app/` directory of your project, add an `app-events` directory. In `app-events/`, add a JSON configuration file to define the app event type. This file can have any name, but must end in `*-hsmeta.json`. You can include up to 750 event types per app, with each having its own `*-hsmeta.json` file.
In the event type `*-hsmeta.json` file, set up your [event type schema](/docs/apps/developer-platform/add-features/app-events/reference#event-type-schema). This schema will configure event type attributes such as name, CRM record timeline display template, and the CRM object type to associate event data with.
While some of these fields can be updated after creation, the `name` and `objectType` fields cannot be changed once the event type is created.
For example, the following event type schema could be used for tracking customer login events.
```json theme={null}
{
"uid": "customer_login_event",
"type": "app-event",
"config": {
"name": "Customer login",
"description": "Tracks when a customer logs into their account including the method of login.",
"headerTemplate": "{{customerName}} logged in",
"detailTemplate": "{{customerName}} logged in via the {{loginLocation}}.",
"objectType": "CONTACT",
"properties": [
{
"name": "customerName",
"label": "Customer Name",
"description": "The full name of the customer who logged in.",
"type": "string"
},
{
"name": "loginLocation",
"label": "Login location",
"description": "Where the customer logged in from.",
"type": "enumeration",
"options": [
{
"value": "mobileApp",
"label": "Mobile app"
},
{
"value": "website",
"label": "Website"
}
]
}
]
}
}
```
| Field | Type | Description |
| ----------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid` | String | An internal unique identifier for the event type. |
| `type` | String | The type of component. Must be `app-event`. |
| `name` | String | The label displayed in HubSpot (up to 50 characters). This value cannot be updated after creation. |
| `description` | String | A description of the event type. Can be up to 1,000 characters in length. |
| `objectType` | String | The fully qualified name of the CRM object type that event occurrences can be associated with. This value cannot be changed after creation. Can be one of: `APP_OBJECT`, `APPOINTMENT`, `COMPANY`, `CONTACT`, `COURSE`, `DEAL`, `LEAD`, `LISTING`, `ORDER`, `PROJECT`, `SERVICE`, `TICKET`. To create an event type for custom objects, use the `supportsCustomObject` field instead. |
| `supportsCustomObject` | Boolean | Set to `true` to configure the app event type for custom objects. When set to `true`:
The event type will be enabled for all custom objects in the account.
You cannot specify another `objectType` in addition to custom objects (i.e., the `objectType` cannot be set to `CONTACT` and support custom objects).
You cannot copy event property values to object property values via [property stamping](/docs/apps/developer-platform/add-features/app-events/reference#property-stamping).
|
| `headerTemplate` | String | The [rendering template](/docs/apps/developer-platform/add-features/app-events/reference#rendering-templates) for the header of the CRM timeline activity card. Can be up to 1,000 characters in length. |
| `detailTemplate` | String | The [rendering template](/docs/apps/developer-platform/add-features/app-events/reference#rendering-templates) for the body of the CRM timeline activity card. Can be up to 10,000 characters in length. |
| `properties` | Array | Properties defined for the event type that you'll store event occurrence data in. Each event type can have up to 500 properties. Learn more about properties, including requirements and limitations, in the [app events reference documentation](/docs/apps/developer-platform/add-features/app-events/reference). |
Upload your project to HubSpot with the `hs project upload` command.
```shell theme={null}
hs project upload
```
During the build phase, HubSpot will validate the event type. If there are any schema validation errors, the CLI will return an error indicating what to fix.
By default, after a successful build, HubSpot will automatically deploy the project. If you've turned this off in your [project settings](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#view-your-app-on-the-project-details-page), you can manually deploy using `hs project deploy`.
After deploying, your app event type will now be available and can be used for receiving event occurrence data.
## Retrieve the fullyQualifiedName (optional)
To send event occurrence data for the event type, you can either use your app's `uid` that's [set in the top-level](/docs/apps/developer-platform/build-apps/app-configuration#specifying-uids) `app-hsmeta.json` file, or you can use the `fullyQualifiedName`, which is retrievable via the app events API. This name identifies the event type, and will need to be included with all app event API operations.
If you want to use the `fullyQualifiedName` when sending occurrence data:
* To retrieve the `eventTypeName`, make a `POST` request to `https://api.hubapi.com/integrators/timeline/v4/types/projects`. In the request body, include:
* A `projectName` field, which should be set to the `name` value from the `hsproject.json` file of your project.
* A `developerSymbol` field, which should be set to the `uid` value from the event type `*-hsmeta.json` configuration file.
```json theme={null}
{
"projectName": "my-marketplace-app",
"developerSymbol": "customer_login_event"
}
```
The response will return the project name, event type name, and the `fullyQualifiedName`.
```json highlight={6} theme={null}
{
"developerQualifiedSymbol": {
"projectName": "marketplace",
"developerSymbol": "customer_login_event"
},
"fullyQualifiedName": "ae000000_integrators-timeline-event-type-id-0000000"
}
```
The `fullyQualifiedName` value will never change, so you can safely hard code it into your implementation. The API limits the number of times you can retrieve this value per day, and cannot be used programmatically.
## Managing event types
To update an event type after creation, you'll just need to update the event type schema in your project, then re-upload to build and deploy the app.
If you no longer wish to use an event type, you can delete it from your project. However, before deleting an event type, note the following:
* Deleting an event type is a permanent and irreversible action.
* Deleting an event type removes both the event type and all occurrences of that type from all accounts that have installed the app.
* Deleting an event type will break other HubSpot tools that rely on that event type, such as reports and workflows.
To delete the event type, delete the `*-hsmeta.json` event type configuration file from your project, then upload and deploy the project.
## Migrate an existing timeline event type
If you have an existing public app with a timeline event, you can migrate it to the developer platform using the CLI.
Before migrating your app:
* Check out the [public app migration guide](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-an-existing-public-app) to review the current considerations and limitations of the developer platform beta.
* Once you've migrated an existing timeline event type (`v1`/`v3`) to the developer platform, you'll have 7 days to change existing `v1`/`v3` timeline event API requests to the new `v4` endpoints, including both event type event occurrence/instance API requests. After 7 days, any existing calls to `v1`/`v3` event occurrence endpoints will return `401` errors.
* For creating and updating event types and templates, you'll no longer need to use the API. Instead, you'll manage them directly in the project via the [event type configuration file](/docs/apps/developer-platform/add-features/app-events/reference#event-type-schema).
* For sending event occurrence data, you'll need to use the `v4` endpoints, which offers both [single and batch send endpoints](/docs/apps/developer-platform/add-features/app-events/send-event-occurrences).
To migrate your app:
* In the terminal, run `hs app migrate`, then follow the terminal prompts to migrate your app and timeline event.
* Once the migration process is done, your app will now live in a developer project, and a local project directory will be created for you.
* To open the project in HubSpot, navigate into the local project directory, then run `hs project open`.
Note that the generated configuration files (`*-hsmeta.json`) may contain fields with `null` values. These fields are an artifact from the migration, and it's safe to remove them.
## Next Steps
After creating an event type, learn how to [send event occurrence data via the API](/docs/apps/developer-platform/add-features/app-events/send-event-occurrences).
You can also check out the [app events reference documentation](/docs/apps/developer-platform/add-features/app-events/reference) to continue building and customizing your app events.
# App events overview
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/app-events/overview
Learn how to define app events on the latest version of the developer platform.
This feature is intended only for [technology partners](https://www.hubspot.com/partners/technology/join), and requires approval from HubSpot to use. To apply for app events access, or if you want to learn more about the functionality, please submit [this in-app form](https://app.hubspot.com/l/developer-overview/appObjectsEventsRequest).
Using app events, you can send enriched activity data from your external systems directly into HubSpot. App event data is associated with CRM records, and can be used across HubSpot's automation, analytics, and reporting tools.
Below, learn more about how app events work. To start building app events into your project:
* [Create an event type](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types)
* [Send event occurrence data](/docs/apps/developer-platform/add-features/app-events/send-event-occurrences)
* Review the [app events reference documentation](/docs/apps/developer-platform/add-features/app-events/reference)
The app events described in this documentation are intended for apps built by [technology partners](https://www.hubspot.com/partners/technology/join) for the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps). To build custom event reporting for other types of integrations, you can instead use [custom events](/docs/api-reference/latest/events/define-events/guide).
## How app events work
Events are data points that capture what occurred at a specific moment in time, often related to user actions, such as submitting a form, sending an SMS message, or viewing a page. Unlike CRM records, which are intended to show the current state of an entity, events can give you a better understanding of activity patterns, behavioral changes over time, and other trends.
App events are ideal when your data:
* Represents actions or activities that happen repeatedly.
* Needs to be analyzed for changes or volume over time.
* Doesn't require updating after creation.
At a high level, an app event consists of:
* **Event type definition:** a JSON schema that defines the structure, properties, and validation rules for the event. This includes the event name, display label, target CRM object, and event properties. Event types also include definitions for display templates for CRM record timeline rendering. Event types are defined using developer projects.
* **Event occurrences:** individual instances of events that contain actual data, validated against your event type schema. Event occurrence data is sent via the app events API, and includes the event type identifier, property values, timestamps and additional metadata, and CRM object reference data for CRM record association.
**Timeline events vs. app events**
Previously, HubSpot released timeline events to enable you to surface event data on CRM object timelines. App events allow for that same functionality across an expanded list of supported objects, while also being available in other HubSpot tools. App events also now have their own index page alongside custom events, providing a unified view of event activities.
Existing timeline event integrations will continue to function and will be visible in the same places as app events. However, to create new event types or make changes to existing types, you must recreate it as an app event in your developer project.
## Supported objects and tools
When defining an event type, you'll configure it to associate with a specific CRM object, enabling you to tie event data to its source, such as the contacts who log in to your website. Event occurrences can then be associated with CRM records of that type. App events can be associated with the following CRM objects:
* App objects
* Appointments
* Companies
* Contacts
* Courses
* Custom objects
* Deals
* Leads
* Listings
* Orders
* Projects
* Services
* Tickets
App events can be used in the following HubSpot tools:
* **Event management interface:** view and analyze app event data in the custom events dashboard alongside custom event data by navigating to **Data Management** > **Event management** in your account.
* **CRM record timelines:** view chronological app event activity on the timelines of CRM records associated with event occurrences.
* **Reporting tools:** use app event data to build custom reports, customer journey analytics, and datasets for analysis and visualization.
* **Lead scores, lists, and workflows:** segment your CRM database using events for dynamic lists, lead scoring, and triggering workflows.
## Best practices
When building event types:
* Use clear, descriptive names for [event types](/docs/apps/developer-platform/add-features/app-events/reference#event-type-schema) and [properties](/docs/apps/developer-platform/add-features/app-events/reference#event-properties), as they'll appear as filters in workflows, lists, reports, and the custom events dashboard.
* Choose appropriate property types (string, number, datetime, etc.) that match your data and enable proper filtering and analysis.
* Include properties that will be useful for segmentation, reporting, and automation triggers (e.g., transaction amounts, user roles, action categories).
* Design event types with reporting and workflow use cases in mind, considering how users will filter and group event data.
* Avoid making breaking changes to existing event type schemas, as this can disrupt existing workflows and reports that depend on the event structure.
To maintain high quality event data:
* Review any programmatic event occurrence data requests to ensure they will properly validate against your event type schema.
* Use consistent formatting for event property values, especially for dates, numbers, and categorical data.
* Ensure event occurrences are associated with [valid CRM record IDs](/docs/apps/developer-platform/add-features/app-events/send-event-occurrences#crm-record-association).
* Monitor event submission success rates and [error responses](/docs/apps/developer-platform/add-features/app-events/reference#occurrence-send-errors).
* Regularly review event data on CRM record timelines and in the custom events dashboard for accuracy.
Optimize event submission performance by:
* [Batching multiple event occurrences](/docs/apps/developer-platform/add-features/app-events/send-event-occurrences#sending-event-occurrences) in single API requests when possible (up to 500 events per batch).
* Keeping event occurrence payloads lean by only including necessary property data.
* Implementing exponential backoff retry logic for failed event submissions.
## Limitations
Keep in mind the following limitations when planning and building app events.
### Version requirements
* App events can only be created through the latest versions of the developer project platform (`2025.2` and `2026.03`).
* For existing event types, you can continue to use the [v1](/docs/api-reference/legacy/crm/extensions/timeline/v1/get-integrations-v1-application-id-timeline-event-event-typeid-event-id) and [v3 timeline events API](/docs/api-reference/legacy/crm/extensions/timeline/guide). However, you cannot create new event types using the `v1` and `v3` APIs.
* For existing event types that have been migrated to app events on the developer platform, see the [migration restrictions](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types#migrate-an-existing-timeline-event-type) for more information about accessing the legacy API versions.
### Event type constraints
* Event occurrences must conform to the defined event type schema or they will be rejected.
* Some [event type fields](/docs/apps/developer-platform/add-features/app-events/reference#event-type-schema) cannot be updated after the event type is created (e.g., `name` and `objectType`).
* New event types are subject to a review process before they can be activated.
### Data limitations
* Individual properties are limited to 510KB, with total event size capped at 1MB.
* App events are intended for HubSpot Marketplace providers, and will only work with apps configured to use OAuth authentication. Internal systems should instead use [custom events](/docs/api-reference/latest/events/define-events/guide).
## Getting started
To get started working with app events, you'll need to:
* [Create an event type](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types) to define the structure of your event data using a developer project.
* [Send event occurrence data](/docs/apps/developer-platform/add-features/app-events/send-event-occurrences#send-a-single-occurrence) to the app using the [app events API]().
* Leverage the data in HubSpot as needed using reporting, automation, or analytics tools.
# App events reference
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/app-events/reference
Reference information for app events on the latest version of the developer platform.
This feature is intended only for [technology partners](https://www.hubspot.com/partners/technology/join), and requires approval from HubSpot to use. To apply for app events access, or if you want to learn more about the functionality, please submit [this in-app form](https://app.hubspot.com/l/developer-overview/appObjectsEventsRequest).
Below, find reference information for using app events, including event type schemas, event timeline rendering templates, event occurrence fields, and more.
## Project structure
To define an app event type, create an `app-events` directory within `src/app/`. Then, add a configuration file to that directory for each event type you want to define, using the naming convention `*-hsmeta.json`.
To include event type definitions in a project requires the following:
* Your app must use OAuth authentication and be configured for HubSpot Marketplace distribution. In addition, the app must include `timeline` in its `requiredScopes`. Learn more about [app configuration](/docs/apps/developer-platform/build-apps/app-configuration).
* Your project must successfully deploy before you can include an app event component.
## Event type configuration
Below are the configuration options available for event type schemas (`*-hsmeta.json`). Note that `objectType` cannot be changed after the event type is created.
Each app is limited to 750 event types.
```json theme={null}
{
"uid": "customer_login_event",
"type": "app-event",
"config": {
"name": "Customer login",
"description": "Tracks when a customer logs into their account including the method of login.",
"headerTemplate": "{{customerName}} logged in.",
"detailTemplate": "{{customerName}} logged in via the {{loginLocation}}.",
"objectType": "CONTACT",
"properties": [
{
"name": "customerName",
"label": "Customer Name",
"description": "The full name of the customer who logged in.",
"type": "string"
},
{
"name": "loginLocation",
"label": "Login location",
"description": "Where the customer logged in from.",
"type": "enumeration",
"options": [
{
"value": "mobileApp",
"label": "Mobile app"
},
{
"value": "website",
"label": "Website"
}
]
}
]
}
}
```
| Field | Type | Description |
| ----------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid` | String | An internal unique identifier for the event type. |
| `type` | String | The type of component. Must be `app-event`. |
| `name` | String | The label displayed in HubSpot (up to 50 characters). |
| `description` | String | A description of the event type. Can be up to 1,000 characters in length. |
| `objectType` | String | The fully qualified name of the CRM object type that event occurrences can be associated with. Can be one of: `APP_OBJECT`, `APPOINTMENT`, `COMPANY`, `CONTACT`, `COURSE`, `DEAL`, `LEAD`, `LISTING`, `ORDER`, `PROJECT`, `SERVICE`, `TICKET`. To create an event type for custom objects, use the `supportsCustomObject` field instead.
This value cannot be changed after creation. |
| `supportsCustomObject` | Boolean | Set to `true` to configure the app event type for custom objects. When set to `true`:
The event type will be enabled for all custom objects in the account.
You cannot specify another `objectType` in addition to custom objects (i.e., the `objectType` cannot be set to `CONTACT` and support custom objects).
You cannot copy event property values to object property values via [property stamping](#property-stamping).
|
| `headerTemplate` | String | The [rendering template](#rendering-templates) for the header of the CRM timeline activity card. Can be up to 1,000 characters in length. |
| `detailTemplate` | String | The [rendering template](#rendering-templates) for the body of the CRM timeline activity card. Can be up to 10,000 characters in length. |
| `properties` | Array | Properties defined for the event type that you'll store event occurrence data in. Each event type can include up to 500 properties. Learn more about event properties below. |
### Event properties
The `properties` array of the schema contains the fields that you can [send event occurrence data](/docs/apps/developer-platform/add-features/app-events/send-event-occurrences) to. When HubSpot receives event occurrence data, it validates any properties included with it against the ones defined in the schema. Incoming occurrence properties must match these event type properties, otherwise HubSpot will reject the occurrence.
**Please note:** once you create a property, you cannot change its `type`.
```json theme={null}
"properties": [
{
"name": "customerName",
"label": "Customer Name",
"description": "The full name of the customer who logged in.",
"type": "string"
},
{
"name": "loginLocation",
"label": "Login location",
"description": "Where the customer logged in from.",
"type": "enumeration",
"options": [
{
"value": "mobileApp",
"label": "Mobile app"
},
{
"value": "website",
"label": "Website"
}
]
}
]
```
| Field | Type | Description |
| ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | String | The internal name of the property. Must be lowercase and between 3-100 characters. Property names must be unique per event type. The value cannot match the regular expression `"[A-Za-z0-9_\\-.]+"`, begin with `hs_`, or match any reserved keywords.
`applicationId`
`domain`
`email`
`eventTemplateId`
`eventTypeId`
`extraData`
`id`
`log`
`lookup`
`objectType`
`objectId`
`portalId`
`properties`
`timelineIFrame`
`timestamp`
`tokens`
`utk`
|
| `label` | String | The label displayed in HubSpot. Must be lowercase and between 1-500 characters. |
| `description` | String | A description of the property. Can be up to 1,000 characters in length. |
| `type` | String | The type of data captured in the property. Can be one of `string`, `number`, `date`, `enumeration`, `bool`. This field can only be set during property creation (it cannot be updated after).
Note that any `date` event properties must be specified as a Unix timestamp in milliseconds (e.g., `1762198997303`) or [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format (e.g., `"2026-02-19T14:06:16-05:00"`). |
| `options` | Array | For `enumeration` type properties, this field provides the available options. Must contain at least one option. Each option is an object that contains:
`name`: the label for the option displayed in HubSpot.
`value`: the internal value provided by the event occurrence.
The `name` and `label` must be unique within the event type. |
| `objectProperty` | String | If included, the name of the CRM object property that should be updated when event data is sent to HubSpot. The value in this property will overwrite any existing values in that property. Learn more about stamping CRM record properties below. |
| `isMultiValued` | Boolean | For `enumeration` type properties, this field sets whether or not users should be allowed to select more than one enumeration option |
### Property stamping
In some cases, you may want to modify the CRM record's property values based on app event occurrence data. For example, you may want to update a contact's first and last name with new values set by the occurrence (e.g., form submission).
Property stamping is not available for event types configured for custom objects.
To update CRM record properties via event occurrences, you can link an event property to a CRM property within the event type schema. In the definition fields for a given event property, include the `objectProperty` field and specify the CRM property to link. Once a property is linked, HubSpot will always update the property value on the CRM record using the value from the most recent occurrence based on the `timestamp` field.
For example, the event type schema below links the event property `customerName` with a custom contact property named `custom_property_name`. When event occurrence data includes a value for `customerName`, `custom_property_name` will be updated for the associated CRM record.
```json theme={null}
"properties": [
{
"name": "customerName",
"label": "Customer Name",
"type": "string",
"objectProperty": "custom_property_name"
}
]
```
### Rendering templates
Event type schemas can include the `headerTemplate` and `detailTemplate` fields to configure how event occurrences renders on CRM record timelines.
* `headerTemplate`: a one-line description of the event at the top of the activity card (up to 1,000 characters).
* `detailTemplate`: the details of the event in the body of the activity card (up to 10,000 characters).
Rendering templates are written using [Markdown](https://www.markdownguide.org/basic-syntax/) (excluding inline HTML and HTML blocks) with [Handlebars](https://handlebarsjs.com/guide/) templates. These templates can render event occurrence data as follows:
* In both templates, you can access any `property` data passed by the event occurrence using the syntax `{{propertyName}}`.
* In the `detailTemplate`, you can additionally access `extraData` values passed by the event occurrence using the syntax `{{extraData.fieldName}}`. You can access any tier of attribute in `extraData` through dot notation, such as `{{extraData.person1.preferredName}}`.
The `extraData` object can only contain valid JSON. If the JSON is malformed, the occurrence will be rejected and you'll receive an error response.
For example, the templates below use the `customerName` and `loginLocation` property data, along with the `surveyData` field from `extraData` [sent via the event occurrence](#event-occurrences).
```json wrap theme={null}
"headerTemplate": "{{customerName}} logged in via the {{loginLocation}}.",
"detailTemplate": "#### Post-login survey\n{{#each extraData.surveyData}}\n- **{{question}}**: {{answer}}\n{{/each}}",
```
Since templates are built with Markdown and Handlebars, you can take advantage of Handlebars helpers to make the content more dynamic. For example, the following `detailTemplate` includes the [`#if` helper](https://handlebarsjs.com/guide/builtin-helpers.html#if) to conditionally render content based on whether the event occurrence data includes the `surveyData` field in `extraData`.
* If `extraData` contains `surveyData`, show the post-login survey responses.
* If no `surveyData` was present in the event occurrence, render `No additional information.`.
```json theme={null}
"detailTemplate": "{{#if extraData.surveyData}}#### Post-login survey\n{{#each extraData.surveyData}}\n- **{{question}}**: {{answer}}\n{{/each}}{{else}}No additional information."
```
### Using iframes
When event occurrence data contains the `timelineIFrame` field, the timeline activity card will include a hyperlink that users can click to open the linked contents in an iframe.
```json theme={null}
"timelineIFrame": {
"linkLabel": "Click me",
"headerLabel": "This is an iframe",
"url": "https://developers.hubspot.com/docs/apps/developer-platform/overview",
"width": 300,
"height": 300
}
```
| Field | Type | Description |
| ------------- | ------- | ---------------------------------------------------------------- |
| `linkLabel` | String | The hyperlink text that will launch the iframe on click. |
| `headerLabel` | String | The label of the modal window that displays the iframe contents. |
| `url` | String | The URL of the iframe contents. |
| `width` | Integer | The width of the iframe modal. |
| `height` | Integer | The height of the iframe modal. |
## Event occurrences
To send event occurrences for a given event type, make a `POST` request to the endpoints below. The app events API includes endpoints for sending single event occurrences and batches of multiple event occurrences. For both endpoints, the event occurrence data will need to be validated against an existing event type schema, which you'll specify with `eventTypeName` in the request body.
**Please note:** each event occurrence (including all properties and metadata) cannot exceed 1 MB in size. Individual property values cannot exceed 512 KB.
To send a single event occurrence, make a `POST` request to `/integrators/timeline/v4/events`.
In the request body, include the event occurrence data, adhering to the event type's defined schema.
```json theme={null}
{
"eventTypeName": "ae000000_integrators-timeline-event-type-id-0000000",
"objectId": "123456",
"id": "login-1",
"properties": {
"customerName": "Mark S.",
"loginLocation": "mobileApp",
"preferredDevice": "iPhone;MacBook"
}
},
```
To send a batch of event occurrences, make a `POST` request to `/integrators/timeline/v4/events/batch`.
In the request body, include up to 500 comma-separated event occurrence objects within an `inputs` array. If any occurrences in the batch fail to validate, no occurrences from the batch will be accepted.
```json theme={null}
{
"inputs": [
{
"EventTypeName": "ae000000_integrators-timeline-event-type-id-0000000",
"id": "login_event_100",
"objectId": "769851",
"properties": {
"customerName": "Tim",
"loginLocation": "mobileApp"
},
"extraData": {
"surveyData": [
{
"question": "How was your login experience?",
"answer": "Fine!"
},
{
"question": "How likely are you to recommend logging in to a co-worker?",
"answer": "Extremely likely"
}
]
}
},
{
"EventTypeName": "ae000000_integrators-timeline-event-type-id-0000000",
"id": "login_event_101",
"objectId": "769851",
"properties": {
"customerName": "Tim",
"loginLocation": "website"
},
"extraData": {}
}
]
}
```
In the request body, include data based on the defined event type schema. The request body must include the `eventTypeName`, which can either be your app's `uid` [defined](/docs/apps/developer-platform/build-apps/app-configuration#specifying-uids) in the top-level `*-hsmeta.json` file, or `fullyQualifiedName` returned from the `/integrators/timeline/v4/types/projects` [API](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types#retrieve-the-fullyqualifiedname-optional).
```json theme={null}
{
"eventTypeName": "ae000000_integrators-timeline-event-type-id-0000000",
"objectId": "8675309",
"properties": {
"customerName": "Mark S.",
"loginLocation": "mobileApp"
},
"id": "login-1529a3gda23",
"extraData": {
"surveyData": [
{
"question": "How was your login experience?",
"answer": "Fine!"
},
{
"question": "How likely are you to recommend logging in to a co-worker?",
"answer": "Extremely likely"
}
]
}
}
```
| Field | Type | Description |
| ------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `eventTypeName` | String | The fully qualified name of the event type, which you'll use to identify the event via the API.
This value can either be your app's `uid` [defined](/docs/apps/developer-platform/build-apps/app-configuration#specifying-uids) in the top-level `*-hsmeta.json` file, or can be [obtained via the API](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types#retrieve-the-fullyqualifiedname) after creating the event type. This value cannot be changed after creation. |
| `objectId` | String | The ID of the CRM record to associate with the event occurrence. This field can be used for all types of CRM records, and is the recommended identifier. Learn more about [CRM record association](#crm-record-association). |
| `email` | String | For contact association, you can provide the email address of the contact to associate. Learn more about [CRM record association](#crm-record-association). |
| `utk` | String | For contact association, you can provide the usertoken of an existing contact to associate. Learn more about [CRM record association](#crm-record-association). |
| `domain` | String | For company association, you can provide the website domain of an existing company. Learn more about [CRM record association](#crm-record-association). |
| `timestamp` | String | Sets the time of the event occurrence ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). If not provided, HubSpot will default to the timestamp of when the event occurrence data is sent. |
| `properties` | Object | Key-value pairs of property names and values for [properties](/docs/apps/developer-platform/add-features/app-events/reference#event-properties) you've defined on the event type. Sending properties that don't exist in the event type schema or that are a different type than defined in the schema will result in the occurrence being rejected.
Note that to send multiple values for a multi-valued enumeration property, provide a string with options separated by a semicolon |
| `extraData` | Array | When included, provides additional information for timeline rendering. Must be valid JSON. Learn more about [extra data in rendering templates](#rendering-templates). |
| `timelineIFrame` | Object | When included, the timeline card will include a hyperlink that allows users to open the linked contents in an iframe. Learn more about [using iframes](#using-iframes). |
| `id` | String | A unique identifier for the event occurrence. Must be unique within the event type across all accounts. If not provided, HubSpot will generate a random UUID. When multiple events have the same ID within a year, the first will be accepted and all others will be rejected. |
If any occurrences fail to validate, successfully validated occurrences will still be accepted and persisted. The error messaging in the response will provide information about what you'll need to fix.
### CRM record association
Each event occurrence must be associated with a CRM record, with the CRM object type defined by the event type schema. The app events API includes multiple fields for associating event occurrence data with CRM records. For all supported CRM objects, it's recommended to use the `objectId` field. However, there are some situations where you may want to use the other fields.
* `utk`/`email`: if you don't know the contact's ID, use the `utk` and/or `email` field for identification. Providing both of these identifiers also enables you to create and update contacts. For example:
* If `utk` matches an existing contact but the `email` doesn't match, HubSpot will update the contact with the new email address.
* If no `objectId` is provided, the event occurrence will associate with an existing contact that matches the `utk`/`email`, or HubSpot will create a new contact if no match is found.
* Note that the `utk` alone cannot create new contacts. You should always include `email` with `utk` to ensure proper association.
* `domain`: for company association, you must provide the `objectId`, but you can also include `domain` to update the `domain` property of that company.
# Send event occurrences
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/app-events/send-event-occurrences
Learn how to send event occurrence data into HubSpot using your defined event type schemas.
This feature is intended only for [technology partners](https://www.hubspot.com/partners/technology/join), and requires approval from HubSpot to use. To apply for app events access, or if you want to learn more about the functionality, please submit [this in-app form](https://app.hubspot.com/l/developer-overview/appObjectsEventsRequest).
After [defining an event type schema](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types) and [retrieving its fullyQualifiedName](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types#retrieve-the-fullyqualifiedname), you can send event occurrence data via the app events API. When sending event data, you'll need to adhere to the schema you created earlier. Requests that don't match the schema will fail validation and will not be captured by the app.
**Please note:** event occurrences are immutable. Once created, they cannot be updated or removed.
## Sending event occurrences
To send a single event occurrence, make a `POST` request to `/integrators/timeline/v4/events`.
In the request body, include the following:
* The event data following the event type's defined schema, along with the `id` of the CRM record to associate with the event occurrence.
* The `fullyQualifiedName` value in a `eventTypeName` field. This value can either be your app's `uid` [defined](/docs/apps/developer-platform/build-apps/app-configuration#specifying-uids) in the top-level `*-hsmeta.json` file, or `fullyQualifiedName` returned from the `/integrators/timeline/v4/types/projects` [API](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types#retrieve-the-fullyqualifiedname-optional).
```json theme={null}
{
"eventTypeName": "ae000000_integrators-timeline-event-type-id-0000000",
"objectId": "123456",
"id": "login-1",
"properties": {
"customerName": "Mark S.",
"loginLocation": "mobileApp"
}
}
```
To send a batch of event occurrences, make a `POST` request to `/integrators/timeline/v4/events/batch`.
In the request body, include up to 500 comma-separated event occurrence objects within an `inputs` array:
* Each event occurrence object should adhere to your event type's defined schema, and should also include the `id` of the CRM record to associate with the event occurrence.
* Each occurrence object should also include the `fullyQualifiedName` value in a `eventTypeName` field. This value can either be your app's `uid` [defined](/docs/apps/developer-platform/build-apps/app-configuration#specifying-uids) in the top-level `*-hsmeta.json` file, or `fullyQualifiedName` returned from the `/integrators/timeline/v4/types/projects` [API](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types#retrieve-the-fullyqualifiedname-optional).
* If any occurrences in the batch fail to validate, no occurrences from the batch will be accepted.
```json theme={null}
{
"inputs": [
{
"eventTypeName": "ae000000_integrators-timeline-event-type-id-0000000",
"id": "login_event_100",
"objectId": "769851",
"properties": {
"customerName": "Tim",
"loginLocation": "mobileApp"
},
"extraData": {
"surveyData": [
{
"question": "How was your login experience?",
"answer": "Fine!"
},
{
"question": "How likely are you to recommend logging in to a co-worker?",
"answer": "Extremely likely"
}
]
}
},
{
"eventTypeName": "ae000000_integrators-timeline-event-type-id-0000000",
"id": "login_event_101",
"objectId": "769851",
"properties": {
"customerName": "Tim",
"loginLocation": "website"
},
"extraData": {}
}
]
}
```
Fields marked with \* are required.
| Field | Type | Description |
| ------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `eventTypeName`\* | String | The fully qualified name of the event type, which you'll use to identify the event via the API.
This value can either be your app's `uid` [defined](/docs/apps/developer-platform/build-apps/app-configuration#specifying-uids) in the top-level `*-hsmeta.json` file, or can be [obtained via the API](/docs/apps/developer-platform/add-features/app-events/create-and-manage-event-types#retrieve-the-fullyqualifiedname) after creating the event type. This value cannot be changed after creation. |
| `objectId`\* | String | The ID of the CRM record to associate with the event occurrence. This field can be used for all types of CRM records, and is the recommended identifier. Learn more about [CRM record association](#crm-record-association). |
| `objectTypeFullyQualifiedName` | Type | This field is required when the event type is configured for custom objects. Specifies the target custom object by its [fullyQualifiedName](/docs/api-reference/latest/crm/objects/custom-objects/guide#retrieve-custom-object-records), using the format `p{accountId}_{objectName}` (e.g., `p1234567_Cats`). Including this field when targeting a non-custom object event type will result in a rejected data send. |
| `email` | String | For contact association, you can provide the email address of the contact to associate. Learn more about [CRM record association](#crm-record-association). |
| `utk` | String | For contact association, you can provide the usertoken of an existing contact to associate. Learn more about [CRM record association](#crm-record-association). |
| `domain` | String | Include this field in addition to `objectId` to set the company's `domain` property value. Learn more about [CRM record association](#crm-record-association). |
| `timestamp` | String | Sets the time of the event occurrence ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). If not provided, HubSpot will default to the timestamp of when the event occurrence data is sent. |
| `properties` | Object | Key-value pairs of property names and values for properties you've configured for the event type. An occurrence will be rejected if it contains properties not defined in the event type schema, or if the property type in the occurrence doesn’t match the property type defined in the event type schema. Learn more about [event properties](/docs/apps/developer-platform/add-features/app-events/reference#event-properties). |
| `extraData` | Object | Additional information which will be available to [timeline rendering templates](/docs/apps/developer-platform/add-features/app-events/reference#rendering-templates). Must be in valid JSON format. |
| `timelineIFrame` | Object | When included, the timeline card will include a hyperlink that allows users to open the linked contents in an iframe. Learn more about [using iframes](/docs/apps/developer-platform/add-features/app-events/reference#using-iframes). |
| `id` | String | A unique identifier for the event occurrence. Must be unique within the event type across all accounts. If not provided, HubSpot will generate a random UUID. When multiple events have the same ID within a year, the first will be accepted and all others will be rejected. |
## CRM record association
Each event occurrence must be associated with a CRM record, with the CRM object type defined by the event type schema. The app events API includes multiple fields for associating event occurrence data with CRM records. For all supported CRM objects, it's recommended to use the `objectId` field. However, there are some situations where you may want to use the other fields.
* `utk`/`email`: if you don't know the contact's ID, use the `utk` and/or `email` field for identification. Providing both of these identifiers also enables you to create and update contacts. For example:
* If `utk` matches an existing contact but the `email` doesn't match, HubSpot will update the contact (by `utk`) with the new email address.
* If no `objectId` is provided, the event occurrence will associate with an existing contact that matches the `utk`/`email`, or HubSpot will create a new contact if no match is found.
* Note that the `utk` alone cannot create new contacts. You should always include `email` with `utk` to ensure proper association.
* `domain`: for company association, you must provide the `objectId`, but you can also include `domain` to update the `domain` property of that company.
When the `email` field is provided with other identifiers (`objectId`, `utk`), HubSpot will always update the email property on the contact record. For example, if the contact's email is `test@hubspot.com` and you send an occurrence with the `objectId` and `email` as `test-updated@hubspot.com`, the contact's email address property will be updated to `test-updated@hubspot.com`.
Below is the priority order for the CRM record association properties, with the lowest number being the highest priority:
| Field | Priority | Description |
| ---------- | -------- | ------------------------------------------ |
| `objectId` | 1 | The CRM record ID (recommended). |
| `utk` | 2 | The contact usertoken (contacts only). |
| `email` | 3 | The contact email address (contacts only). |
| `domain` | 4 | The company domain (companies only). |
## Sending additional data
Beyond sending data to [event properties](/docs/apps/developer-platform/add-features/app-events/reference#event-properties) and [updating CRM properties via event occurrences](/docs/apps/developer-platform/add-features/app-events/reference#property-stamping), you can include additional data for [timeline rendering](/docs/apps/developer-platform/add-features/app-events/reference#rendering-templates) via the `extraData` object.
The `extraData` object can only contain valid JSON. If the JSON is malformed, the occurrence will be rejected and you'll receive an error response.
`extraData` field values can be accessed by the event type's `detailTemplate` using `{{extraData.fieldName}}` syntax. All attribute levels of `extraData` are available through dot notation, such as `{{extraData.person1.preferredName}}`.
For example, the templates below use the `customerName` and `loginLocation` property data, along with the `surveyData` field from `extraData` [sent via the event occurrence](#event-occurrences).

```json theme={null}
{
"eventTemplateId": "5488733",
"objectId": "769851",
"tokens": {
"customerName": "Tim",
"loginLocation": "mobileApp"
},
"extraData": {
"surveyData": [
{
"question": "How was your login experience?",
"answer": "Fine!"
},
{
"question": "How likely are you to recommend logging in to a co-worker?",
"answer": "Extremely likely"
}
]
}
}
```
```json theme={null}
"headerTemplate": "{{customerName}} logged in via the {{loginLocation}}.",
"detailTemplate": "#### Post-login survey\n{{#each extraData.surveyData}}\n- **{{question}}**: {{answer}}\n{{/each}}",
```
# Overview of app objects
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/app-objects/overview
Learn how to define app objects on the latest version of the developer platform.
This feature requires approval from HubSpot to use. If you're interested in applying to get access to app objects, or if you want to learn more about the functionality, please submit [this in-app form](https://app.hubspot.com/l/developer-overview/appObjectsEventsRequest).
App objects provide the same flexibility of custom objects (i.e., a unique name and customized schema definition) in tandem with the ability to standardize and manage that schema definition across all accounts that install your app.
After a HubSpot user installs an app with support for app objects, they can consult [this Knowledge Base](https://knowledge.hubspot.com/integrations/use-app-objects-from-connected-apps) article for details on how to use them in different tools within their account.
## Quickstart guide to using app objects
The [quickstart guide](/docs/apps/developer-platform/add-features/app-objects/quickstart-guide-to-app-objects) walks you through how to get up and running with a proof-of-concept app that uses a boilerplate example project and schema definition for an app object. You can then upload the associated configuration files to a project in your developer account, and test out an install using a developer test account.
## Reference
The app objects [reference article](/docs/apps/developer-platform/add-features/app-objects/reference) provides details for the required directory structure of your app, along with example schema files, including configuration file definitions. The article also provides important caveats such as limits imposed for app objects, and additional callouts you should keep in mind as you build your app.
# Get started with app objects
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/app-objects/quickstart-guide-to-app-objects
Learn how to build a new proof-of-concept app with support for app objects.
This feature requires approval from HubSpot to use. If you're interested in applying to get access to app objects, or if you want to learn more about the functionality, please submit [this in-app form](https://app.hubspot.com/l/developer-overview/appObjectsEventsRequest).
Learn how to build a proof-of-concept app where you'll configure an app object that you can test and use in a developer test account.
You'll need the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli) to create an app. In a terminal window, run the following command to update your version of the CLI:
```shell theme={null}
npm install -g @hubspot/cli@latest
```
You'll then need to authenticate your developer account by running the following command:
```shell theme={null}
hs account auth
```
* Follow the prompts to generate a Personal Access Key in your account, then copy and paste it into the terminal to save your configuration.
* It's recommended that you make this account your default by running the `hs account use` [command](/docs/developer-tooling/local-development/hubspot-cli/reference#set-default-account).
Run the command below in your terminal to create a new project and marketplace app with either the currently compatible features or an app object reference schema. You'll be prompted to provide a **name** and **folder** for the project.
```shell theme={null}
hs project create --project-base app --features app-object --distribution marketplace
```
Apps created using version 2025.2 of the developer platform use source code files, typically defined as `-hsmeta.json` configuration files, to configure the various features of your project.
App features are then created using a combination of subfolders from the main `/src/app` directory and other configuration files as needed. A full reference for your project structure can be found in the \[app configuration] guide.
To configure your project:
* Add one or more valid redirect URLs to the `src/app/app-hsmeta.json` file based on your local (or another non-production) OAuth server configuration.
To get started, you can use the [sample OAuth Node.js example](http://github.com/hubspot/oauth-quickstart-nodejs) and run it locally. It's already set up to work with `https://localhost:3000/oauth-callback` as the redirect URL configured in the boilerplate example code from the `hs project create` command you ran in the previous step.
* Change the `uid` property of the app in the `src/app/app-hsmeta.json` file and the other `*-hsmeta.json` configuration files in your project.
UIDs are used as a unique identifier for all your project's components and features. Once a feature and UID are created, changing or modifying the UID in subsequent deployments will force the platform to recognize it as different from previous features, which may not be intended.
When using app objects for your app, only the authorized "name" property definitions will be allowed based on your object request. As part of app object approval process, you should have separately received confirmation of your app's approved "name". For reference, the fully-qualified name (FQN) for your app object will be `a_`. For example, if your `appId` is `16858319` and your `name` property was `CARS`, then your FQN would be `a16858319_cars`.
Next you'll configure your app object schema:
* The configuration for your app object is defined within the `/src/app/app-objects` directory of your project. This directory is automatically included within the boilerplate project created by running the `hs project create` command above.
```shell theme={null}
project-folder/
└──src/
└── app/
├── app-hsmeta.json
└── app-objects/
└── app-object-hsmeta.json
```
* Within the `/src/app/app-objects` directory, an example `app-object-hsmeta.json` is defined as a starting point for configuring your actual app object schema. Open this file in your preferred editor, then update the schema accordingly:
* Consult the [app object component definition](/docs/apps/developer-platform/add-features/app-objects/reference#app-schema) reference file and customize the fields to the corresponding values for your app object.
* You should use the object name granted to your app during the preview process as the prefix for the file name, followed by `-object-hsmeta.json`. For example, if your app object name is "CAR", the resulting configuration file should be named `car-object-hsmeta.json`.
* Within the `config` object of your definition, the `name` field must match the name that was granted to your app during the review process, formatted in `UPPER_SNAKE_CASE` format.
* Note that once the properties and fields have been added to your app object schema and uploaded to your project in the next step, they cannot be removed.
For convenience during testing, the same app object name can be used across multiple apps to support your development lifecycle. Start with this proof-of-concept app, then as migrations become available, you can use the same name and schema definitions across your development, staging, and production apps. UIDs for each of these instances will need to be unique.
* When you're done editing your app object schema definition, and you're ready to commit these changes, run the following commands to save your changes:
```shell theme={null}
hs project upload
hs project deploy
```
* In a browser window, navigate to `https://app.hubspot.com/developer_projects/` to visit the projects UI and confirm the app and project have been created, built, and deployed correctly.
Now that your app object schema has been uploaded, you'll need to update the scopes defined in your `app-hsmeta.json` file to reflect the scopes created from the previous step. These scopes should be visible in the CLI logs after you ran `hs project upload`.
* Edit the `app-hsmeta.json` file and add the new scopes to the array of `requiredScopes` within the `auth` definition. For example, if your `appId` was `a12345`, then you'd edit the `auth` definition to the following:
```json theme={null}
"auth": {
"type" : "oauth",
"redirectUrls": ["http://localhost:3000/oauth-callback"],
"requiredScopes": [
"crm.objects.contacts.read",
"crm.objects.contacts.write",
"crm.app.objects.a12345_my_app_object.view",
"crm.app.objects.a12345_my_app_object.create",
"crm.app.objects.a12345_my_app_object.edit",
"crm.app.schemas.a12345_my_app_object.read",
"crm.app.objects.a12345_my_app_object.merge",
"crm.app.objects.a12345_my_app_object.delete",
"crm.app.schemas.a12345_my_app_object.properties.write"
],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
```
**Please note:**
Customers will only be able to see your app object if the `schemas.read`
scope is included in your app settings, and is requested during the
installation/reauthorization OAuth flow. It's highly recommended including
all app object scopes in your settings, but `schemas.read` is mandatory for
customers to be able to access it. For example, for an `appId` of `12345`,
you'd include `crm.app.schemas.a12345_MY_APP_OBJECT.read` as a required
scope.
Depending on the app you're testing with (prototype, development, staging,
production), you'll need to be mindful of where you add your scope
definitions. It's generally safest to include these
scopes as `conditionallyRequiredScopes` when you're ready for production.
Learn more about these [app scope types](/docs/apps/developer-platform/build-apps/authentication/scopes#scope-types).
* When you're done adding these scopes and you've saved your changes, run the following commands to commit your changes to the platform:
```shell theme={null}
hs project upload
hs project deploy
```
After uploading your project, you'll need to get the auth details for your app to copy over to your OAuth configuration:
* Click **Projects** in the *Development* navigation menu.
* Click the **name** of your new project.
* Click the **UID** of your app, then click the **Auth** tab.
* Copy the *Client ID* and *Client secret* from your new app and paste them into the corresponding locations in your local OAuth server's configuration, then restart your OAuth server.
Your app and the corresponding app object are now ready to test with an installed account.
If don't already have a [test account](/docs/getting-started/account-types#developer-test-accounts), you can create one in HubSpot:
* Navigate to **Test accounts** in the *Development* navigation menu, then click **Create developer test account**. Follow the prompts to create your new test account.
* In the left sidebar menu, navigate to **Projects**, click the **name** of your new project, then click the **UID** of your app in the component list.
* On the *Auth* tab, copy your app's install link.
* Use this link to install the app in your developer test account.
* Open the test account and navigate to the *Connected Apps* page, where you should see your installed app listed.
* In your test account, navigate to **CRM** > **Contacts**, then click the CRM object dropdown menu and confirm that your app object is available.
* You can then confirm that your schema definition conforms to your configuration file by creating a new record of your app object.
Now that you've successfully created your app object and you've tested it in a developer test account, you can use the OAuth access token associated with the installed test account to make requests to update data in the account directly via the objects API.
Read through the [objects API](/docs/guides/crm/using-object-apis) for more information on the API, but any requests specific to your app object will follow the same conventions as other standard objects in HubSpot. You'll need to use your app object's `objectTypeId` or `fullyQualifiedName` as the `objectType` path parameter in your request.
For example, the following code block demonstrates how to make cURL request to create a new record of your app object:
```shell theme={null}
curl --request POST \
--url https://api.hubapi.com/crm/v3/objects/ \
--header 'authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'content-type: application/json' \
--data '{
"properties": {
"additionalProp1": "string",
"additionalProp2": "string",
"additionalProp3": "string"
}
}'
```
You can find the `objectTypeId` for your app object by navigating to the records index page:
* Navigate to **CRM** > **Contacts** in the developer test account that you installed your app.
* Click the **dropdown** menu at the top of the page and select your **app object**.
* The `objectTypeId` will appear in the URL between the `/objects//views` portion.
## Next steps
Check out the [reference documentation](/docs/apps/developer-platform/add-features/app-objects/reference) for next steps on how to use app objects with different developer platform features:
* [Configure an app card](/docs/apps/developer-platform/add-features/app-objects/reference#app-card-schema)
* [Configure webhook subscriptions](/docs/apps/developer-platform/add-features/app-objects/reference#webhooks-component-definition)
* [Enable associations with other CRM object types](/docs/apps/developer-platform/add-features/app-objects/reference#app-object-associations)
# App objects reference
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/app-objects/reference
Reference information for building app objects, including configuration options for your project.
This feature requires approval from HubSpot to use. If you're interested in applying to get access to app objects, or if you want to learn more about the functionality, please submit [this in-app form](https://app.hubspot.com/l/developer-overview/appObjectsEventsRequest).
Below, find reference information for developer platform app features with app objects, including configuration file definitions, scopes details, and more.
## Project structure
* All project components must live within the `src` directory specified in the `hsproject.json` config file.
* All app features and components must live within the `app/` directory.
* App objects are defined within the `app-objects/` directory.
* App object associations are defined within the `app-object-associations/` directory.
* All component and feature instances are declared using `*-hsmeta.json` files. You can use any file name you'd like, as long as it ends in `-hsmeta.json` (e.g., `my-cool-object-hsmeta.json`). These files must live at the root level of their respective folder.
```shell theme={null}
project-folder/
└──src/
└── app/
├── app-hsmeta.json
└── app-objects/
└── app-object-hsmeta.json
└── app-object-associations/
└── app-object-to-contact-hsmeta.json
```
## App objects
To create an app object, include an `app-objects` component directory in the project, along with a configuration file.
```shell theme={null}
project-folder/
└───src/
└── app/
└── app-hsmeta.json
└── app-objects/
└── my-cool-object-hsmeta.json
```
Below are the configuration options available for `*-object-hsmeta.json`.
```json theme={null}
{
"uid": "car-app-object",
"type": "app-object",
"config": {
"name": "CAR",
"appPrefix": "Vroom",
"description": "An automobile in our warehouse.",
"singularForm": "Car",
"pluralForm": "Cars",
"primaryDisplayLabelPropertyName": "model",
"secondaryDisplayLabelPropertyNames": ["make"],
"settings": {
"hasRecordPage": true,
"allowsUserCreatedRecords": true,
"hasEngagements": true
},
"properties": [
{
"type": "string",
"fieldType": "text",
"name": "model",
"label": "Model",
"description": "The model of the car"
},
{
"type": "enumeration",
"fieldType": "select",
"name": "make",
"label": "Make",
"description": "The manufacturer of the car.",
"options": [
{
"label": "Ford",
"value": "ford",
"displayOrder": 0
},
{
"label": "Toyota",
"value": "toyota",
"displayOrder": 1
},
{
"label": "Chevrolet",
"value": "chevrolet",
"displayOrder": 2
}
]
}
]
}
}
```
Fields marked with \* are required.
| Field | Type | Description |
| --------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid`\* | String | A unique identifier for the app object. Must be globally unique within the project. |
| `type`\* | String | The type of component. Must match the name of the parent folder (`app-object`). |
| `name`\* | String | The name of your app object. Use the approved name that you received in your approved app confirmation. Must be uppercase snake case (`MY_OBJECT_NAME`). |
| `appPrefix` | String | A string that precedes the singular or plural name of the object in HubSpot's UI to help differentiate it from other objects. In this example, the `appPrefix` of `Vroom` and `singularForm` of `Car` would result in "Vroom Car" displaying in the UI. |
| `description`\* | String | the description of the object, which will display in HubSpot. |
| `singularForm`\* | String | The singular form of the object name. |
| `pluralForm`\* | String | The plural form of the object name. |
| `properties`\* | Array | A list of CRM properties defined for the object. Properties are defined using the same fields as the [properties API](/docs/api-reference/latest/crm/properties/guide). The resulting properties will automatically have `a_` prepended to it (e.g., `a12345_make`) on creation. You should not include the prefix in the config file. |
| `primaryDisplayLabelPropertyName`\* | String | The name of the property that should be used as the [primary display property](https://knowledge.hubspot.com/object-settings/create-custom-objects#:~:text=object%27s%20singular%20name.-,Primary%20display%20property,-%3A%20the%20property%20used). The value should match the name provided in the `properties` list (i.e., it should not include the generated `a_` prefix.) |
| `secondaryDisplayLabelPropertyNames`\* | Array | The list of properties that should be used as [secondary display properties](https://knowledge.hubspot.com/object-settings/create-custom-objects#:~:text=Secondary%20properties%3A). The value should match the name provided in the `properties` list (i.e., it should not include the generated `a_` prefix.) |
| `settings`\* | Object | An object containing object settings. |
| `hasRecordPage`\* | Boolean | Whether record pages exist for instances of this app object. When set to `false`, the index page will still exist, but it will not include links to individual record pages. |
| `allowsUserCreatedRecords`\* | Boolean | Whether end-users will be able to create records using the object. When set to `false`, users will not be able to create records in HubSpot |
| `hasEngagements`\* | Boolean | Whether the app object records support activities/engagements. When set to `false`, app object record pages will not include any engagement functionalities, such as the activity tab or activity-related timeline filters. |
The fully-qualified name (FQN) for your app object will be `a_`. For example: if your `appId` is `16858319` and the app object name is `CARS`, then the FQN would be `a16858319_CARS`. You'll use the FQN when setting scope values for your app objects.
## App schema
To create an app object, include an `app-hsmeta.json` configuration file in the `app` directory.
```shell theme={null}
project-folder/
└── src/
└── app/
└── app-hsmeta.json
```
Below are the configuration options available for `app-hsmeta.json`.
```json theme={null}
{
"uid": "app_object_poc_app",
"type": "app",
"config": {
"description": "An example to demonstrate how to build an app with an app object on developer projects.",
"name": "my first app object app",
"distribution": "marketplace",
"auth": {
"type": "oauth",
"redirectUrls": ["http://localhost:3000/oauth-callback"],
"requiredScopes": ["crm.objects.contacts.read", "crm.objects.contacts.write"],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
"permittedUrls": {
"fetch": ["https://api.hubapi.com"],
"iframe": [],
"img": []
},
"support": {
"supportEmail": "support@example.com",
"documentationUrl": "https://example.com/docs",
"supportUrl": "https://example.com/support",
"supportPhone": "+18005555555"
}
}
}
```
### App schema \*-hsmeta.json fields
Fields marked with \* are required
| Field | Type | Description |
| ----------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid`\* | String | An internal unique identifier for the app. Must be globally unique within the project. Can be any string up to 64 characters. Characters can be uppercase or lowercase, and can include numbers, underscores (`_`), dashes (`-`), and periods (`.`). |
| `type`\* | String | The type of component. Must match the name of the parent folder (`app`). |
| `description`\* | String | A description of what the app does for the installing user. Can be any string up to 8192 characters. |
| `name`\* | String | The name of the app, which will display in HubSpot. Can be any string up to 200 characters. Must not start or end with a whitespace character. |
| `distribution`\* | String | The method of app distribution. Must be set to `marketplace` for the app to be eligible for HubSpot Marketplace listing. |
| `auth`\* | Object | An object containing the app's authentication method details. See the [auth table](#auth-fields) for details. |
| `permittedUrls` | Object | An array containing the URLs that the app is allowed to call. URLs must use the HTTPS scheme and must contain an [authority](https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Web_mechanics/What_is_a_URL#authority), followed by an optional path prefix if needed. |
| `supportEmail` | String | A valid email address that users can contact for support. |
| `documentationUrl` | String | The external URL that users can navigate to for supporting documentation. Must use HTTPS. |
| `supportUrl` | String | The external URL that users can navigate to for additional support. Must use HTTPS. |
| `supportPhone` | String | The phone number that users can contact for support. Must start with a plus sign (`+`). |
### auth fields
Fields marked with \* are required.
| Field | Type | Description |
| ------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`\* | String | The type of authentication. Must be set to `oauth` for the app to use OAuth authentication. |
| `redirectUrls`\* | Array | A list of URLs that the OAuth process is allowed to reroute back to. Each app must have at least one auth redirect URL, and it must use HTTPS. The only exception is that `http://localhost` is allowed for testing. |
| `requiredScopes`\* | Array | A list of your app's required scopes. Each app must include at least one scope, and the installing user must grant these scopes to successfully install the app. [Learn more about scopes below](#scopes). |
| `optionalScopes` | Array | A list of your app's optional scopes. These scopes can be excluded from the authorization during installation if the account or user installing the app doesn't have the proper permissions. In that case, the scope will not be included in the resulting refresh token or access token. [Learn more about scopes below](#scopes). |
| `conditionallyRequiredScopes` | Array | A list of scopes that are required only when included in the `scope` query parameter of the install URL. [Learn more about scopes below](#scopes). |
### Scopes
In the `auth` field of an app configuration file, you can specify three [types of scopes](/docs/apps/legacy-apps/public-apps/overview#scope-types): required scopes, conditionally required scopes, and optional scopes. If you're just getting started with app objects, you should only include your app object scopes as `conditionallyRequiredScopes`. This will allow you to silo your new features to specific customers by including the app object scopes in the install URL.
App object scopes use the following format:
`crm.app.schemas..read`
For example, for an app object with the FQN `a16858319_cars`, the `read` scope would be:
`crm.app.schemas.a16858319_cars.read`.
At a minimum, your app must include the above `read` scope to enable customers to access the object. It's recommended to include all app object scopes in your app, as shown below.
```json theme={null}
"auth": {
"type" : "oauth",
"redirectUrls": ["http://localhost:3000/oauth-callback"],
"requiredScopes": [
"crm.objects.contacts.read",
"crm.objects.contacts.write",
"crm.app.objects.a12345_MY_APP_OBJECT.view",
"crm.app.objects.a12345_MY_APP_OBJECT.create",
"crm.app.objects.a12345_MY_APP_OBJECT.edit",
"crm.app.schemas.a12345_MY_APP_OBJECT.read",
"crm.app.objects.a12345_MY_APP_OBJECT.merge",
"crm.app.objects.a12345_MY_APP_OBJECT.delete",
"crm.app.schemas.a12345_MY_APP_OBJECT.properties.write"
],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
```
For a full list of available scopes, see the [scopes reference](/docs/apps/legacy-apps/authentication/scopes).
## Webhooks component definition
To define a set of webhook subscriptions for your app, include a `webhooks` directory in the project, along with a `*-hsmeta.json` configuration file.
```shell theme={null}
project-folder/
└── src/
└── app/
├── app-hsmeta.json
└── webhooks/
└── webhook-hsmeta.json
```
Below are the available configuration options for the `*-hsmeta.json` file.
```json theme={null}
{
"uid": "webhooks",
"type": "webhooks",
"config": {
"settings": {
"targetUrl": "https://example.com/webhook",
"maxConcurrentRequests": 10
},
"subscriptions": {
"crmObjects": [
{
"subscriptionType": "object.creation",
"objectType": "contact",
"active": true
},
{
"subscriptionType": "object.propertyChange",
"objectType": "car_app_object",
"propertyName": "carProperty",
"active": true
}
],
"legacyCrmObjects": [
{
"subscriptionType": "contact.propertyChange",
"propertyName": "lastname",
"active": true
},
{
"subscriptionType": "contact.deletion",
"active": true
}
],
"hubEvents": [
{
"subscriptionType": "contact.privacyDeletion",
"active": true
}
]
}
}
}
```
### Webhook \*-hsmeta.json fields
Fields marked with \* are required.
| Field | Type | Description |
| ------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uid`\* | String | An internal unique identifier for the webhook component. |
| `type`\* | String | The type of component, which should be `webhooks` in this case. |
| `settings`\* | Object | An object that specifies two fields: `targetUrl`, which is the publicly available URL for HubSpot to call where event payloads will be delivered, and `maxConcurrentRequests`, which is the upper threshold of HTTP requests that HubSpot will make in a given time frame. |
| `subscriptions`\* | Object | An object that specifies the subscription types your app will subscribe to. |
| `crmObjects` | Array | An array containing event subscription definitions. This is the standard array to include, and should be used for all events in the new format (`object.*`). Classic webhook subscription types should instead be included in `legacyCrmObjects` and `hubEvents` arrays, depending on the event. |
| `legacyCrmObjects` | Array | An array containing classic subscription types, such as `contact.creation` and `deal.deletion`. |
| `hubEvents` | Array | An array containing the classic subscription types `contact.privacyDeletion` and `conversation.*` |
**Please note:** the `associationChange` event is not currently supported for app objects.
For each `subscription` object, the following fields can be specified, based on the subscription definition type you're subscribed to (i.e., `crmObjects`, `legacyCrmObjects`, or `hubEvents`) or whether you're subscribing to a specific property change (e.g., `contact.propertyChange`).
| Field | Type | Description |
| ------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptionType` | String | The type of event being subscribed to. |
| `objectType` | String | For subscriptions specified within the `crmObjects` array, this specifies the CRM object your app is subscribing to. For subscribing to changes to an app object, include your app object name for this field (e.g., `car_app_object`). |
| `propertyName` | String | For property change subscriptions, this specifies which property will trigger the webhook event. |
| `active` | Boolean | Whether webhook events will be triggered for this subscription. |
## App card schema
To create an app card that appears on an app object record page, include a `cards` component directory in the project, along with a configuration file.
```shell theme={null}
project-folder/
└── src/
└── app/
├── app-hsmeta.json
└── cards/
└── my-app-card-hsmeta.json
└── my-app-card.jsx
```
* Make sure you've run `hs project upload` after you created your app object component and the associated configuration files.
* In your `my-app-card-hsmeta.json` file, add your app object UID to the `objectTypes` array (e.g., `"app_object_uid"` in this example). Each of the available fields in the `.json` file are detailed in the [table](#app-card-*-hsmeta.json-fields) below.
```json theme={null}
{
"uid": "my-app-card",
"type": "card",
"config": {
"name": "My app card",
"description": "An example description of the card, which lives on contact records.",
"previewImage": {
"file": "./preview.png",
"altText": "A short description of the preview image"
},
"entrypoint": "/app/cards/MyCard.jsx",
"location": "crm.record.tab",
"objectTypes": ["contacts", "app_object_uid"]
}
}
```
* After you've saved the changes to your `example-card-hsmeta.json` file, run `hs project upload`.
Cards are added automatically to the default view for app objects. If the card doesn't automatically show up, learn how to [add cards to CRM records](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card#view-the-card-in-hubspot).
### App card \*-hsmeta.json fields
Fields marked with \* are required.
| Field | Type | Description |
| ---------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uid`\* | String | The card's unique identifier. This can be any string, but should meaningfully identify the card. HubSpot will identify the card by this ID so that you can change the card's title without removing historical or stateful data, such as the position on the CRM record. |
| `type` | String | The type of component, which should be `card` in this case. |
| `config` | Object | An object containing configuration details. |
| `name`\* | String | The card's title, as displayed in HubSpot's UI. |
| `description` | String | A description of the card. |
| `previewImage` | Object | An object containing the `file` and `altText` fields. The `file` field is the relative path to the preview image. Valid file extensions are png, jpeg, jpg, or gif. The maximum file size is 5.0 MB. The `altText` field is a short description of the image. |
| `entrypoint`\* | String | The file path of the card's front-end React code. |
| `location`\* | `crm.record.tab` \| `crm.record.sidebar` \| `helpdesk.sidebar` | Where the card appears in HubSpot's UI. Learn more about [extension location](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference#supported-locations). |
| `objectTypes`\* | Array | The types of CRM records that the card will appear on. |
## App object associations
To enable associations between your app object and other CRM objects, include an `app-object-associations` component directory in the project, along with a `*-hsmeta.json` configuration file for each record type you want to define an association for.
The directory structure and example `*-hsmeta.json` file below provide an example of defining an association between an app object with a `uid` of `car-app-object` and contacts.
```shell theme={null}
project-folder/
└── src/
└── app/
├── app-hsmeta.json
└── app-object-associations/
└── car-to-contact-hsmeta.json
```
Below are the available configuration options for your app object association details in the corresponding `*-hsmeta.json` file.
```json theme={null}
{
"uid": "car_to_contact_association",
"type": "app-object-association",
"config": {
"firstObjectType": "car-app-object",
"secondObjectType": "CONTACT",
"firstToSecondBaseLimit": 5000,
"secondToFirstBaseLimit": 100,
"labels": [
{
"name": "product_manager",
"firstObjectTypeLabel": "Managed Widget",
"secondObjectTypeLabel": "Product Manager",
"firstToSecondLabelLimit": 1,
"secondToFirstLabelLimit": 50
},
{
"name": "quality_inspector",
"firstObjectTypeLabel": "Inspected Widget",
"secondObjectTypeLabel": "Quality Inspector"
},
{
"name": "technical_support",
"firstObjectTypeLabel": "Supported Widget",
"secondObjectTypeLabel": "Technical Support Rep",
"firstToSecondLabelLimit": 3
}
]
}
}
```
### Association \*-hsmeta.json fields
Fields marked with \* are required.
| Field | Type | Description |
| ------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid`\* | String | An internal unique identifier for the association. Must be globally unique within the project. |
| `type`\* | String | The type of component. Must match the name of the parent folder (`app-object-association`). |
| `config`\* | Object | An object containing the object association configuration.
This object includes the `firstObjectType`, `secondObjectType`, `firstToSecondBaseLimit`, `secondToFirstBaseLimit`, and `labels` properties, which are defined in detail in the table rows below. |
| `firstObjectType` | String | The first object type in the association. Can be an app object UID (e.g., `"car-app-object"`) or the `fullyQualifiedName` of a HubSpot-defined object (e.g., `"CONTACT"`). |
| `secondObjectType` | String | The second object type in the association. Can be an app object UID (e.g., `"car-app-object"`) or the `fullyQualifiedName` of a HubSpot-defined object (e.g., `"CONTACT"`). |
| `firstToSecondBaseLimit` | Number | The maximum number of `secondObjectType` records that a single `firstObjectType` record can be associated to.
The maximum value is 10,000. Note that if this property is not provided, the corresponding association limit will default to [*Many*](https://knowledge.hubspot.com/object-settings/set-limits-for-record-associations). |
| `secondToFirstBaseLimit` | Number | The maximum number of `firstObjectType` records that a single `secondObjectType` record can be associated to.
The maximum value is 10,000. Note that if this property is not provided, the corresponding association limit will default to [*Many*](https://knowledge.hubspot.com/object-settings/set-limits-for-record-associations). |
| `labels` | Array | An array of association labels that describe the relationships between associated records. Each label is an object with the following properties available:
`name`\* : Unique identifier for the first object type in this association.
`firstObjectTypeLabel`\* : Display label for the first object type in this association.
`secondObjectTypeLabel`\* : Display label for the second object type in this association.
`firstToSecondLabelLimit`: The maximum number of `secondObjectType` records that a single `firstObjectType` record can be associated with using this label. Cannot be greater than the base limit.
Note that if this property is not provided, the corresponding association limit will default to [*Many*](https://knowledge.hubspot.com/object-settings/set-limits-for-record-associations).
`secondToFirstLabelLimit`: The maximum number of `firstObjectType` records that a single `secondObjectType` record can be associated with using this label. Cannot be greater than the base limit.
Note that if this property is not provided, the corresponding association limit will default to [*Many*](https://knowledge.hubspot.com/object-settings/set-limits-for-record-associations).
|
# Configure a webhook subscription
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/configure-webhooks
Learn how to define a webhook subscription on the latest version of the developer platform.
Webhooks enable your app to receive real-time notifications when specific events occur in a HubSpot account. Instead of repeatedly polling HubSpot's APIs to check for changes, webhooks push event data to your application as soon as it happens.
When you configure a webhook subscription, HubSpot will send a `POST` request to your specified endpoint whenever subscribed events occur. Your application receives the event payload, processes it, and returns a `2xx` status code to acknowledge receipt.
## When to use webhooks
Use webhooks when you need to:
* Sync data in real-time between HubSpot and external systems (e.g., updating a contact in your database when they're modified in HubSpot).
* Trigger automated workflows based on HubSpot events (e.g., sending a Slack notification when a deal closes).
* Monitor specific changes without constant API polling (e.g., tracking when contacts unsubscribe from emails).
Common use cases include CRM synchronization, notification systems, data warehousing, and integration platforms that need to stay current with HubSpot data.
## Project structure
To define a set of webhook subscriptions for an app, create a `webhooks` directory within `src/app/`. Then, add a configuration file to that directory using the naming convention `*-hsmeta.json`.
```shell theme={null}
project-folder/
└── src/
└── app/
├── app-hsmeta.json
└── webhooks/
└── webhook-hsmeta.json
```
## Webhook configuration
Below are the available configuration options for the `*-hsmeta.json` file.
```json theme={null}
{
"uid": "webhooks",
"type": "webhooks",
"config": {
"settings": {
"targetUrl": "https://example.com/webhook",
"maxConcurrentRequests": 10
},
"subscriptions": {
"crmObjects": [
{
"subscriptionType": "object.creation",
"objectType": "contact",
"active": true
}
],
"legacyCrmObjects": [
{
"subscriptionType": "contact.propertyChange",
"propertyName": "lastname",
"active": true
},
{
"subscriptionType": "contact.deletion",
"active": true
}
],
"hubEvents": [
{
"subscriptionType": "contact.privacyDeletion",
"active": true
}
]
}
}
}
```
Fields marked with \* are required.
| Field | Type | Description |
| ------------------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid`\* | String | An internal unique identifier for the webhook component. |
| `type`\* | String | The type of component, which should be `webhooks` in this case. |
| `settings`\* | Object | An object that specifies two fields: `targetUrl`, which is the publicly available URL for HubSpot to call where event payloads will be delivered, and `maxConcurrentRequests`, which is the upper threshold of HTTP requests that HubSpot will make in a given time frame. |
| `subscriptions`\* | Object | An object that specifies the subscription types your app will subscribe to. |
| `crmObjects` | Array |
An array containing event subscription definitions. This is the standard array to include, and should be used for all events in the [new format](/docs/apps/legacy-apps/public-apps/create-generic-webhook-subscriptions) (`object.*`).
[Classic webhook subscription types](/docs/api-reference/latest/webhooks#webhook-subscriptions) should instead be included in `legacyCrmObjects` and `hubEvents` arrays, depending on the event.
|
| `legacyCrmObjects` | Array | An array containing [classic subscription types](/docs/api-reference/latest/webhooks#webhook-subscriptions), such as `contact.creation` and `deal.deletion`. |
| `hubEvents` | Array | An array containing the classic subscription types `contact.privacyDeletion` and `conversation.*` |
For each `subscription` object, the following fields can be specified, based on the subscription definition type you're subscribed to (i.e., `crmObjects`, `legacyCrmObjects`, or `hubEvents`) or whether you're subscribing to a specific property change (e.g., `contact.propertyChange`).
| Field | Type | Description |
| ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------- |
| `subscriptionType` | String | The type of event being subscribed to. |
| `objectType` | String | For subscriptions specified within the `crmObjects` array, this specifies the CRM object your app is subscribing to. |
| `propertyName` | String | For property change subscriptions, this specifies which property will trigger the webhook event. |
| `active` | Boolean | Whether webhook events will be triggered for this subscription. |
# Define a custom workflow action
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/custom-workflow-actions
Learn how to define a custom workflow action on the latest version of the developer platform.
Custom workflow actions extend HubSpot's automation capabilities by allowing your app to perform specialized tasks within workflows. When users build workflows in HubSpot, your custom actions will appear in the action selection panel alongside other workflow actions, enabling seamless integration of your app's functionality into HubSpot's automation ecosystem.
## When to use custom workflow actions
Use custom workflow actions when you need to:
* Integrate external services into HubSpot workflows (e.g., sending data to a third-party API, creating records in external systems).
* Perform complex calculations or data transformations that aren't available in standard workflow actions.
* Automate app-specific tasks that users would otherwise need to do manually (e.g., generating documents or triggering notifications in your platform).
Custom workflow actions are ideal when your app needs to be part of HubSpot's automation ecosystem, rather than just responding to events (which is better handled by [webhooks](/docs/apps/developer-platform/add-features/configure-webhooks)).
## Project structure
To define a custom workflow action for an app, create a `workflow-actions` directory within `src/app/`. Then, add a configuration file to that directory using the naming convention `*-hsmeta.json`.
```shell theme={null}
project-folder/
└── src/
└── app/
└── app-hsmeta.json
└── workflow-actions/
└── workflow-action-hsmeta.json
```
## Custom workflow action configuration
Below are the available configuration options for the `*-hsmeta.json` file.
You can also use the in-app [custom action builder](/docs/api-reference/latest/automation/workflow-actions/custom-action-builder) to create workflow actions using a visual tool, then export the JSON to use in the workflow action configuration file.
```json theme={null}
{
"uid": "simple_notification_action",
"type": "workflow-action",
"config": {
"actionUrl": "https://example.com",
"isPublished": false,
"supportedClients": [
{
"client": "WORKFLOWS"
}
],
"inputFields": [
{
"typeDefinition": {
"name": "message",
"type": "string",
"fieldType": "textarea"
},
"supportedValueTypes": ["STATIC_VALUE"],
"isRequired": true
},
{
"typeDefinition": {
"name": "priority",
"type": "enumeration",
"fieldType": "select",
"options": [
{
"value": "high",
"label": "High Priority"
},
{
"value": "normal",
"label": "Normal Priority"
},
{
"value": "low",
"label": "Low Priority"
}
]
},
"supportedValueTypes": ["STATIC_VALUE"],
"isRequired": true
}
],
"labels": {
"en": {
"actionName": "My Custom Action (via Projects V2)",
"actionDescription": "Sends a notification with custom message and priority level",
"actionCardContent": "Send {{priority}} priority notification",
"inputFieldLabels": {
"message": "Notification Message",
"priority": "Priority Level"
},
"inputFieldDescriptions": {
"message": "Enter the message to be sent in the notification",
"priority": "Select the priority level for this notification"
}
}
},
"objectTypes": ["CONTACT"]
}
}
```
Fields marked with \* are required.
| Field | Type | Description |
| ----------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid`\* | String | An internal unique identifier for the workflow action. |
| `type`\* | String | The type of component, which should be `workflow-action` in this case. |
| `actionUrl`\* | String | The webhook URL of the API to deliver a workflow execution request. |
| `isPublished` | Boolean | Determines whether the definition is visible in accounts that installed your app. By default, this is set to `false`. |
| `supportedClients`\* | Array of objects | Specifies the clients that the custom workflow action supports. Each object in the array should have a client key with a string value indicating the client type (e.g., `WORKFLOWS`). |
| `inputFields` | Array | The values for the inputs that the user has filled out. |
| `typeDefinition.name` | String | The name or key of the input field. |
| `typeDefinition.type` | String | The value type that the input field should expect. |
| `typeDefinition.fieldType` | String | The type of field that appears to users creating the workflow. |
| `typeDefinition.options` | Array | For enumeration types, this field provides a list of options. Each option must have a `value`, based on the input the user provides, and a `label`, which identifies the option in the workflows tool. |
| `inputFieldDependencies` | Array | A list of rules that define the relationships between two or more inputs, based on their `dependencyType`. Learn more in the example [here](/docs/api-reference/latest/automation/workflow-actions/custom-action-guide#example-%233). |
| `labels.`\* | String | Locale key that maps to the locale definition. At a minimum, an english label (`en`) and its definition must be defined. |
| `labels..inputFieldDescriptions` | Object | An object that defines the details for the inputs for your action. In the example above, this object includes `message` and `priority` fields. |
| `labels..inputFieldOptionLabels` | Object | An object that's required if your input field(s) have options. Provides a map of input field option labels, keyed by the option's value or label. |
| `labels..outputFieldLabels` | Object | An object that maps the definitions from `outputFields` to the corresponding labels that appear in the workflows tool. |
| `labels..actionName`\* | String | The action's name as shown in the *Choose an action* panel in the workflows editor. |
| `labels..appDisplayName`\* | String | The name of the section in the *Choose an action* panel where all actions for the app appear. If `appDisplayName` is defined for multiple actions, the first one found will be used. |
| `labels..actionCardContent` | String | A summarized description shown in the action's card. |
| `labels..executionRules` | Object | An object that maps the definitions from your `executionRules` to messages that will appear for execution results in the workflow's history. |
| `objectTypes` | Array | The available CRM object types that this action can be used with. If empty, the action will be available for all object types. |
| `outputFields` | Array | The values that the action will output that can be used by later actions in the workflow. A custom action can have 0, 1, or many outputs. |
| `executionRules` | Object | A list of definitions you can specify to surface errors from your service to the user creating the workflow. |
# Connect external MCP servers to HubSpot agents (BETA)
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/mcp-server
Add an MCP server component to your app to connect external MCP servers to HubSpot agents.
By adding a Model Context Protocol (MCP) server component to your app, you can connect external MCP servers to HubSpot agents. Once defined and installed in a customer account, these servers become available as agent tools within Agent Builder, allowing agents to interface with external data and functionality provided by your MCP server.
**Please note:** this functionality is currently in public beta. By participating in this beta, you agree to HubSpot's [Developer Terms](https://legal.hubspot.com/hs-developer-terms) and [Developer Beta Terms](https://legal.hubspot.com/hubspot-beta-terms). The functionality is still under active development and is subject to change based on testing and feedback.
## Prerequisites
To get started with MCP server components, you'll need:
* A HubSpot account with access to the MCP server public beta.
* The HubSpot CLI installed and authenticated with your HubSpot account.
* A HubSpot app that's configured for private or marketplace distribution.
* The project's `hsproject.json` file must be configured to use the latest beta platform version (`2026.09-beta` or newer).
* The app's scopes must include `oauth` at a minimum, even if your MCP server does not read HubSpot data.
* A functioning MCP server that's deployed and accessible via a public URL. Note that HubSpot's authorization flow does not support Dynamic Client Registration (DCR). You will need to generate a valid client ID for customers to connect to your MCP server, and this is a required field in the component schema.
**Please note:** your MCP server must be reachable from the public internet, not just your local machine. During the connection flow, HubSpot's backend performs OAuth metadata discovery and token exchange server-side. URLs pointing to `localhost` or private networks will fail with a generic "Authentication failed" error, even if the server is running locally.
For local development, use a tunneling service such as [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/create-local-tunnel/) to expose your server via a public HTTPS URL, and set `mcpUrl` to the tunnel URL.
## Marketplace distribution
Marketplace-distributed apps with MCP server components must complete Ecosystem review and approval before installed customers can access the server. You can use feature flags to test the component with up to five installed accounts before approval. Learn more about [distribution and feature flags](#distribution-and-feature-flags).
## Integration overview
At a high level, integrating with MCP server components involves:
1. **[Update your project to 2026.09-beta](#update-your-project-to-202609-beta):** update your app and project to the `2026.09-beta` platform version before adding an MCP server component.
2. **[Defining the server](#define-an-mcp-server-component):** in your HubSpot project, add an MCP server component and configure it with your MCP server's connection details.
3. **[Implementing OAuth](#mcp-server-oauth-requirements):** ensure your server implements the OAuth endpoint requirements, including discovery, PKCE support, and the fixed redirect URI.
4. **Deploying to HubSpot:** upload your project to your HubSpot account using the HubSpot CLI.
5. **[Installing the app](#install-the-app):** install the app into a target HubSpot account.
6. **[Testing the connection](#test-the-connection-in-hubspot):** test the connection to the MCP server from your HubSpot account.
7. **[Distribution and feature flags](#distribution-and-feature-flags):** for marketplace-distributed apps, enable testing for selected installed accounts and complete Ecosystem review before broader release.
8. **[Testing agents with your MCP tools](#testing-agents-with-your-mcp-tools):** test your MCP tools in the Agent Builder.
## Update your project to 2026.09-beta
Before adding an MCP server component, update your project's `platformVersion` to `2026.09-beta`. This setting is defined in the `hsproject.json` file in the project directory. The `hsproject.json` file is located at the project root.
For example:
```json theme={null}
{
"name": "mcp-server-project",
"platformVersion": "2026.09-beta",
"srcDir": "src"
}
```
## Define an MCP server component
To define an MCP server component, you must create a `*-hsmeta.json` file within the `mcp-server` directory within `app/`. This file will contain your MCP server configuration options.
Below are the configuration options available for MCP server type schemas (`*-hsmeta.json`).
```json theme={null}
{
"uid": "my-mcp-server",
"type": "mcp-server",
"config": {
"name": "My MCP Server",
"description": "Provides context and tools for specific external data.",
"mcpUrl": "https://api.myserver.com/mcp",
"mcpClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"version": "1.0.0",
"requiredScopes": ["data:read", "data:write"],
"logoUrl": "https://image.com/image",
"enabled": true,
"websiteUrl": "https://www.mysite.com",
"privacyPolicyUrl": "https://www.mysite.com/privacy"
}
}
```
A unique identifier for this component within your project.
The type of component. Must be set to `mcp-server`.
The human-readable name of the server, which will appear in the agent tool selector.
A brief description of the server's capabilities, used by customers to understand when to use your MCP server.
The full HTTPS URL to your MCP server's endpoint. This URL must be publicly accessible from HubSpot's servers. See the [reachability warning above](#prerequisites) for details.
The client ID registered to your MCP OAuth server to be used with the HubSpot app this component belongs to. This ID will be used to identify all requests being made by agents to your MCP server. For more control and visibility into underlying customer requests, you can use multiple apps and client IDs to monitor this traffic in more detail.
The version of the MCP server definition. Default is `1.0.0`.
A list of OAuth scopes accepted by your MCP server's authorization system, which correspond to a permission boundary enforced by your MCP server (e.g., `["data:read", "data:write"]`). These are not HubSpot scopes. Configure the scopes that your authorization server requires or supports, whether or not it lists every supported scope in its [OAuth authorization server metadata](#mcp-server-oauth-requirements). These scopes will be presented during the connection flow and determine which tools and data are accessible to authenticated clients. Set to an empty array (`[]`) to connect without requesting specific scopes, which allows access to all tools the server exposes.
URL to a public image file for the server logo, which will be displayed in the agent tool selector.
Whether the MCP server is enabled. Default is `true`.
URL of your server's website or external documentation.
URL of your server's privacy policy.
Once you've defined your MCP server component, upload your project to HubSpot using the `hs project upload` command.
If you receive an error of `unsupported type: mcp-server`, ensure the `platformVersion` in your project's `hsproject.json` file is configured to `2026.09-beta` or newer.
## MCP server OAuth requirements
HubSpot connects to your MCP server using an OAuth 2.1 authorization code flow with PKCE. HubSpot's backend initiates and coordinates this flow: it fetches OAuth metadata and exchanges the authorization code for tokens server-side. However, the authorization step where the user grants access happens through their browser, which is redirected to your MCP server's authorization endpoint.
```mermaid actions={false} theme={null}
sequenceDiagram
participant B as User's Browser
participant H as HubSpot Backend
participant M as Your MCP Server
B->>H: User clicks "Connect and add"
H->>M: GET /.well-known/oauth-authorization-server
M->>H: Authorization Server Metadata (JSON)
H->>B: Redirect to MCP server's authorization endpoint
Note over H: Includes client_id, code_challenge (S256), redirect_uri, and state parameters
B->>M: GET /authorize?response_type=code&client_id=...&code_challenge=...
Note over M: User authorizes the connection
M->>B: Redirect to HubSpot callback with auth code
B->>H: Authorization code delivered
H->>M: POST /token (code + code_verifier)
M->>H: Access token + refresh token
Note over H: Connection established
```
### Required endpoints
Your MCP server must implement the following endpoints, per the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) and [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414).
| Endpoint | Description |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/.well-known/oauth-authorization-server` | Returns OAuth 2.0 authorization server metadata, including `authorization_endpoint` and `token_endpoint`. HubSpot fetches this from the domain root of your `mcpUrl`. For example:
|
| Authorization endpoint | Accepts `response_type=code` requests with PKCE parameters (`code_challenge` and `code_challenge_method=S256`). Must redirect the user back to HubSpot's fixed callback URL with an authorization code. |
| Token endpoint | Exchanges authorization codes for access and refresh tokens, and refresh tokens for new access tokens. Must validate the PKCE `code_verifier` against the original `code_challenge`. |
### Authorization
HubSpot redirects to your server's authorization endpoint using PKCE with the S256 challenge method. Your authorization endpoint must support the following:
* **Client ID:** HubSpot passes the `mcpClientId` from your component schema as the `client_id` parameter throughout the flow. Your server must recognize this value.
* **PKCE (S256):** HubSpot includes `code_challenge` and `code_challenge_method=S256` parameters in the authorization request.
* **Fixed redirect URI:** HubSpot always uses `https://oauth-redirect.hubspot.com/callback/mcp_server` as the redirect URI. Your OAuth server must be configured to accept this URI.
* **Scopes:** if your configuration includes `requiredScopes`, these scopes will be associated with the connection. Your authorization endpoint should grant the requested scopes and your token endpoint should include them in the access token response.
**Please note:** the redirect URI `https://oauth-redirect.hubspot.com/callback/mcp_server` is fixed and cannot be customized. You must register this URI in your MCP server's OAuth configuration, or the authorization flow will fail.
### Token exchange
After the user authorizes, HubSpot's backend exchanges the authorization code for tokens by sending a `POST` request to your token endpoint. This request includes the PKCE `code_verifier` for validation. Because this exchange happens server-side, your token endpoint must be publicly reachable.
HubSpot currently requires the authorization code exchange to return both an access token and a refresh token. Your authorization server must also support the `refresh_token` grant so HubSpot can obtain a new access token after the current one expires.
### Refresh tokens and `offline_access`
The refresh token requirement is specific to HubSpot's MCP OAuth client. [OAuth 2.1](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/) allows the authorization server to decide whether to issue refresh tokens, and the [MCP authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/index#refresh-tokens) states that clients must not assume they will receive one.
Some authorization servers use the optional OpenID Connect [`offline_access` scope](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess) to request a refresh token. Other authorization servers issue refresh tokens without this scope or may reject or ignore it. HubSpot does not automatically add `offline_access` to the authorization request.
Add `offline_access` to `requiredScopes` only when your authorization provider requires or accepts it. For example:
```json theme={null}
{
"requiredScopes": ["data:read", "data:write", "offline_access"]
}
```
If the authorization server metadata includes `offline_access` in `scopes_supported`, you can use that as a signal that the server supports the scope. Its absence does not necessarily mean the scope is unsupported, so confirm the expected configuration in your provider's documentation and verify that the token response includes a refresh token.
### Example metadata response
Below is an example of the JSON your `/.well-known/oauth-authorization-server` endpoint should return:
```json theme={null}
{
"issuer": "https://api.myserver.com",
"authorization_endpoint": "https://api.myserver.com/authorize",
"token_endpoint": "https://api.myserver.com/token",
"scopes_supported": ["data:read", "data:write"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"]
}
```
## Install the app
With your project deployed, you can install the app into a target HubSpot account. To do this, you'll navigate into HubSpot to get the install URL, along with the app's client ID and secret for your OAuth server. For private distribution, use the [privately distributed OAuth app install flow](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#install-a-privately-distributed-oauth-app). For marketplace distribution, use the [OAuth marketplace app install flow](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#install-an-oauth-marketplace-app).
* To open the project in HubSpot from the terminal, run `hs project open` from within the project directory.
* From the project page, navigate to the **App** in the left sidebar.
* Click the **Distribution** tab.
* Click **Copy install link**, then paste it into your browser to install the app into the desired target account.
* To get your app's client ID and secret for your OAuth server, click the **Auth** tab, then copy the *Client ID* and *Client secret* values.
## Test the connection in HubSpot
Once the app is installed, test the connection to the MCP server from your HubSpot account:
* In the account where you uploaded the project, navigate to **Development** > **Projects**, then click the **name** of the project where the app is installed.
* In the left sidebar, navigate to the **MCP server component**.
* Click **Test connection**.
* In the dialog box, click **Connect**.
* A pop-out window will appear as the OAuth handshake is attempted using the details defined in your MCP server component's `*-hsmeta.json` file. If successful, you'll see a message indicating the server is reachable and responding as expected.
If the connection does not complete or stops working after the access token expires, verify the following:
* The initial token response includes a `refresh_token`.
* The authorization server supports the `refresh_token` grant.
* If the provider requires the `offline_access` scope, it is included in the component's `requiredScopes` configuration.
Authorization servers that cannot issue refresh tokens are not currently supported by HubSpot's MCP server connection flow.
## Distribution and feature flags
MCP server components are available in public beta for privately distributed apps and marketplace-distributed apps. For apps that are or will be distributed through the HubSpot Marketplace, HubSpot automatically creates the `hs-release-mcp-server` feature flag in an `OFF` state when you add the `mcp-server` component. This keeps the server hidden from installed customers until your app completes Ecosystem review and approval.
While you develop and prepare for review, use the [feature flags API](/docs/api-reference/latest/app-management/feature-flags/guide) to set the `hs-release-mcp-server` flag to `ON` for up to five installed accounts for testing. After approval, you can set the app-level `defaultState` to `ON` for broader customer access.
The following limits apply to marketplace-distributed apps with MCP server components:
* Portal-level flag writes require the app to be installed in the target account.
* Apps that haven't completed Ecosystem approval can set `ON` portal overrides for up to five installed accounts.
* After Ecosystem approval, you can set the app-level `defaultState` to `ON` for broader release.
Learn more about the [HubSpot Marketplace requirements for listing apps with MCP server components](/docs/apps/developer-platform/list-apps/mcp-server-listing-requirements).
## Testing agents with your MCP tools
To test your MCP tools in the agent builder:
* In the account where your app is installed, navigate to **Agent Hub** > **Agent Builder**.
* Click the **name** of an existing agent, or create a new one. It's recommended to start by testing with the Developer Tool Testing Agent.
* In the agent builder, click **Configure**.
* Click **Add tool**.
* In the left sidebar, locate the *MCP Servers* category tab, and locate your MCP server. Then, click **Connect and add** to add it to the agent.
# Set up SCIM for your app
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/scim
Learn how to set up SCIM to sync identity information between a third-party provider and HubSpot.
Provisioning users through SCIM (the System for Cross-domain Identity Management) provides a secure and automated way to create and manage HubSpot users through your Identity Provider.
By setting up SCIM in your app, you can provision and de-provision users in your HubSpot account from your Identity Provider (IdP). This helps ensure that your users stay in sync while meeting your company's IT admin and compliance requirements.
This feature requires a HubSpot *Professional* or *Enterprise* account.
**Please note:** this guide is intended for HubSpot customers who use an Identity Provider (IdP) other than Google or Okta. To set up SCIM through Google or Okta, check out the Knowledge Base articles below:
* [Provision HubSpot users with SCIM through Google](https://knowledge.hubspot.com/account-security/provision-hubspot-users-with-scim-through-google)
* [Provision HubSpot users with SCIM through Okta](https://knowledge.hubspot.com/user-management/provision-hubspot-users-with-scim-through-okta)
## Prerequisites
Keep the following requirements and caveats in mind when setting up SCIM:
* [Single sign-on (SSO)](https://knowledge.hubspot.com/account-security/set-up-single-sign-on-sso) must be enabled in your HubSpot account.
* It's recommended to set up [user permission sets](https://knowledge.hubspot.com/user-management/create-permission-sets) in HubSpot based on your team's IdP Roles before setting up SCIM. Your IdP can then assign permissions to a user if their roles in the IdP matches the name of the HubSpot permission set.
* To verify your domain, you will need to update your DNS records. Ensure you have the login details for your DNS provider and have access to the TXT records.
* Assigning seats will vary depending on whether or not you're on the seats-based pricing model:
* **If you are not on a seats-based pricing model:** to assign paid seats for users set up with SCIM, [purchase the seats in HubSpot](https://knowledge.hubspot.com/account-management/manage-seats#assign-add-or-remove-sales-hub-or-service-hub-seats) and create a permission set with a paid seat. Navigate back to your IdP and set the user's Roles to be the same as the permission set.
* **If you are using the seats-based pricing model:** you cannot assign seats such as core seats, Sales or Service Hub seats, or view-only seats, based on permission sets assigned in your IdP. Before assigning permissions through your IdP, you will need to [update a user's seat in HubSpot](https://knowledge.hubspot.com/account-management/manage-seats).
**Please note:** if you're setting up SCIM with Microsoft Entra, you should disable group sync in the [provisioning settings](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/configure-automatic-user-provisioning-portal) of your Entra account. Otherwise, the Entra ID service will enter a quarantine state after you finish configuring SCIM in your HubSpot account.
## Limitations and user syncing
For users created through SCIM in your HubSpot account:
* Only user permissions can be edited in HubSpot, and only if permission set management is not configured. All other user information, including name and email address, can only be updated through your identity provider.
* To sync identity provider roles with HubSpot permission sets, turn on permission set management in HubSpot. The permission set name in HubSpot must match the exact role name in your identity provider, including any space characters and capital letters.
* Your identity provider can't automatically assign users to teams, but after the user is added, you can [update their team manually](https://knowledge.hubspot.com/user-management/create-and-manage-teams) in HubSpot.
* Deleting a user in HubSpot will not delete the user in your identity provider. However, if you remove a user's access to HubSpot from your identity provider, or deactivate the user in your identity provider, the user will be deactivated in HubSpot as well.
* Adding a user to HubSpot will not add the user to your identity provider.
After setting up SCIM through your identity provider, any existing HubSpot users who match users in your identity provider wil automatically be converted to SCIM users. HubSpot will attempt to assign the user a permission set based on the corresponding roles in your identify provider. If the user doesn't have roles in your identity provider that match a permission set in HubSpot, the user will only have minimal permissions in HubSpot.
## Set up SCIM with a new app
To create a new app to set up SCIM in your account:
* If you haven't already authenticated your account using the HubSpot CLI, run [`hs init`](/docs/developer-tooling/local-development/hubspot-cli/reference#hs-init-command). Learn more about [installing the HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli).
* Run the following command to create a project with configured with a boilerplate SCIM configuration file and set up the required project directory structure:
```shell theme={null}
hs project create --name scim-app --dest scim-app --project-base app --distribution private --auth static --features scim
```
A new project will be created in the working directory with the required [app configuration](/docs/apps/developer-platform/build-apps/app-configuration) files and directories. The [next section](#manage-your-scim-configuration-in-your-project) provides details on how to update these project files with the required information before you upload the project to your account.
## Manage your SCIM configuration in your project
Your app's top-level configuration is specified using the `app-hsmeta.json` configuration file in the `src/app/` directory. The SCIM feature is configured via a `scim/` directory in your project's `src/app/` directory, which includes a `scim-hsmeta.json` configuration file within it.
```shell theme={null}
└── src/
└── app/
└── app-hsmeta.json
└── scim/
└── scim-hsmeta.json
```
First, open your app configuration file located at `src/app/app-hsmeta.json`, then replace the boilerplate content with the following:
```json theme={null}
{
"uid": "scim-app",
"type": "app",
"config": {
"description": "A private app for integrating an IdP with SCIM.",
"name": "scim app",
"distribution": "private",
"auth": {
"type": "static",
"requiredScopes": [
"crm.objects.users.read",
"crm.objects.users.write",
"settings.users.read",
"settings.users.write"
],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
"permittedUrls": {
"fetch": [],
"iframe": [],
"img": []
}
}
}
```
Note that you can update the `uid`, `description`, and `name` if needed, but all other fields should not be changed. The `uid` serves to uniquely identify this app from others in your account, so make sure to provide a distinct name from any existing apps you may have.
Next, within the `src/app/scim/` directory, edit the `scim-hsmeta.json` file and review the `uid`, `type`, and `config` properties. The `config` property has a single subproperty, `roleSyncEnabled`, which determines whether to sync user role data (if supported by your provider).
```json theme={null}
{
"uid": "scim",
"type": "scim",
"config": {
"roleSyncEnabled": false
}
}
```
| Field | Type | Description |
| -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid` | String | A unique identifier for your SCIM configuration. This can be set to any value, but it will appear in your project settings in your account, so it should be different from other `uid` values of other app components. |
| `type` | String | The type of component, which should be `scim` in this case. |
| `config` | Object | An object containing a single SCIM configuration field, `roleSyncEnabled`, which is used to control whether or not you want roles from your identity provider synced to permission sets in HubSpot. `roleSyncEnabled` is set to `false` by default, but you can set it to `true` at any time then upload the changes by running `hs project upload`. Note that for syncing to work properly, the role name in your identifier provider must exactly match the permission set in HubSpot, including letter-casing and spaces. |
Once you've updated the project's top-level `app-hsmeta.json` file and reviewed your `scim-hsmeta.json` configuration, run the following command to upload the project to your account:
```shell theme={null}
hs project upload
```
To open a browser window and navigate directly to your project details page in HubSpot you can run the `hs project open` command, or consult the [section below](#install-and-manage-scim-in-your-hubspot-account) to learn how to navigate to your app details page from within your HubSpot account.
**Please note:** there's a limit of one SCIM app in your HubSpot account. Attempting to upload a second project with SCIM configured will fail.
## Install and manage SCIM in your HubSpot account
Once uploaded, you can manage your SCIM configuration on the app details page:
* In your HubSpot account, navigate to **Development**.
* On the **Projects** page, click the **name** of your project.
* On the *Overview* tab of the project details page, under *Project Components*, click the top-level app name as defined in the `name` field in your app's [top-level schema file](/docs/apps/developer-platform/build-apps/app-configuration#app-schema).
* Under *App Features* click the **scim** component, as defined by the `uid` field of your `scim-hsmeta.json` file.
### Add a verified domain
To verify a domain:
* On the SCIM component details page, click **Add domain**.
* In the right panel, enter the **domain**, then click **Next**.
* In the DNS settings of your domain provider, you'll need to set up a TXT record with the provided *Host (name)* and *Value* fields with the values provided. Once configured in your domain provider, click **Next**.
* The verification process usually completes within an hour, but you may need to wait up to 48 hours for the new DNS information to propagate. Once verified, you'll see the domain appear with a success state on the SCIM component details page.
**Please note:** if you notice the error *This domain couldn't be verified*, ensure you entered your domain correctly with no spelling mistakes. You should also confirm that you copied the exact value for the TXT record from the domain setup in HubSpot into your DNS provider.
### Install the app in your account
To install the SCIM app you created in your HubSpot account:
* Click the *Distribution* tab on the SCIM component details page.
* Under the *Manage distribution* section, click **Install now** next to your account.
* You'll be prompted to confirm that you're allowing an unverified app to connect to your account. Review your app permissions then click **Connect app**.
* Back on the *Distribution* tab in your account, a new access token will appear next to your account. Click **Show** to view the token and **Copy** to copy the token to the clipboard. You can use this token, along with the *Tenant URL* of `https://api.hubspot.com/scim/v2` in your identity provider settings to connect your new SCIM app.
# Create and execute serverless functions in app cards
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/serverless-functions/create-serverless-functions
Learn how to use serverless functions in an app card on version 2026.03 of the developer platform.
If you're building UI extensions, such as app cards, an app home, or app settings page, you can leverage serverless functions to retrieve or write data when an end-user triggers an action from your UI extension. When the serverless function executes, HubSpot runs the function server-side using JavaScript, and mitigates the need to manage your own server.
This guide will walk you through how to add an app card with a serverless function that creates a contact when the user clicks a button within your app card.
## Prerequisites
Review the following prerequisites before proceeding with the steps below:
* Confirm that you're on the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli#install-the-latest-version-of-the-hubspot-cli). Version `8.4.0` or above is recommended.
* An ***Enterprise*** subscription is required to install an app with serverless functions. While developing your app, you can use a [developer test account](/docs/getting-started/account-types#developer-test-accounts) to test your app's functionality without the need for an ***Enterprise*** subscription.
## Create a new app with a serverless function
To get started, run the command below to create a new project with scaffolding for an app card and support for serverless functions.
```shell wrap theme={null}
hs project create --project-base app --features app-function card --auth static --distribution private
```
The command above configures a project with [static auth](/docs/apps/developer-platform/build-apps/authentication/overview#static-auth). 2026.03 apps with serverless functions do not currently support using [OAuth authentication](/docs/apps/developer-platform/build-apps/authentication/overview#oauth).
Follow the prompts to provide a **name** and **location** for your new project.
Your new project will have the following structure:
```shell theme={null}
my-project-folder/
└── hsproject.json
└── src
└── app/
└── app-hsmeta.json/
└── cards/
└── MyCard.jsx
└── card-hsmeta.json
└── package.json
└── functions/
└── NewFunction.js
└── private-function-hsmeta.json
└── package.json
```
## Wire up your app card and serverless function
Next, edit the following files to include the contents of the respective code blocks below:
```shell highlight={7,11,12} theme={null}
my-project-folder/
└── hsproject.json
└── src
└── app/
└── app-hsmeta.json/
└── cards/
└── MyCard.jsx
└── card-hsmeta.json
└── package.json
└── functions/
└── NewFunction.js
└── private-function-hsmeta.json
└── package.json
```
```jsx theme={null}
import React, { useState } from 'react';
import { Button, Input, hubspot } from '@hubspot/ui-extensions';
// Define the extension to be run within HubSpot
hubspot.extend(({ context, actions }) => (
));
const CreateContactForm = ({ context, addAlert }) => {
const [email, setEmail] = useState('');
const [firstname, setFirstname] = useState('');
const [lastname, setLastname] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
setLoading(true);
try {
const result = await hubspot.serverless('app_function_private', {
parameters: { email, firstname, lastname }
});
if (result.body.success) {
addAlert({
title: "Contact successfully created",
message: `New contact ID: ${result.body.contactId}`,
type: "success"
});
} else {
addAlert({
title: "Error creating contact",
message: result.body.error,
type: "danger"
});
}
} catch (error) {
addAlert({
title: "Error creating contact",
message: error.message,
type: "danger"
});
} finally {
setLoading(false);
}
};
return (
<>
{loading ? 'Creating...' : 'Create Contact'}
>
);
};
```
```js theme={null}
const axios = require('axios');
exports.main = async (context) => {
const { parameters, crm } = context;
const { email, firstname, lastname } = parameters;
// Validate input
if (!email || !firstname || !lastname) {
return {
statusCode: 400,
body: {
success: false,
error: 'Missing required parameters: email, firstname, lastname'
}
};
}
try {
// Get token to make API requests on behalf of your app
const accessToken = process.env.PRIVATE_APP_ACCESS_TOKEN;
// Create contact in HubSpot
const response = await axios.post(
'https://api.hubapi.com/crm/v3/objects/contacts',
{
properties: {
email,
firstname,
lastname
}
},
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
console.log('Contact created:', response.data.id);
return {
statusCode: 200,
body: {
success: true,
contactId: response.data.id,
message: 'Contact created successfully'
}
};
} catch (error) {
console.error('Error creating contact.');
const { response } = error;
return {
statusCode: (response && response.status) ? response.status : 500,
body: {
success: false,
error: (response && response.data) ? response.data.message : 'Unknown error occurred'
}
};
}
};
```
```json theme={null}
{
"uid": "app_function_private",
"type": "app-function",
"config": {
"entrypoint": "/app/functions/NewFunction.js",
"secretKeys": []
}
}
```
Note that a reserved secret named `PRIVATE_APP_ACCESS_TOKEN` is accessible by default in every private serverless function. If needed, you can manage additional secrets by following the instructions [here](/docs/apps/developer-platform/add-features/serverless-functions/reference#managing-and-referencing-secrets).
After updating the files above, save your changes. Since the example code in `NewFunction.js` uses the third-party `axios` dependency, you'll also need to install the package locally by navigating to the `functions/` directory, then running `npm install axios`. For example, in the terminal, if you're already in the root directory of your project, you'd run the following two commands:
```shell theme={null}
cd src/app/functions
npm install axios
```
After `axios` is installed, run the following command to upload your project to your HubSpot account:
```shell theme={null}
hs project upload
```
## Install your app and locate your access token
With your project uploaded, you can install your app in either your standard HubSpot account, or a developer test account:
If you have the [required user permissions](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#permission-requirements), you can install your app directly in your [standard account](/docs/getting-started/account-types#standard-hubspot-accounts):
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Projects**, click the **name** of your new project, then click the **UID** of your app in the component list.
* On the *Distribution* tab, under *Standard install*, click **Install now**.
After initiating the install, you'll be prompted to review the app permissions.
* Select the **checkbox** to authorize installing an unverified app, then click **Connect app**.
* Once successful, click **View installed app details** to navigate to the *Connected Apps* page of the account where you installed your app.
* Navigate back to **Development**.
* In the left sidebar menu, navigate to **Projects**, click the **name** of your new project, then click the **UID** of your app in the component list.
* If you need to use your app's access token, you can click the *Distribution* tab, then click **Show** under *Standard install* to reveal your static auth access token. Note that your serverless function will already be able to access a built-in secret called `PRIVATE_APP_ACCESS_TOKEN` by default, and you don't need to add it manually as a secret.
* Navigate to **Test accounts** in the *Development* navigation menu, then click **Create developer test account**. Follow the prompts to create your new test account.
* In the left sidebar menu, navigate to **Projects**, click the **name** of your new project, then click the **UID** of your app in the component list.
* Click the *Distribution* tab, under *Test installs*, click **Add test installs**.
* In the right panel, click **Install** next to the test account you created.
* Review the app permissions, select the **checkbox** to authorize installing an unverified app, then click **Connect app**.
* Navigate back to your standard HubSpot account where you originally uploaded your project, then navigate to **Development**.
* In the left sidebar menu, navigate to **Projects**, click the **name** of your new project, then click the **UID** of your app in the component list.
* If you need to use your app's access token, you can click the *Distribution* tab, under *Test installs*, click **Show** next to the test account you installed your app in to reveal your static auth access token. Note that your serverless function will already be able to access a built-in secret called `PRIVATE_APP_ACCESS_TOKEN` by default, and you don't need to add it manually as a secret.
## Test out your app card
You can now test out the serverless function on a contact record.
First, you'll need to add the app card to the default contact record view:
* In your HubSpot account, navigate to **CRM** > **Contacts**.
* Click the **name** of an existing contact.
* In the middle column of the record, to the right of the existing tabs, click **Customize**.
* On the *Record Customization* tab, in the view table, click **Default view**.
* In the middle column, under the default tabs, hover over where you want your app card to appear, then click **Add card**.
* In the right panel, click the **Card library** tab.
* Search for the name of your app card by the `uid` specified in your project's `/src/app/cards/card-hsmeta.json` file. You can also click the **All card types** dropdown menu and select **Apps** to filter by app cards.
* Locate your app card then click **Add card**.
* Click the **X** in the top right to close the right panel.
* In the top right, click **Save and exit**.
You'll be taken back to the contact record, where your app card will now appear in the location you chose.
To test out your card, enter a test **email address**, **first name**, and **last name**, then click **Create Contact**. After a short delay, you should see a success notification appear at the top of the page.
## Local development
As you continue to develop and test your serverless function and app card, you can run `hs project dev` to help you preview changes and debug errors directly in your terminal. You'll be prompted to select the account to test on, which you should choose based on where you installed your app in the [step above](#install-your-app-and-locate-your-access-token)
Once your deployed build and dependencies are validated, a new browser tab or window will open to a *Local dev panel* where you can review the status of each of your app's components.
* In the *Actions* column, you can click **Preview** next to your app card component to navigate to the contacts index page.
* You can then navigate to a specific contact and test out your app card, and any `console` logging statements (e.g., `console.log()` or `console.error()`) will be logged to your terminal to help you debug.
If you edit and save any changes to your frontend app card code (e.g., a change to `/src/app/cards/MyCard.tsx`) while running `hs project dev`, the changes should automatically be detected and reflected when previewing the changes.
Note that changes to your serverless functions in `/src/app/functions` will not automatically update your app and you'll need to run `hs project upload` to ensure your changes are deployed to the account where you installed your app.
## Next steps
Check out the following resources as you develop your app:
* [Serverless function reference](/docs/apps/developer-platform/add-features/serverless-functions/reference)
* [App card reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference)
* [UI components overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview)
* [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk)
# Serverless functions overview
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/serverless-functions/overview
Learn how more about serverless function support on version 2026.03 of the developer platform.
Starting with version `2026.03`, apps built on the developer platform fully support and deploy serverless functions. These server-side JavaScript functions execute within HubSpot's infrastructure, which eliminates the need to manage your own external server while still providing enhancements over older versions of the platform (e.g., `2025.1`).
An ***Enterprise*** subscription is required to install an app with serverless functions. While developing your app, you can use a [developer test account](/docs/getting-started/account-types#developer-test-accounts) to test your app's functionality without the need for an ***Enterprise*** subscription.
## Comparison of serverless function options
2026.03 serverless functions are best suited if you want to work with UI extensions and leverage HubSpot's REST APIs, while CMS serverless functions are better used in tandem with your CMS website.
The table below provides more details on the advantages offered both serverless function options:
| Feature | 2026.03 serverless functions | CMS serverless functions |
| -------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------ |
| Private functions | ✅ Yes | ❌ No |
| Public endpoints | ✅ Yes (***Content Hub** Enterprise* only) | ✅ Yes |
| [UI extension](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) support | ✅ Yes | ❌ No |
| [Test account](developer-tooling/local-development/configurable-test-accounts) support | ✅ Yes | ❌ No |
| NPM packages | ✅ Yes | ✅ Yes |
## Get started
To get started with serverless functions for 2026.03 apps, check out the articles below:
* Check out the [serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/create-serverless-functions) guide for a walkthrough of how to create a new app that includes an app card wired up to an example serverless function.
* Consult the [serverless function reference article](/docs/apps/developer-platform/add-features/serverless-functions/reference) for information on project structure, schema details, limits, and more.
# Serverless functions reference
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/serverless-functions/reference
Reference information for serverless functions on version 2026.03 of the developer platform.
This article provides reference information for configuring serverless functions on version 2026.03 of the developer platform.
Using serverless functions with 2026.03 apps requires that you're on the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli#install-the-latest-version-of-the-hubspot-cli). Version `8.4.0` or above is recommended.
You can check the version you're using by running `hs --version`.
## Project structure
To add serverless function support to an existing project, run the following command:
```shell theme={null}
hs project add --features app-function
```
This command will create the following directory and files in your project:
```shell highlight={6-9} theme={null}
project-folder/
└── src/
└── app/
├── app-hsmeta.json
...
└── functions/
└── private-function-hsmeta.json
└── NewFunction.js
└── package.json
```
## Function configuration
The `src/app/functions/private-function-hsmeta.json` file provides the main configuration for your serverless functions, while the code for your serverless function is defined in the `NewFunction.js` file. These boilerplate files are meant as a starting point for your app, which can be adapted to fit your app's needs.
```json theme={null}
{
"uid": "app_function_private",
"type": "app-function",
"config": {
"entrypoint": "/app/functions/NewFunction.js",
"secretKeys": []
}
}
```
```js theme={null}
exports.main = async () => {
return "New Private Function!";
};
```
The table below provides details on each of the available properties you can configure in your `function-hsmeta.json` file:
| Field | Type | Description |
| ---------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid` | String | A unique identifier for the app function component. This can be any string, but should meaningfully identify the app function. HubSpot will identify the function by this ID. |
| `type` | String | The type of component, which should be `app-function` in this case. |
| `config` | Object | An object containing configuration details, which includes the `entrypoint`, `endpoint`, and `secretKeys` properties, detailed below. |
| `entrypoint` | String | A sub-property of the `config` object, which is the path to your serverless function JavaScript file. |
| `endpoint` | Object | An object that defines a publicly accessible endpoint (without authentication), which requires ***Content Hub** Enterprise*. This object includes the following properties:
`path`: The URL path for the public endpoint.
`method`: the HTTP method for the endpoint (e.g., `"POST"`, `"GET"`).
|
| `secretKeys` | Array | Array of secret names to inject as environment variables. |
## Manage multiple serverless functions
2026.03 apps support multiple serverless functions, added via sets of `.js` and `*-hsmeta.json` files.
For example, to define another serverless function, you could create two new files, 'SecondFunction.js' and `second-function-hsmeta.json` in the `src/app/functions/` directory:
```shell highlight={8,10} theme={null}
project-folder/
└── src/
└── app/
├── app-hsmeta.json
...
└── functions/
└── private-function-hsmeta.json
└── NewFunction.js
└── second-private-function-hsmeta.json
└── SecondFunction.js
└── package.json
```
Then, you'd edit the newly added files to ensure the `second-private-function-hsmeta.json` file references the path to `SecondFunction.js`, and it has a `uid` that's distinct from the `uid` of any other function.
```json highlight={2,5} theme={null}
{
"uid": "second_app_function_private",
"type": "app-function",
"config": {
"entrypoint": "/app/functions/SecondFunction.js",
"secretKeys": []
}
}
```
## Managing and referencing secrets
Secrets provide secure storage for any API keys, tokens, or other sensitive data your serverless function might need to use when making an external request.
By default, a reserved secret named `PRIVATE_APP_ACCESS_TOKEN` is accessible by default in every private serverless function to make HubSpot API requests on behalf of your app, but you can add new secrets if your serverless function needs to make other external requests.
To add a secret, use the `hs secret add` command. The example below would add a secret with a name of `THIRD_PARTY_ACCESS_TOKEN`:
```shell theme={null}
hs secret add THIRD_PARTY_ACCESS_TOKEN
```
You'd then be prompted to enter or paste in the value of the secret (which will not appear in the terminal).
You should then add the secret name to the `secretKeys` array in the corresponding `private-function-hsmeta.json` file:
```json highlight={6} theme={null}
{
"uid": "app_function_private",
"type": "app-function",
"config": {
"entrypoint": "/app/functions/NewFunction.js",
"secretKeys": ["THIRD_PARTY_ACCESS_TOKEN"]
}
}
```
The secret is then injected as an environment variable in your serverless function:
```js highlight={5} theme={null}
exports.main = async (context) => {
// ...
// Get third-party access token
const accessToken = process.env.THIRD_PARTY_ACCESS_TOKEN;
// ...
};
```
After adding a secret, you may need to run `hs project upload` to ensure your secret can be correctly referenced in a deployed UI extension.
Learn more about [managing secrets](/docs/developer-tooling/local-development/hubspot-cli/commands/account-commands#managing-secrets) using the HubSpot CLI.
## Calling serverless functions
The way you invoke your serverless function depends on whether you configured a private function to run in a UI extension or whether you configured a publicly accessible endpoint.
### Private functions in UI extensions
To execute your serverless function from one of your UI extensions (e.g., an [app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview), [app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages), or [app settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page)), use the `hubspot.serverless()` API, as demonstrated in the code block below:
```jsx theme={null}
import { hubspot } from '@hubspot/ui-extensions';
// In your React component (App Card, App Home, App Settings, etc.)
const handleSubmit = async () => {
try {
const result = await hubspot.serverless('app_function_private', {
parameters: {
customParam: 'value'
},
propertiesToSend: ['firstname', 'lastname', 'email']
});
console.log('Function result:', result);
} catch (error) {
console.error('Function error:', error);
}
};
```
Learn more about how to [use serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/create-serverless-functions) in an app card.
### Endpoint functions via public HTTP request
**Please note:** public endpoints require ***Content Hub** Enterprise*, and are accessible without authentication. Only add endpoint configuration if you intentionally need to create a public API. Consider implementing your own authentication logic (API keys, tokens, etc.) within the function if you need to restrict access.
If you configured a public endpoint, you can make HTTP requests to the endpoint that you specified by the `config.endpoint.path` property in your `function-hsmeta.json` file.
The `cURL` example below demonstrates how to make a request to a function with a `path` of `https://your-domain.com/hs/serverless/api/app_function_endpoint`:
```shell theme={null}
curl -X POST https://your-domain.com/hs/serverless/api/app_function_endpoint \
-H "Content-Type: application/json" \
-d '{"key": "value"}'
```
## Function context
Serverless functions are passed a `context` object, which contains metadata based on whether you configure your function to be private or public.
### Private function
When your function is called from a UI extension (e.g., an app card, app home, or app settings page), the `context` object can be deconstructed to reference the properties shown below:
```js highlight={5} theme={null}
exports.main = async (context) => {
const {
accountId, userId, userEmail, // User and account information
parameters, // Parameters passed from your frontend code
propertiesToSend // CRM properties
} = context;
// ...
};
```
The `propertiesToSend` field is only available when your function is invoked from a CRM record context (like an App Card on a contact record). It will not be present when called from App Homes or App Settings.
Learn more about using `context` in the UI extensions SDK [reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk).
### Public function
If your serverless function is a publicly accessible endpoint, the `context` object contains the following fields:
| Field | Type | Description |
| ----------- | ------ | ----------------------------------------- |
| `accountId` | Number | The HubSpot account ID (if authenticated) |
| `method` | String | The HTTP request method. |
| `body` | Object | Request body, if provided. |
| `query` | Object | Request query parameters, if provided. |
| `headers` | Object | Request headers. |
## Add NPM packages
Serverless functions support custom NPM dependencies. You can add them by running `npm install ` in the `src/app/functions/` directory.
For example, if you wanted to add axios as a dependency, you'd run `npm install axios` in the `src/app/functions/` directory, which would install `axios` and update the `package.json` file in the `functions/` directory automatically:
```json highlight={5} theme={null}
{
"name": "example-function",
"version": "0.1.0",
"dependencies": {
"axios": "^1.13.6"
}
}
```
## Limitations
Keep the following limits in mind as you develop and test your function:
* Functions have a 15 second execution timeout
* Functions may experience "cold starts" after periods of inactivity.
* Only privately distributed apps with static auth are currently supported. Apps using [OAuth](/docs/apps/developer-platform/build-apps/authentication/overview#oauth) for authentication cannot use 2026.03 serverless functions.
To help mitigate both limitations above, keep your functions lightweight, minimize the number of external API calls, and assign variables within functions instead of at the module level.
## Troubleshooting
The table below outlines common errors you might encounter while you develop and test your serverless function:
| Error | Details |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Function "[name]" not found` | Occurs when the `uid` in your `functions/*-hsmeta.json` file doesn't match the function name. Double-check that the `uid` matches, then check for any errors when running `hs project upload`. |
| `Function execution timed out` | The function didn't complete within 15 seconds. Try reducing any unnecessary external API calls, breaking larger functions into separate functions, or cache results from your requests. |
| `Build failed: invalid configuration` | Results from project configuration issues. Run the `hs project validate` command to identify any schema errors, then address any missing fields in your project's `*-hsmeta.json` files. You should also confirm that all `uid` values are unique across all functions. |
## Related resources
Check out the following resources as you develop your app:
* [Create an app card with a serverless function](/docs/apps/developer-platform/add-features/serverless-functions/create-serverless-functions)
* [App card reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference)
* [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk)
# PageBreadcrumbs
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-breadcrumbs
Learn about the PageBreadcrumbs component for breadcrumb navigation in app pages.
The `PageBreadcrumbs` component is used to display breadcrumb navigation at the top of your page, helping users understand their current location in your app's hierarchy and navigate back to parent pages.
```tsx theme={null}
import { PageBreadcrumbs } from "@hubspot/ui-extensions/pages";
HomeSupportContact Us
```
To use this component, you'll need to be on version `0.13.0` or later of the `ui-extensions` NPM package. You can check your current version by running `npm list` or `npm list -g`, and install the latest version by running `npm i @hubspot/ui-extensions`.
## Basic usage
The `PageBreadcrumbs` component should be placed at the top of your page component, before the `PageTitle`. It typically contains a combination of `PageBreadcrumbs.PageLink` components (for navigable breadcrumbs) and a `PageBreadcrumbs.Current` component (for the current page).
```tsx theme={null}
import { Text } from "@hubspot/ui-extensions";
import { PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
const ContactDetailsPage = () => {
return (
<>
HomeContactsContact DetailsContact DetailsContact information goes here...
>
);
};
```
## Props
ReactNode>} description={<>The links and text to render as breadcrumbs. Use PageBreadcrumbs.PageLink for navigable crumbs and PageBreadcrumbs.Current for the current page.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Examples
### Simple breadcrumb trail
```tsx theme={null}
import { PageBreadcrumbs } from "@hubspot/ui-extensions/pages";
HomeDocumentation
```
### Multi-level breadcrumb trail
```tsx theme={null}
import { PageBreadcrumbs } from "@hubspot/ui-extensions/pages";
HomeContactsCompaniesACME Corporation
```
### Breadcrumbs with dynamic content
```tsx theme={null}
import { PageBreadcrumbs, usePageRoute } from "@hubspot/ui-extensions/pages";
const ContactPage = () => {
const { params: { contactId } } = usePageRoute();
return (
<>
HomeContactsContact {contactId}Contact Details
{/* Rest of page content */}
>
);
};
```
### Complete page example
Here's a full example showing breadcrumbs, title, and page content together:
```tsx theme={null}
import React from "react";
import { Text, Heading, Flex } from "@hubspot/ui-extensions";
import { PageBreadcrumbs, PageTitle, usePageRoute } from "@hubspot/ui-extensions/pages";
const DealNotePage = () => {
const { params: { dealId, noteId } } = usePageRoute();
return (
<>
HomeDeals
Deal {dealId}
Note {noteId}Deal NoteNote content goes here...
>
);
};
```
### Breadcrumbs in a layout component using route IDs
When using a layout component, you can use `routeId` from `usePageRoute()` to render different breadcrumb trails based on the active route:
```tsx theme={null}
import type { ReactNode } from "react";
import { Flex } from "@hubspot/ui-extensions";
import { createPageRouter, PageRoutes, PageBreadcrumbs, PageTitle, usePageRoute } from "@hubspot/ui-extensions/pages";
const BREADCRUMBS: Record = {
home: Home,
docs: (
<>
HomeDocumentation
>
),
support: (
<>
HomeSupport
>
),
};
function AppLayout({ children }: { children: ReactNode }) {
const { routeId } = usePageRoute();
return (
{BREADCRUMBS[routeId]}
{children}
);
}
const PageRouter = createPageRouter(
);
```
## Breadcrumb patterns
### Pattern: All links except current page
The most common pattern is to make all breadcrumb items clickable except the current page, which is displayed as plain text:
```tsx theme={null}
Home {/* Clickable */}
Section {/* Clickable */}
Current Page {/* Not clickable */}
```
### Pattern: Minimal breadcrumbs
For shallow hierarchies, you might only show the home link and current page:
```tsx theme={null}
HomeSettings
```
### Pattern: Resource hierarchy
Show the hierarchy when viewing a specific resource:
```tsx theme={null}
HomeAll ContactsJohn Doe
```
## Guidelines
* **DO:** place `PageBreadcrumbs` at the top of your page component, before `PageTitle`.
* **DO:** use `PageBreadcrumbs.PageLink` for all breadcrumb items except the current page.
* **DO:** use `PageBreadcrumbs.Current` for the current page (the last breadcrumb item).
* **DO:** keep breadcrumb labels concise and descriptive.
* **DO:** include breadcrumbs on all pages except the home page.
* **DON'T:** make the current page clickable in the breadcrumbs.
* **DON'T:** create excessively deep breadcrumb trails (more than 4-5 levels).
* **DON'T:** use breadcrumbs for linear processes - use step indicators instead.
## Accessibility
The `PageBreadcrumbs` component automatically provides proper accessibility features:
* Uses semantic HTML breadcrumb navigation
* Includes proper ARIA labels for screen readers
* Provides clear visual hierarchy
* Supports keyboard navigation through the links
## Related components
* [PageTitle](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-title)
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
* [PageHeader](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header)
* [Page linking guide](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking)
# PageHeader
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header
Learn about the PageHeader component and action buttons for app pages.
The `PageHeader` component and its sub-components are used to add action links to the header area of app pages. These components allow you to create primary and secondary actions that users can take within your app.
```tsx theme={null}
import { PageHeader } from "@hubspot/ui-extensions/pages";
HomeDocumentationSupportExternal Link
```
To use these components, you'll need to be on version `0.13.0` or later of the `ui-extensions` NPM package. You can check your current version by running `npm list` or `npm list -g`, and install the latest version by running `npm i @hubspot/ui-extensions`.
## PageHeader
The `PageHeader` component is the main wrapper component for primary and secondary header actions. It's recommended to only include one instance of `PageHeader` in your app, and to avoid adding/removing it dynamically which can result in unexpected behavior.
Only `PageHeader.PrimaryAction` and `PageHeader.SecondaryActions` are supported as children.
```tsx theme={null}
import { PageHeader } from "@hubspot/ui-extensions/pages";
Home
```
### Props
ReactNode>} description={<>>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## PageHeader.PrimaryAction
The `PageHeader.PrimaryAction` component is a wrapper for the primary action in the page header. It can contain a `PageHeader.PageLink` component (for navigating to another page in your app) or a `PageHeader.Link` component (for URLs outside your app pages). The primary action is displayed as a button.
`PageHeader` can contain only one instance of `PageHeader.PrimaryAction`.
```tsx theme={null}
import { PageHeader } from '@hubspot/ui-extensions/pages';
// Primary action linking to another app page
Documentation
// Primary action as external URL
View Details
```
`PageHeader.PrimaryAction` can only contain `PageHeader.PageLink` or `PageHeader.Link` components. No other component types are supported.
### Props
ReactNode>} description={<>The content to render inside the primary action container.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## PageHeader.SecondaryActions
The `PageHeader.SecondaryActions` component is a wrapper for secondary actions that appear in an *Actions* dropdown menu next to the primary action. It can contain multiple `PageHeader.PageLink` components (for navigating to other pages in your app) or `PageHeader.Link` components (for URLs outside your app pages).
```tsx theme={null}
import { PageHeader } from "@hubspot/ui-extensions/pages";
import { hubspot } from "@hubspot/ui-extensions";
hubspot.extend<"pages">(() => {
return ;
});
function AdvancedHeaderActions() {
return (
HomeDocumentationSupportExternal Resource
);
}
```
`PageHeader.SecondaryActions` can only contain `PageLink` or `Link` components. No other component types are supported.
### Props
ReactNode>} description={<>The content to render inside the secondary actions container.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## PageHeader.PageLink
The `PageHeader.PageLink` component renders a button that navigates to another page within your app when clicked. Use this component inside `PageHeader.PrimaryAction` or `PageHeader.SecondaryActions` for in-app navigation.
```tsx theme={null}
import { PageHeader } from "@hubspot/ui-extensions/pages";
// Navigate to a static path
Documentation
// Navigate with path parameters
View Contact
// Navigate with query string parameters
Search
```
### Props
ReactNode>} description={<>Sets the content that will render inside the PageHeader action. This prop is passed implicitly by providing sub-components.>} />
string>} description={<>The path to navigate to when the link is clicked. Supports path parameters (e.g. /view-contact/:contactId).>} />
Record<string, string>>} description={<>Values for path parameters and query string entries. Parameters matching :paramName tokens in to are substituted into the path. Any remaining parameters are appended as query string entries.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## PageHeader.Link
The `PageHeader.Link` component renders a button that opens a URL outside your app when clicked. It behaves like the standard [`Link`](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link) component, but is rendered as a button when used inside `PageHeader.PrimaryAction` or `PageHeader.SecondaryActions`.
```tsx theme={null}
import { PageHeader } from "@hubspot/ui-extensions/pages";
Visit HubSpot
```
### Props
ReactNode>} description={<>Sets the content that will render inside the PageHeader action. This prop is passed implicitly by providing sub-components.>} />
boolean>} description={<>Determines whether or not the button should be disabled.>} />
string>} description={<>A URL that will be opened when the button is clicked. If the value is a URL external to hubspot.com it will be opened in a new tab.>} />
(event: { type: string, bubbles: boolean, timeStamp: number, id: string }, reactions: { openPanel: (panelId: string) => void, closePanel: (panelId: string) => void, openModal: (modalId: string) => void, closeModal: (modalId: string) => void }) => Promise<void> | void>} description={<>A function that will be invoked when the button is clicked. Do not use this function for submitting a form; use Form's onSubmit function instead.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Guidelines
* **DO:** use `PageHeader.PrimaryAction` for the most important action on the page.
* **DO:** use `PageHeader.PageLink` for navigating to pages within your app and `PageHeader.Link` for any URLs outside your app pages.
* **DO:** keep link labels short and action-oriented (e.g., "Documentation", "Support").
* **DO:** use `PageHeader.SecondaryActions` for additional or less common actions.
* **DON'T:** include more than one `PageHeader.PrimaryAction` in a PageHeader.
* **DON'T:** include components other than `PageHeader.PageLink` or `PageHeader.Link` in `PageHeader.PrimaryAction` or `PageHeader.SecondaryActions`.
## Related components
* [PageLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link)
* [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link)
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
* [PageRoutes](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes)
# PageLink
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link
Learn about the PageLink component for navigating between pages in your app.
The `PageLink` component is used to create clickable links that navigate to other pages within your app. Use `PageLink` for internal page navigation and the standard `Link` component for external URLs.
```tsx theme={null}
import { PageLink } from "@hubspot/ui-extensions/pages";
// Basic link to another page
Documentation
// Link with parameters
Active Contacts
```
To use this component, you'll need to be on version `0.13.0` or later of the `ui-extensions` NPM package. You can check your current version by running `npm list` or `npm list -g`, and install the latest version by running `npm i @hubspot/ui-extensions`.
## Basic usage
Here's a complete example showing how to use `PageLink` within a page component:
```tsx theme={null}
import React from "react";
import { Text, Heading } from "@hubspot/ui-extensions";
import { PageLink, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
const DocsPage = () => {
return (
<>
HomeDocumentationDocumentationGetting Started
Check out our support page for help.
>
);
};
```
## Props
ReactNode>} description={<>The content to render inside the link.>} />
string>} description={<>The path to navigate to when the link is clicked. Supports path parameters (e.g. /view-contact/:contactId).>} />
Record<string, string>>} description={<>Values for path parameters and query string entries. Parameters matching :paramName tokens in to are substituted into the path. Any remaining parameters are appended as query string entries.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Examples
### Simple page navigation
```tsx theme={null}
DocumentationSupportAnalytics
```
### Navigation with query parameters
```tsx theme={null}
// Navigate to /docs?section=getting-started
Getting Started Guide
// Navigate to /analytics?view=dashboard&period=30d
Dashboard (Last 30 Days)
```
### Navigation with path parameters
```tsx theme={null}
// Navigate to /view-contact/12345
View Contact
// Navigate to /deals/456/notes/789?highlight=comments
View Note
```
## PageLink vs Link
Use the right component for your navigation needs:
| Component | Use for | Import from |
| ---------- | -------------------------------------------- | ------------------------------ |
| `PageLink` | Internal navigation to pages within your app | `@hubspot/ui-extensions/pages` |
| `Link` | External URLs outside your app | `@hubspot/ui-extensions` |
```tsx theme={null}
import { Link } from "@hubspot/ui-extensions";
import { PageLink } from "@hubspot/ui-extensions/pages";
// Internal navigation - use PageLink
Documentation
// External URL - use Link
External Site
```
## Guidelines
* **DO:** use `PageLink` for all navigation between pages within your app.
* **DO:** use descriptive link text that clearly indicates where the link leads.
* **DO:** use the `params` prop for passing data through URLs.
* **DON'T:** use `PageLink` for external URLs - use the standard `Link` component instead.
* **DON'T:** use `PageLink` for actions that aren't navigation (use `Button` with `onClick` instead).
## Related components
* [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link)
* [PageHeader](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header)
* [PageBreadcrumbs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-breadcrumbs)
* [PageRoutes](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes)
## Related resources
* [Page linking guide](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking)
* [Page routing guide](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing)
# PageRoutes
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes
Learn about the PageRoutes component for defining routes in your app.
The `PageRoutes` component and its sub-components are used to define routing for app pages. Use `createPageRouter` to wrap your route definitions, which returns a React component that handles routing.
```tsx theme={null}
import { createPageRouter, PageRoutes } from "@hubspot/ui-extensions/pages";
const PageRouter = createPageRouter(
);
```
## Basic example
Here's a complete example showing how to create multiple pages in your app with routing:
```tsx theme={null}
import React from "react";
import { Text, Heading, hubspot } from "@hubspot/ui-extensions";
import { createPageRouter, PageHeader, PageRoutes, PageLink, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
const AppHomePage = () => {
return (
<>
HomeHomeThis is the home page of your app.View Documentation
>
);
};
const AppDocsPage = () => {
return (
<>
HomeDocumentationDocumentationWelcome to the documentation page!Back to Home
>
);
};
const AppNotFoundPage = () => {
return (
<>
HomeNot FoundPage Not FoundThe page you're looking for doesn't exist.Go to Home
>
);
};
const PageRouter = createPageRouter(
);
const AppPages = () => {
return (
<>
HomeDocumentation
>
);
};
hubspot.extend<"pages">(() => );
```
## createPageRouter
The `createPageRouter` function accepts JSX route definitions and returns a React component that handles routing. Call it at the module level with your `` tree:
```jsx theme={null}
import { createPageRouter, PageRoutes } from "@hubspot/ui-extensions/pages";
const PageRouter = createPageRouter(
);
function App() {
return ;
}
```
### Parameters
| Parameter | Type | Required | Description |
| --------- | ----------- | -------- | ----------------------------------------------------------- |
| `routes` | `ReactNode` | Yes | A `` JSX tree defining all routes for your app. |
### Returns
A React component (`PageRouter`) that you render in your app to activate routing.
## PageRoutes
The `PageRoutes` component is the container for route definitions. When used as the root element passed to `createPageRouter`, it defines the top-level routes. When nested inside another ``, it defines a group of sub-routes under a shared path prefix.
```jsx theme={null}
import { createPageRouter, PageRoutes } from "@hubspot/ui-extensions/pages";
const PageRouter = createPageRouter(
);
```
### Props
ReactNode>} description={<>Sets the content that will render inside the component. Should contain PageRoutes.IndexRoute, PageRoutes.Route, PageRoutes.AnyRoute, and/or nested PageRoutes components.>} />
string>} description={<>The path prefix for nested routes. Required when nesting PageRoutes inside another PageRoutes. Child routes will be scoped under this path.>} />
ComponentType<{ children: ReactNode }>>} description={<>A React component that wraps all child routes. Receives the matched page as children. Useful for shared navigation, sidebars, or other persistent UI.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## PageRoutes.IndexRoute
The `PageRoutes.IndexRoute` component defines the home page (index route) of your app or a nested section. This is the page that will be rendered when users navigate to the base URL of the enclosing `PageRoutes`.
```jsx theme={null}
```
### Props
string>} description={<>A stable identifier for this route. When the route matches, the id value is available as routeId from the usePageRoute hook. Useful for determining which route is active in layout components.>} />
ComponentType>} description={<>The React component to render when the index route matches. Pass the component reference, not a JSX element.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## PageRoutes.Route
The `PageRoutes.Route` component defines a named page route in your app. Users can navigate to these routes using the path specified in the `path` prop.
```jsx theme={null}
```
### Props
string>} description={<>A stable identifier for this route. When the route matches, the id value is available as routeId from the usePageRoute hook. Useful for determining which route is active in layout components.>} />
string>} description={<>The path for this route. Paths are automatically normalized, so both "docs" and "/docs" work equivalently.>} />
ComponentType>} description={<>The React component to render when this route matches. Pass the component reference, not a JSX element.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## PageRoutes.AnyRoute
The `PageRoutes.AnyRoute` component defines a catch-all route that will be rendered when no other routes match. This is useful for displaying a custom 404 or "page not found" message.
```jsx theme={null}
```
### Props
string>} description={<>A stable identifier for this route. When the route matches, the id value is available as routeId from the usePageRoute hook. Useful for determining which route is active in layout components.>} />
ComponentType>} description={<>The React component to render when no other routes match. Pass the component reference, not a JSX element.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Guidelines
* **DO:** use `createPageRouter` to define your routes and render the returned component.
* **DO:** use `PageRoutes.IndexRoute` to define your app's home page.
* **DO:** use meaningful path names that describe the page content (e.g., "/docs", "/settings", "/support").
* **DO:** include a `PageRoutes.AnyRoute` to handle unmatched routes gracefully.
* **DO:** use `layoutComponent` to share common UI across groups of routes.
* **DO:** assign an `id` to each route when you need to identify the active route in layout components.
## Related components
* [PageHeader](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header)
* [PageLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link)
* [PageBreadcrumbs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-breadcrumbs)
* [PageTitle](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-title)
## Related guides
* [Create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages)
* [Page routing guide](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing)
* [Page linking guide](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking)
* [App pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference)
# PageTitle
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-title
Learn about the PageTitle component for displaying page titles in app pages.
The `PageTitle` component is used to display the main title heading on your page and automatically updates the browser tab's `document.title` to match. This provides both visual page identification and improved user experience when users have multiple browser tabs open.
```tsx theme={null}
import { PageTitle } from "@hubspot/ui-extensions/pages";
Contact Us
```
To use this component, you'll need to be on version `0.10.0` or later of the `ui-extensions` NPM package. You can check your current version by running `npm list` or `npm list -g`, and install the latest version by running `npm i @hubspot/ui-extensions`.
## What PageTitle does
The `PageTitle` component serves two key functions:
1. **Visual page heading**: Renders the main title heading at the top of your page
2. **Browser document title**: Automatically updates the browser tab's `document.title` to match the page title
This dual functionality helps users understand which page they're viewing, both within your app and when switching between browser tabs.
## Basic usage
The `PageTitle` component should be placed near the top of your page component, immediately after `PageBreadcrumbs` (if present). It works alongside `PageHeader` - use `PageTitle` for the page title and `PageHeader` for action buttons.
```tsx theme={null}
import React from "react";
import { Text, Heading } from "@hubspot/ui-extensions";
import { PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
const SupportPage = () => {
return (
<>
HomeSupportSupportHow can we help?Browse our support resources...
>
);
};
```
## Props
string>} description={<>The text to display as the page heading. Also sets the browser tab title.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Examples
### Simple page title
```tsx theme={null}
import { PageTitle } from "@hubspot/ui-extensions/pages";
Documentation
```
### Title with breadcrumbs
```tsx theme={null}
import { PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages";
<>
HomeDocumentationDocumentation
>
```
### Dynamic page title
```tsx theme={null}
import { PageBreadcrumbs, PageTitle, usePageRoute } from "@hubspot/ui-extensions/pages";
const ContactPage = () => {
const { params: { contactId } } = usePageRoute();
return (
<>
HomeContactsContact {contactId}Contact {contactId}
{/* Rest of page content */}
>
);
};
```
### Title in a layout component using route IDs
When using a layout component, you can use `routeId` from `usePageRoute()` to set the page title based on the active route:
```tsx theme={null}
import { ReactNode } from "react";
import { Flex } from "@hubspot/ui-extensions";
import { createPageRouter, PageRoutes, PageTitle, usePageRoute } from "@hubspot/ui-extensions/pages";
const TITLES: Record = {
home: "Home",
docs: "Documentation",
support: "Support",
"not-found": "Page Not Found",
};
function AppLayout({ children }: { children: ReactNode }) {
const { routeId } = usePageRoute();
return (
{TITLES[routeId]}
{children}
);
}
const PageRouter = createPageRouter(
);
```
### Complete page structure
Here's a complete example showing how `PageTitle` works with `PageBreadcrumbs` and `PageHeader`:
```tsx theme={null}
import React from "react";
import { Text, Heading, Flex, Link } from "@hubspot/ui-extensions";
import { PageHeader, PageBreadcrumbs, PageTitle, hubspot } from "@hubspot/ui-extensions/pages";
hubspot.extend<"pages">(() => );
const AnalyticsPage = () => {
return (
<>
HomeDocumentationExport DataHomeAnalyticsAnalytics DashboardPerformance MetricsYour analytics data appears here...
>
);
};
```
## How document.title works
When you use `PageTitle`, the browser tab title automatically updates to match:
```tsx theme={null}
Contact Details
// Browser tab shows: "Contact Details"
```
This is particularly helpful when users have multiple tabs open, as they can easily identify which tab contains which page of your app.
## PageTitle vs PageHeader
`PageTitle` and `PageHeader` serve different purposes and should be used together:
| Component | Purpose | Location |
| ------------ | -------------------------------------------------- | -------------------------------------- |
| `PageTitle` | Displays the page title and sets browser tab title | Inside page content, after breadcrumbs |
| `PageHeader` | Provides action buttons in the page header | At the app level, outside page routes |
```tsx theme={null}
// Routes are defined with createPageRouter
const PageRouter = createPageRouter(
);
const AppPages = () => {
return (
<>
{/* PageHeader is defined once at the app level */}
HomeDocumentation
>
);
};
hubspot.extend<"pages">(() => );
```
## Guidelines
* **DO:** place `PageTitle` at the top of your page component, immediately after `PageBreadcrumbs`.
* **DO:** keep titles concise and descriptive (typically 1-5 words).
* **DO:** use meaningful titles that help users understand the current page.
* **DO:** consider how the title appears in browser tabs and bookmarks.
* **DO:** include `PageTitle` on every page in your app.
* **DON'T:** use `PageTitle` for section headings - use `Heading` components instead.
* **DON'T:** include redundant words like "Page" in the title (e.g., use "Settings" not "Settings Page").
* **DON'T:** make titles too long - they get truncated in browser tabs.
## Accessibility and user experience
Proper page titles improve both accessibility and user experience:
* **Screen readers**: users with screen readers hear the page title when navigating
* **Browser tabs**: users can identify pages when switching between tabs
* **Browser history**: meaningful titles make it easier to find pages in browser history
* **Bookmarks**: descriptive titles create useful bookmark names
* **Navigation**: clear titles help users understand where they are in your app
## Related resources
* [Page routing guide](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing)
* [PageBreadcrumbs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-breadcrumbs)
* [PageHeader](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header)
* [Heading](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/heading)
* [PageLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link)
# CrmActionButton
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-button
Learn about the CrmActionButton component for use in UI extensions.
The `CrmActionButton` component renders a button that can execute a built-in set of [CRM actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions).
This type of component is useful for enabling your extension to interact with other CRM entities, such as records and engagements. To learn more about how CRM action components work together, check out the [CRM action components overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview).
```jsx theme={null}
import { CrmActionButton } from "@hubspot/ui-extensions/crm";
Preview deal
;
```
## Props
| Prop | Type | Description |
| --------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `actionContext` | Object | An object containing the CRM object and record context for performing the action. See [list of available actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions) for required context values. |
| `actionType` | String | The type of action to perform. See [list of available actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions) for more information. |
| `disabled` | Boolean | When set to `true`, button renders in a disabled, greyed-out state and cannot be clicked. |
| `onClick` | `() => void` | A function that will be invoked when the button is clicked. It receives no arguments and its return value is ignored. |
| `onError` | `(errors: string[]) => void` | An optional callback that will pass any error messages that were generated. Common errors include missing required context values or the user not having sufficient permissions to perform an action. |
| `size` | `'xs'`, `'extra-small'` \| `'sm'`, `'small'` \| `'md'`, `'medium'` (default) | The size of the button. |
| `type` | `'button'` (default) \| `'reset'` \| `'submit'` | The button's HTML `role` attribute. |
| `variant` | `'primary'` \| `'secondary'` (default) \| `'destructive'` | The color variation of the button. |
## Variants
Using the `variant` prop, you can set the color of the button.
* `'primary'`: a dark blue button for the most frequently used or most important action on an extension. Each extension should only have one primary button.
* `'secondary'`: a grey button to provide alternative or non-primary actions. Each extension should include no more than two secondary buttons.
* `'destructive'`: a red button for actions that delete, disconnect, or perform any action that the user can't undo. Button text should clearly communicate what is being deleted or disconnected. After a destructive button is clicked, the user should have to verify or confirm the action.
## Related components
* [CrmActionLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-link)
* [CrmCardActions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-card-actions)
* [Dropdown](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/dropdown)
# CrmActionLink
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-link
Learn about the CrmActionLink component for use in UI extensions.
The `CrmActionLink` component renders a clickable link that can execute a built-in set of [CRM actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions).
This type of component is useful for enabling your extension to interact with other CRM entities, such as records and engagements. To learn more about how CRM action components work together, check out the [CRM action components overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview).
```jsx theme={null}
import { CrmActionLink } from "@hubspot/ui-extensions/crm";
const dealContext = {
objectTypeId: "0-3",
objectId: 14795354663,
};
hubspot.extend(({ context, runServerlessFunction, actions }) => {
return (
<>
Add a note about this deal to the record
>
);
});
```
## Props
| Prop | Type | Description |
| --------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `actionContext` | Object | An object containing the CRM object and record context for performing the action. See [list of available actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions) for required context values. |
| `actionType` | String | The type of action to perform. See [list of available actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions) for more information. |
| `onClick` | `() => void` | A function that will be invoked when the button is clicked. It receives no arguments and its return value is ignored. |
| `onError` | `(errors: string[]) => void` | An optional callback that will pass any error messages that were generated. Common errors include missing required context values or the user not having sufficient permissions to perform an action. |
| `variant` | `'primary'` (default) \| `'light'` \| `'dark'` \| `'destructive'` | The color variation of the link. See the [variants section](#variants) for more information. |
## Variants
Using the `variant` prop, you can control the color of the link.
* `'primary'`: the default blue (`#0091ae`).
* `'light'`: a white link that turns to a lighter shade of blue on hover (`#7fd1de`).
* `'dark'`: a darker shade of blue (`#33475b`).
* `'destructive'`: a red link (`#f2545b`).
## Related components
* [CrmActionButtons](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-button)
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
* [CrmCardActions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-card-actions)
# CrmCardActions
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-card-actions
Learn about the CrmCardActions component for use in UI extensions.
The `CrmCardActions` component renders a smaller standalone or dropdown menu button that can contain multiple [CRM actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions).
This type of component is useful for enabling your extension to interact with other CRM entities, such as records and engagements. To learn more about how CRM action components work together, check out the [CRM action components overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview).
```jsx theme={null}
import { CrmCardActions } from "@hubspot/ui-extensions/crm";
;
```
## Props
Unlike [CrmActionButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-button) and [CrmActionLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-link) where props such as `actionType` are accepted at the top level, `CrmCardActions` includes an `actionConfigs` prop which accepts fields for action configuration.
| Prop | Type | Description |
| --------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `actionConfigs` | Array | An array that stores fields for configuration button actions. See below for list of supported fields. |
| `label` | String | The button's label text. |
| `onError` | `(errors: string[]) => void` | An optional callback that will pass any error messages that were generated. Common errors include missing required context values or the user not having sufficient permissions to perform an action. |
In the `actionConfigs` array, you can include the following fields:
| Field | Type | Description |
| --------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type` | `'action-library-button'` \| `'dropdown'` | The type of button to render:
`action-library-button`: a standalone button that can perform one action.
`dropdown`: a dropdown menu button containing multiple `'action-library-button'` actions. When using this type, you'll need to include an `options` array containing each action.
|
| `options` | Array | For `dropdown` type buttons, this array stores objects for each action in the dropdown menu. Each action should be set to the `'action-library-button'` type. |
| `actionType` | String | The type of action to perform. See [list of available actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions) for more information. |
| `actionContext` | Object | An object containing the CRM object and record context for performing the action. See [list of available actions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview#available-actions) for required context values. |
| `disabled` | Boolean | When set to `true`, the button or dropdown menu option will render in a disabled, greyed-out state and can't be clicked. |
| `tooltipText` | String | Tooltip text that appears when hovering over the button or dropdown menu option. |
## Related components
* [CrmActionLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-link)
* [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button)
* [ButtonRow](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button-row)
# CRM action components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview
Learn about CRM action components for use in UI extensions.
CRM action components provide a built-in set of CRM-related actions, including adding notes to records, opening a one-to-one email composition window, creating new records, and more. Each component can perform the same set of actions, so which component to choose will depend on your needs and preferences.
Below, learn more about CRM action components and actions available to each.
CRM action components are imported from `@hubspot/ui-extensions/crm`.
```jsx theme={null}
import { CrmActionButton, CrmActionLink, CrmCardActions } from "@hubspot/ui-extensions/crm";
```
## Available components
1. [CrmActionButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-button)**:** renders a button.
2. [CrmActionLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-action-link)**:** renders a clickable link.
3. [CrmCardActions](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/crm-card-actions)**:** renders dropdown menu buttons in the top right of the extension.
Users can only take actions through these components when they have the proper permissions. For example, if a user doesn't have permission to create deal records, they won't be able to use a CRM action component to create a deal record. Instead, an error message will be generated and returned through an optional `onError` callback.
Each action requires an `actionType` and `actionContext`.
* `actionType`: the type of action. See the [available actions section](#available-actions) below.
* `actionContext`: the CRM object and record context required for the action to be performed. For example, to include an action to open a preview sidebar for a specified record, you'll need to provide the record's `objectTypeId` and `objectId` in `actionContext`. See the [available actions section](#available-actions) for more information about what's required for each action.
## Available actions
The following actions are available for CRM action components:
* [Preview a CRM record](#preview-a-crm-record)
* [Create a note](#create-a-note)
* [Create a task](#create-a-task)
* [Send a one-to-one email](#send-a-one-to-one-email)
* [Schedule a meeting](#schedule-a-meeting)
* [Create an associated CRM record](#create-an-associated-record)
* [Navigate to an engagement](#navigate-to-an-engagement)
* [Navigate to a CRM record](#navigate-to-a-crm-record)
* [Navigate to a HubSpot page](#navigate-to-a-hubspot-page)
* [Navigate to an external page](#navigate-to-an-external-page)
### Preview a CRM record
The `PREVIEW_OBJECT` action opens a preview sidebar for the specified CRM record.
Requires the following `actionContext`:
* `objectTypeId`: the CRM record's object type (e.g., `0-1` for contacts). See [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-id).
* `objectId`: the ID of the CRM record to preview.
```jsx theme={null}
Preview deal
;
```
### Create a note
The `ADD_NOTE` action opens a note composition window, enabling users to add a note to the specified CRM record.
Requires the following `actionContext`:
* `objectTypeId`: the CRM record's object type (e.g., `0-1` for contacts). See [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-id).
* `objectId`: the ID of the CRM record.
```jsx theme={null}
Create note
;
```
### Create a task
The `ADD_TASK` action opens a task composition window, enabling users to add a task to the specified CRM record.
Requires the following `actionContext`:
* `objectTypeId`: the CRM record's object type (e.g., `0-1` for contacts). See [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-id).
* `objectId`: the ID of the CRM record.
```jsx theme={null}
Create task
;
```
### Send a one-to-one-email
The `SEND_EMAIL` action opens a one-to-one email composition window, enabling users to send an email to the specified contact or the contacts associated with the specified record.
Requires the following `actionContext`:
* `objectTypeId`: the CRM record's object type (e.g., `0-1` for contacts). See [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-id).
* `objectId`: the ID of the CRM record to send the email to.
* `initialEmailSubject`: optionally, set to a string to prefill the email subject line in the composition window.
* `initialEmailBody`: optionally, set to a string to prefill the email body in the composition window.
```jsx theme={null}
Send email
;
```
### Schedule a meeting
The `SCHEDULE_MEETING` action opens a window for [scheduling a meeting](https://knowledge.hubspot.com/meetings-tool/create-and-edit-scheduling-pages).
Requires the following `actionContext`:
* `objectTypeId`: the CRM record's object type (e.g., `0-1` for contacts). See [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-id).
* `objectId`: the ID of the CRM record to schedule the meeting with.
```jsx theme={null}
Schedule meeting
;
```
### Create an associated record
The `OPEN_RECORD_ASSOCIATION_FORM` action opens a side panel for creating a new record to be associated with another.
Requires the following `actionContext`:
* `objectTypeId`: the type of CRM record to create (e.g., `0-2` for companies). See [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-id).
* `association`: an object containing information about the record that the new one will be associated with. This is typically the currently displaying record. Contains:
* `objectTypeId`: the type of CRM record to associate the new one with.
* `objectId`: the ID of the CRM record to associate the new one with.
```jsx theme={null}
Create new record
;
```
### Navigate to an engagement
The `ENGAGEMENT_APP_LINK` action navigates the user to a specific engagement on a CRM record timeline, such as a call or task.
Requires the following `actionContext`:
* `objectTypeId`: the type of CRM record to navigate to (e.g., `0-2` for companies). See [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-id).
* `objectId`: the ID of the CRM record to navigate to.
* `engagementId`: the ID of the engagement, such as a task or note.
* `external`: optionally, set to `true` to navigate to the engagement in a new browser tab.
```jsx theme={null}
Open note
;
```
### Navigate to a CRM record
The `RECORD_APP_LINK` action navigates the user to a specific CRM record.
Requires the following `actionContext`:
* `objectTypeId`: the type of CRM record to navigate to (e.g., `0-2` for companies). See [full list of object IDs](/docs/guides/crm/understanding-the-crm#object-type-id).
* `objectId`: the ID of the CRM record to navigate to.
* `external`: optionally, set to `true` to navigate to the record in a new browser tab.
* `includeEschref`: optionally, set to `true` to include a *Back* button in the top left corner of the opened CRM record to navigate the user back to the original record.
```jsx theme={null}
View company
;
```
### Navigate to a HubSpot page
The `PAGE_APP_LINK` navigates the user to any page within the HubSpot account. Use this action when a user would need to navigate to a non-CRM record account page, such as the email tool.
Requires the following `actionContext`:
* `path`: the URL path of the HubSpot page. This path is relative to `https://app.hubspot.com` and should begin with `/`.
* `external`: optionally, set to `true` to navigate to the page in a new browser tab.
```jsx theme={null}
Open email dashboard
;
```
### Navigate to an external page
The `EXTERNAL_URL` action navigates the user to a website page in a new tab.
Requires the following `actionContext`:
* `href`: the URL, which must begin with `http` or `https`. When protocol is not specified, HubSpot will automatically prefix the URL with `https`.
```jsx theme={null}
Open Google
;
```
# CrmAssociationPivot
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-pivot
Learn about the AssociationPivot component for use in UI extensions.
The `CrmAssociationPivot` component is a CRM data component that renders a list of associated records organized by their assigned [association label](https://knowledge.hubspot.com/object-settings/create-and-use-association-labels). You'll specify the type of records that you want to appear along with table attributes such as pagination, sorting, and more. You can either return all labels or specify the labels to return.
```jsx theme={null}
import { CrmAssociationPivot } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return (
);
};
```
## Props
| Prop | Type | Description |
| ----------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `objectTypeId` Required | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/docs/guides/crm/understanding-the-crm#object-type-id) of object IDs. |
| `associationLabels` | Array | Filters results by specific association labels. By default, all association labels will appear. |
| `maxAssociations` | Number | The number of items to return in each association label group before displaying a "Show more" button. |
| `preFilters` | Array | Filters the data by specific values of the associated records. Review the [CRM data filter options](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview#filtering-data) for more information. |
| `sort` | Array | The default sorting behavior for the table. In the array, you'll include an object for the column you want to sort by, which specifies:
`columnName`: the column to sort by.
`direction`: the direction to sort by. Can be either `1` (ascending) or `-1` (descending). By default, order is ascending.
|
## Related components
* [CrmAssociationPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-property-list)
* [CrmAssociationTable](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-table)
* [CrmReport](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-report)
# CrmAssociationPropertyList
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-property-list
Learn about the CrmAssociationPropertyList component for use in UI extensions.
The `CrmAssociationPropertyList` component renders a list of properties belonging to a record associated with the currently displaying record. For example, you can use this component to display properties of a company record from its associated contact record. You can edit these property values inline, and changes will automatically save when leaving the field or pressing Enter.
```jsx theme={null}
import { CrmAssociationPropertyList } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return (
);
};
```
## Props
| Prop | Type | Description |
| ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `associationLabels` | Array | When provided, returns associated records that have all the specified labels. |
| `filters` | Array | Filters the data by specific values of the associated records. Review the [CRM data filter options](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview#filtering-data) for more information. |
| `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/docs/guides/crm/understanding-the-crm#object-type-id) of object IDs. |
| `properties` | Array | The list of properties to display from the associated record, up to 24. |
| `sort` | Array | The default sorting behavior for the table. In each sort object in the array, you'll specify the following:
`columnName`: the column to sort by.
`direction`: the direction to sort by. Can be either `1` (ascending) or `-1` (descending). By default, order is ascending.
|
## Related components
* [CrmAssociationTable](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-table)
* [CrmReport](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-report)
* [CrmAssociationPivot](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-pivot)
# CrmAssociationStageTracker
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-stage-tracker
Learn about the CrmAssociationStageTracker component for use in UI extensions.
The `CrmAssociationStageTracker` component renders a lifecycle or pipeline stage progress bar and a list of properties.
Use this component to show stage progress for records associated with the currently displaying record. Each component instance can fetch data from one type of object (contacts, companies, deals, tickets, or custom objects). You can specify association labels to only display data from associated CRM records with that assigned label. You can also edit the property values inline.
```jsx theme={null}
import { CrmAssociationStageTracker } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return (
);
};
```
## Props
| Prop | Type | Description |
| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `associationLabels` | Array | If provided, will only request a list of associated records with the specified label (case sensitive). |
| `filters` | Array | If provided, the component will request a list of associated records that match the specified criteria. Learn more about [filtering in CRM data components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview#filtering-data). |
| `objectTypeId` | String | The numeric ID of the type of associated object to display data from (e.g., `0-1` for contacts). See [complete list](/docs/guides/crm/understanding-the-crm#object-type-ids) of object IDs. |
| `properties` | Array | The properties of the associated records to display, up to four. |
| `showProperties` | Boolean | Whether to display the properties below the progress indicator. When set to `false`, properties will not display. |
| `sort` | Array | If provided, overrides the default sorting rules used in the search query. In the array, you'll include an object for the column you want to sort by, which specifies:
`columnName`: the column to sort by.
`direction`: the direction to sort by. Can be either `1` (ascending) or `-1` (descending). By default, order is ascending.
|
## Related components
* [CrmPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list)
* [CrmDataHighlight](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-data-highlight)
* [CrmReport](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-report)
* [CrmStageTracker](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-stage-tracker)
# CrmAssociationTable
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-table
Learn about the CrmAssociationTable component for use in UI extensions.
The `CrmAssociationTable` component renders a table of associated records with optional filtering, sorting, and search methods. You'll specify the type of records that you want to appear along with the properties to display as columns.
```jsx theme={null}
import { CrmAssociationTable } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return (
);
};
```
## Props
| Prop | Type | Description |
| ------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `associationLabelFilter` | Boolean | When set to `false`, hides the "Association label" quick filter above the table. |
| `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/docs/guides/crm/understanding-the-crm#object-type-ids) of object IDs. |
| `pageSize` | Number | The number of rows to include per page of results. Include the `pagination` property to enable users to navigate through returned results. |
| `pagination` | Boolean | When set to `false`, hides the pagination navigation below the table. |
| `preFilters` | Array | Filters the data by specific values of the associated records. Review the [CRM data filter options](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview#filtering-data) for more information. |
| `propertyColumns` | Array | The properties to display as table columns. |
| `quickFilterProperties` | Array | The properties that appear as filters above the table. When included, the "Association label" quick filter will always display. See note below for more details on this prop. |
| `searchable` | Boolean | When set to `false`, hides the search bar above the table. |
| `sort` | Array | The default sorting behavior for the table. In each sort object in the array, you'll specify the following:
`columnName`: the column to sort by.
`direction`: the direction to sort by. Can be either `1` (ascending) or `-1` (descending). By default, order is ascending.
|
**Please note:**
For `quickFilterProperties`:
* By default, four quick filters will display automatically depending on the object type.
* **Contacts (`0-1`):** `[ 'hubspot_owner_id', 'createdate', 'hs_lead_status', 'notes_last_updated' ]`
* **Companies (`0-2`):** `[ 'hubspot_owner_id', 'hs_lead_status', 'notes_last_updated', 'createdate' ]`
* **Deals (`0-3`):** `[ 'hubspot_owner_id', 'closedate', 'createdate', 'dealstage' ]`
* **Tickets (`0-5`):** `[ 'hubspot_owner_id', 'createdate', 'hs_pipeline_stage', 'hs_lastactivitydate' ]`
* Custom objects do not have default quick filters.
* An empty array (`[]`) will remove any default quick filters except for "Association label."
## Related components
* [CrmAssociationPivot](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-pivot)
* [CrmAssociationPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-property-list)
* [CrmReport](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-report)
# CrmDataHighlight
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-data-highlight
Learn about the CrmDataHighlight component for use in UI extensions.
The `CrmDataHighlight` component renders a list of properties along with their values. You can use this component to surface important property data from either the currently displaying record or another specified record.
```jsx theme={null}
import { CrmDataHighlight } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return (
);
};
```
## Props
| Prop | Type | Description |
| -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `objectId` | String | The ID of the CRM record to display property data from. |
| `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/docs/guides/crm/understanding-the-crm#object-type-ids) of object IDs. |
| `properties` | Array | The properties to display, up to four. By default, will display property data from the currently displaying record. To pull data from a specific record, include the `objectTypeId` and `objectId` props. |
## Related components
* [CrmPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list)
* [CrmStageTracker](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-stage-tracker)
* [CrmAssociationPivot](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-pivot)
# CrmPropertyList
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list
Learn about the CrmPropertyList component for use in UI extensions.
The `CrmPropertyList` component renders a list of properties along with their values. You can use this component to surface important property data from either the currently displaying record or another specified record. You can edit these property values inline and will automatically save when leaving the field or pressing Enter.
```jsx theme={null}
import { CrmPropertyList } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return (
);
};
```
## Props
| Prop | Type | Description |
| -------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `direction` | `'column'` (default) \| `'row'` | The layout direction of the table. |
| `objectId` | String | The ID of the CRM record to display property data from. |
| `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts). See [complete list](/docs/guides/crm/understanding-the-crm#object-type-ids) of object IDs. |
| `properties` | Array | The properties to display, up to 24. By default, will display property data from the currently displaying record. To pull data from a specific record, include the `objectTypeId` and `objectId` props. |
## Related components
* [CrmDataHighlight](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-data-highlight)
* [CrmStageTracker](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-stage-tracker)
* [CrmAssociationPivot](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-pivot)
# CrmReport
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-report
Learn about the CrmReport component for use in UI extensions.
The `CrmReport` component renders a [single object report](https://knowledge.hubspot.com/reports/create-custom-single-object-reports), which can be filtered with the `use` prop to surface data based on the currently displaying record, its associations, or unfiltered.
By default, the report data will automatically filter for the currently displaying record, as long as there is an association between the displaying record and records included in the report.
For example, using this component you can display a single object report that shows which deals closed this quarter. When viewing the report on a contact record, by default the report will only display data from deals associated with that contact.
This component requires you to specify the ID of the report to render. To get a report's ID:
* In your HubSpot account, navigate to **Reports** > **Reports**.
* Click the **name** of the report you want to display.
* In the URL, copy the **number** that is not your HubID.
**Please note:**
Report data will only display for users with [permissions to view reports](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#reports).
```js theme={null}
import { CrmReport } from '@hubspot/ui-extensions/crm';
const Extension = () => {
return (
;
);
};
```
## Props
| Prop | Type | Description |
| ---------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reportId` | String | The numeric ID of the single object report, which can be found in the URL when viewing the report in HubSpot. |
| `use` | `'associations'` (default) \| `'subject'` \| `'unfiltered'` | Specifies how the report should be filtered based on its relationship to the currently displaying CRM record:
`associations`: report will only include data from records associated with the currently displaying record.
`subject`: report will only include data from the currently displaying record. Will not include data from associated records.
`unfiltered`: report will display all data regardless of the currently displaying record and its associations.
|
## Related components
* [CrmAssociationPivot](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-pivot)
* [CrmAssociationPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-property-list)
* [CrmAssociationTable](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-table)
# CrmStageTracker
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-stage-tracker
Learn about the CrmStageTracker component for use in UI extensions.
The `CrmStageTracker` component renders a lifecycle or pipeline stage progress bar and a list of properties. Available for contacts, companies, deals, tickets, and custom objects.
Use this component to show stage progress for the currently displaying record, or you can specify a record. You can also edit the property values inline and your changes will automatically save when leaving the field or pressing Enter.
```jsx theme={null}
import { CrmStageTracker } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return ;
};
```
## Props
| Prop | Type | Description |
| ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `objectId` | String | The ID of the CRM record to display property data from. |
| `objectTypeId` | String | The numeric ID of the type of associated object to display (e.g., `0-1` for contacts. See [complete list](/docs/guides/crm/understanding-the-crm#object-type-ids) of object IDs. |
| `properties` | Array | The properties to display, up to four. By default, will display property data from the currently displaying record. To pull data from a specific record, include the `objectTypeId` and `objectId` props. |
| `showProperties` | Boolean | Whether to display the properties below the progress indicator. When set to `false`, properties will not display. |
## Related components
* [CrmPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list)
* [CrmDataHighlight](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-data-highlight)
* [CrmReport](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-report)
# CrmStatistics
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-statistics
Learn about the CrmStatistics component for use in UI extensions.
The `CrmStatistics` component renders data summaries calculated from the currently displaying CRM record's associations. For example, you can use this component to display data such as:
* The average revenue of all of a contact’s associated companies.
* The total number of times that a company has been contacted based on all of their associated tickets.
* The maximum number of days to close from all of a company's associated deals.
To render data, you'll specify the properties you want to read from the associated records along with the type of calculation to perform on the property values. For each property, you can also include filters to narrow down the records that are included in the calculation.
```jsx theme={null}
import { CrmStatistics } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return (
= 10,000
// - Deal must not be closed
filterGroups: [
{
filters: [
{
operator: "GTE",
property: "amount",
value: 10000,
},
{
operator: "EQ",
property: "hs_is_closed",
value: "false",
},
],
},
],
},
]}
/>
);
};
```
## Props
| Prop | Type | Description |
| -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `objectTypeId` | String | The numeric ID of the type of object to fetch statistics about (e.g., `0-1` for contacts). See [complete list](/docs/guides/crm/understanding-the-crm#object-type-ids) of object IDs. |
| `statistics` | Array | An array of objects that define each statistic to fetch. Supports the following fields:
`label`
`propertyName`
`statisticType`
`filterGroups`
[Learn more about these fields below](#specifying-statistics-data). |
## Specifying statistics data
Using the `statistics` prop, you'll define the data that you want the component to display. Data is fetched from CRM properties, and is calculated based on the specified `statisticType`. You'll include an object for each statistic that you want to fetch. You can also optionally specify `filterGroups` to further refine the data.
Below are the supported fields for objects in the `statistics` array.
| Field | Type | Description |
| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | String | The label that displays above the statistic. |
| `propertyName` | String | The name of the property to fetch data from. Must be a number, date, or datetime property. Requesting any other type of property will result in the statistic displaying `--` for its value. |
| `statisticType` | String | The type of statistic to request. Supported values include:
`SUM`: the sum of the values of the specified property.
`AVG`: the average of the values of the specified property.
`MIN`: the smallest value of the specified property.
`MAX`: the largest value of the specified property.
`COUNT`: the number of CRM records with a value for the specified property.
`DISTINCT_APPROX`: an approximate count of distinct values for the specified property.
`PERCENTILES`: the property value at which a certain percentage of observed values occur.
|
| `filterGroups` | String | An optional field for further refining the values that are included in the statistic. Up to three filter group objects may be specified in this array, and you can include up to three filters in each item. Exceeding these limits will result in the statistic showing -- for its value. Filters are structured the same way as filters in the [CRM search API](/docs/api-reference/latest/crm/search-the-crm#filter-search-results). |
| `percentiles` | Number | When `statisticType` is `PERCENTILES`, this field is required. Specifies the percentile to display. Must be an integer from 0-100, inclusive. |
## Related components
* [CrmReport](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-report)
* [CrmAssociationTable](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-table)
* [CrmAssociationPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-property-list)
# CRM data components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview
Learn about the CRM data component for use in UI extensions.
CRM data components can pull data directly from the currently displaying CRM record, including information about associated records and single object reports. These components can only be placed in the middle column of CRM records.
These components are imported from `@hubspot/ui-extensions/crm`.
```jsx theme={null}
import { CrmAssociationPivot, CrmReport } from "@hubspot/ui-extensions/crm";
```
## Available components
* [CrmAssociationPivot](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-pivot)
* [CrmAssociationPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-property-list)
* [CrmAssociationTable](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-association-table)
* [CrmDataHighlight](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-data-highlight)
* [CrmPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list)
* [CrmReport](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-report)
* [CrmStageTracker](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-stage-tracker)
* [CrmStatistics](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-statistics)
## Filtering data
In the `CrmAssociationPivot` and `CrmAssociationTable` components, you can filter the data to fetch only what's most relevant. Review the table below for available filtering options.
```jsx theme={null}
import { CrmAssociationPivot, CrmReport } from "@hubspot/ui-extensions/crm";
const Extension = () => {
return (
);
};
```
| Prop | Type | Description |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `highValue` | String \| number | The upper value to filter by when using an operator that requires a range, such as `BETWEEN`. |
| `operator` | `EQ` \|`NEQ` \| `LT` \| `LTE` \| `GT` \| `GTE` \| `BETWEEN` \| `IN` \| `NOT_IN` \| `HAS_PROPERTY` \| `NOT_HAS_PROPERTY` | The filter's operator (e.g. `IN`). Can be one of:
`EQ`: is equal to `value`.
`NEQ`: is not equal to `value`.
`LT`: is less than `value`.
`LTE`: is less than or equal to `value`.
`GT`: is greater than `value`.
`GTE`: is greater than or equal to `value`.
`BETWEEN`: is within the specified range between `value` and `highValue`.
`IN`: is included in the specified `values` array. This operator is case-sensitive, so inputted values must be in lowercase.
`NOT_IN`: is not included in the specified `values` array.
`HAS_PROPERTY`: has a value for the specified property.
`NOT_HAS_PROPERTY`: does not have a value for the specified property.
Learn more about [filtering CRM searches](/docs/api-reference/latest/crm/search-the-crm#filter-operators). |
| `property` | String | The property to filter by. |
| `value` | String \| number | The property value to filter by. |
| `values` | String \| number | The property values to filter by when using an operator that requires an array, such as `IN`. |
# BarChart | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/bar-chart
Learn about the BarChart component for use in UI extensions.
The `BarChart` component renders a bar chart for visualizing data. This type of chart is best suited for comparing categorical data. Alternatively, you can use a [LineChart](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/line-chart) component for time series plots or visualizing trend data. Learn more about [charts](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview).
To see an example of how to implement charts in a UI extension, check out [HubSpot's Charts example project](https://github.com/HubSpot/ui-extensions-examples/tree/main/charts-example). Note that this is a version `2025.1` project, but the chart component implementation would be similar for projects on version `2025.2` or `2026.03`.
1. **Title:** the title of the chart.
2. **Legend:** lists the data categories with their corresponding color for readability.
3. **Axis label:** the label for the axis.
4. **Data labels:** labels for data points.
```jsx theme={null}
import { BarChart } from '@hubspot/ui-extensions';
const dailyInventorySample = [
{
Product: 'Hats',
Amount: 187,
},
{
Product: 'Socks',
Amount: 65,
},
{
Product: 'Ascots',
Amount: 120,
}
];
return (
);
};
```
## Props
| Parameter | Type | Description |
| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data` | Object | An object containing the chart's data in an array.
Data should be formatted as comma-separated objects containing key-value pairs.
Data will be displayed in the order it's provided, so any sorting will need to be done before passing it to the component.
While it's recommended to pre-format your data to be human-readable, you can also provide the `propertyLabels` parameter via this prop's `options` to relabel data values. See example in the [Stacking section](#stacking) below.
Learn more about [formatting data](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#formatting-data). |
| `axes` | Object | Configures the chart's axes. Using the `x` and `y` fields, you'll configure each axis individually with `field` and `fieldType` parameters, along with an optional `label` parameter:
`field` (Required): the field from your dataset to use. This value will be used as the displayed axis label if no `label` is specified.
`fieldType` (Required): the type of field. Can be `category`, `datetime`, or `linear`. Learn more about [field types](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#configuring-axes).
`label`: the axis label. If not specified, the `field` value will be used.
You can also include an `options` field to further configure the axes with the following options:
`groupFieldByColor` (string): specify a field to [apply color](#colors) to for visual clarity.[](#color-grouping)
`stacking` (boolean): [stack grouped data](#stacking) instead of always rendering separate bars.
`colors` (object): [specify colors for values](#specify-colors-per-field-value) in the field specified in `groupFieldByColor`.
Learn more about [chart axes](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#configuring-axes). |
| `options` | Object | Additional chart configuration options. Options include:
`title` (string): a title for the chart.
`showLegend` (boolean): set to `true` to display a legend above the chart.
`showDataLabels` (boolean): set to `true` to display labels above data points.
`showTooltips` (boolean): set to `true` to display tooltips for data points on hover.
`colorList` (array): specify a custom order for colors to used in the report.
Learn more about [chart options](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#chart-options). |
## Colors
To apply colors to a chart for visual clarity, you can group data fields by color using the `groupFieldByColor` parameter within the axes `options`. For example, the bar chart below use `groupFieldByColor` to add colors to each `Product` defined in the dataset.
```jsx theme={null}
const dailyInventorySample = [
{
Product: "Standalone product A",
Sales: 159,
},
{
Product: "Bundle A",
Sales: 53,
},
{
Product: "Bundle B",
Sales: 99,
},
];
return (
);
```
For comparison, below is the same chart without `groupFieldByColor` configured for the axes:
Colors will be automatically assigned in a [preset order](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#colors). To customize the color selection order, include the `colorList` field in the `options` prop, then specify the colors to pick from as shown below.
```jsx theme={null}
const dailyInventorySample = [
{
Product: "Standalone product A",
Sales: 159,
},
{
Product: "Bundle A",
Sales: 53,
},
{
Product: "Bundle B",
Sales: 99,
},
];
return (
);
```
Or you can specify colors to use for specific values in the field specified in `groupFieldByColor`. To do so, include the `colors` field within the axes `options`, then specify each field value and color, as shown below. Learn more about [colors](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#colors).
```jsx theme={null}
const dailyInventorySample = [
{
Product: "Standalone product A",
Sales: 159,
},
{
Product: "Bundle A",
Sales: 53,
},
{
Product: "Bundle B",
Sales: 99,
},
];
return (
);
```
## Stacking
Use the stacking axes option to stack grouped data rather than rendering individual bars. For example, the following bar chart displays the number of deals by deal stage. The data also includes the sales rep who owns each deal. Because two sales reps have deals in the same deal stage, `stacking` has been set to `true` to visually combine the data into one bar.
```jsx theme={null}
const Extension = ({ context }) => {
const dealCountSample = [
{
count: 1,
dealstage: "appointmentScheduled",
user_id: "194784",
},
{
count: 2,
dealstage: "closedWon",
user_id: "295834",
},
{
count: 1,
dealstage: "closedWon",
user_id: "938453",
},
];
return (
);
};
```
Because the `dealstage` field data is not written in a human-readable format (e.g., `appointmentScheduled`), the data prop includes `propertyLabels` in its `options` to convert the labels. Note that the data prop formatting is slightly different to accommodate both the dataset and its `options`. Learn more about [data options](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#data-options).
For comparison, below is the same chart without stacking. In this version, each sales rep has their own bar in the deal stage category.
## Guidelines
* **DO:** title your data categories with human-readable text so they are easy to understand.
* **DO:** use sentence-casing for the data categories and chart title (only first letter capitalized).
* **DO:** sort your data in ascending/descending order of your x-axis field to prevent unordered rendering prior to passing it to a charting component. If you intend to display information over time, your data will be displayed in the order you provide it.
* **DO:** display the chart legend if you’re graphing more than one category of data. This prevents your users from having to rely only on color to identify different data on your chart.
* **DO:** for readability, use larger surfaces to showcase charts, such as the record page middle column. Avoid using charts with many data points on smaller surfaces such as the preview panel or sidebar.
* **DON’T:** use more than 14 data categories unless it cannot be avoided for your use case.
* **DON’T:** use the same colors to indicate different data categories.
## Related components
* [BarChart](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/bar-chart)
* [Statistics](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/statistics)
* [Table](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/table)
# LineChart | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/line-chart
Learn about the LineChart component for use in UI extensions.
The `LineChart` component renders a line chart for visualizing data. This type of chart is best suited for time series plots or trend data. Alternatively, you can use a [BarChart](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/bar-chart) component for comparing categorical data. Learn more about [charts](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview).
To see an example of how to implement charts in a UI extension, check out [HubSpot's Charts example project](https://github.com/HubSpot/ui-extensions-examples/tree/main/charts-example). Note that this is a version `2025.1` project, but the chart component implementation would be similar for projects on version `2025.2` or `2026.03`.
1. **Title:** the title of the chart.
2. **Legend:** lists the data categories with their corresponding color for readability.
3. **Axis label:** the label for the axis.
4. **Data labels:** labels for data points.
```jsx theme={null}
import { LineChart } from "@hubspot/ui-extensions";
const salesOverTimeSample = [
{
Date: "2024-08-01",
Sales: 10,
},
{
Date: "2024-08-02",
Sales: 30,
},
{
Date: "2024-08-03",
Sales: 60,
},
];
return (
);
```
## Props
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `data` | Object | An object containing the chart's data in an array.
Data should be formatted as comma-separated objects containing key-value pairs.
Data will be displayed in the order it's provided, so any sorting will need to be done before passing it to the component.
While it's recommended to pre-format your data to be human-readable, you can also provide the `propertyLabels` parameter via this prop's `options` to relabel data values. See example in the [Stacking section](#stacking) below.
Learn more about [formatting data](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#formatting-data). |
| `axes` | Object | Configures the chart's axes. Using the `x` and `y` fields, you'll configure each axis individually with `field` and `fieldType` parameters, along with an optional `label` parameter:
`field` (Required): the field from your dataset to use. This value will be used as the displayed axis label if no `label` is specified.
`fieldType` (Required): the type of field. Can be `category`, `datetime`, or `linear`. Learn more about [field types](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#configuring-axes).
`label`: the axis label. If not specified, the `field` value will be used.
You can also include an `options` field to further configure the axes with the following options:
`groupFieldByColor` (string): specify a field to [apply color](#colors) to for visual clarity.[](#color-grouping)
`colors` (object): [specify colors for values](#specify-colors-per-field-value) in the field specified in `groupFieldByColor`.
Learn more about [chart axes](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#configuring-axes). |
| `options` | Object | Additional chart configuration options. Options include:
`title` (string): a title for the chart.
`showLegend` (boolean): set to `true` to display a legend above the chart.
`showDataLabels` (boolean): set to `true` to display labels above data points.
`showTooltips` (boolean): set to `true` to display tooltips for data points on hover.
`colorList` (array): specify a custom order for colors to used in the report.
Learn more about [chart options](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#chart-options). |
## Colors
To apply [colors](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#colors) to a chart for visual clarity, you can group data fields by color using the `groupFieldByColor` parameter within the axes `options`. For example, the line chart below use `groupFieldByColor` to add colors to each `Breakdown` category defined in the dataset.
```jsx theme={null}
const VisitsPerSourceOverTime = [
{
"Session Date": "2019-09-01",
Breakdown: "Direct",
Visits: 1277,
},
{
"Session Date": "2019-09-01",
Breakdown: "Referrals",
Visits: 1882,
},
{
"Session Date": "2019-09-01",
Breakdown: "Email",
Visits: 1448,
},
{
"Session Date": "2019-09-02",
Breakdown: "Direct",
Visits: 1299,
},
{
"Session Date": "2019-09-02",
Breakdown: "Referrals",
Visits: 1869,
},
{
"Session Date": "2019-09-02",
Breakdown: "Email",
Visits: 1408,
},
{
"Session Date": "2019-09-03",
Breakdown: "Direct",
Visits: 1357,
},
{
"Session Date": "2019-09-03",
Breakdown: "Referrals",
Visits: 1931,
},
{
"Session Date": "2019-09-03",
Breakdown: "Email",
Visits: 1391,
},
];
return (
);
```
Colors will be automatically assigned in a [preset order](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#colors). To customize the color selection order, include the `colorList` field in the `options` prop, then specify the colors to pick from as shown below.
```jsx theme={null}
const VisitsPerSourceOverTime = [
{
"Session Date": "2019-09-01",
Breakdown: "Direct",
Visits: 1277,
},
{
"Session Date": "2019-09-01",
Breakdown: "Referrals",
Visits: 1882,
},
{
"Session Date": "2019-09-01",
Breakdown: "Email",
Visits: 1448,
},
{
"Session Date": "2019-09-02",
Breakdown: "Direct",
Visits: 1299,
},
{
"Session Date": "2019-09-02",
Breakdown: "Referrals",
Visits: 1869,
},
{
"Session Date": "2019-09-02",
Breakdown: "Email",
Visits: 1408,
},
{
"Session Date": "2019-09-03",
Breakdown: "Direct",
Visits: 1357,
},
{
"Session Date": "2019-09-03",
Breakdown: "Referrals",
Visits: 1931,
},
{
"Session Date": "2019-09-03",
Breakdown: "Email",
Visits: 1391,
},
];
return (
);
```
Or you can specify colors to use for specific values in the field specified in `groupFieldByColor`. To do so, include the `colors` field within the axes `options`, then specify each field value and color, as shown below. Learn more about [colors](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#colors).
```jsx theme={null}
return (
);
```
## Stacking
Use the `stacking` axes option to stack grouped data for visual comparison. For example, the following line chart displays website visits over time broken down by source. To help users compare data within each breakdown category, `stacking` has been set to `true`.
```jsx theme={null}
const visitsPerSourceOverTime = [
{
sessionDate: "2019-09-01",
breakdown: "direct",
visits: 1277,
},
{
sessionDate: "2019-09-01",
breakdown: "referrals",
visits: 1882,
},
{
sessionDate: "2019-09-01",
breakdown: "email",
visits: 1448,
},
{
sessionDate: "2019-09-02",
breakdown: "direct",
visits: 1299,
},
{
sessionDate: "2019-09-02",
breakdown: "referrals",
visits: 1869,
},
{
sessionDate: "2019-09-02",
breakdown: "email",
visits: 1408,
},
{
sessionDate: "2019-09-03",
breakdown: "direct",
visits: 1357,
},
{
sessionDate: "2019-09-03",
breakdown: "referrals",
visits: 1931,
},
{
sessionDate: "2019-09-03",
breakdown: "email",
visits: 1391,
},
];
return (
);
```
Because the `breakdown` field data is all lowercase (e.g., `direct`), the data prop includes `propertyLabels` in its `options` to convert the labels. Note that the data prop formatting is slightly different to accommodate both the dataset and its `options`. Learn more about [data options](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview#data-options).
## Guidelines
* **DO:** title your data categories with human-readable text so they are easy to understand.
* **DO:** use sentence-casing for the data categories and chart title (only first letter capitalized).
* **DO:** sort your data in ascending/descending order of your x-axis field to prevent unordered rendering prior to passing it to a charting component. If you intend to display information over time, your data will be displayed in the order you provide it.
* **DO:** display the chart legend if you’re graphing more than one category of data. This prevents your users from having to rely only on color to identify different data on your chart.
* **DO:** for readability, use larger surfaces to showcase charts, such as the record page middle column. Avoid using charts with many data points on smaller surfaces such as the preview panel or sidebar.
* **DON’T:** use more than 14 data categories unless it cannot be avoided for your use case.
* **DON’T:** use the same colors to indicate different data categories.
## Related components
* [LineChart](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/line-chart)
* [Statistics](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/statistics)
* [Table](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/table)
# Charts overview | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/overview
Learn about the available chart components for visualizing data in UI extensions.
Use charts to display data visualizations in UI extensions. HubSpot provides two chart components: [BarChart](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/bar-chart) and [LineChart](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/charts/line-chart). Both components use the same API, but the each type is better suited for different types of data. For example, a bar chart is generally recommended for comparing categorical data, while a line chart is recommended for time series plots or visualizing trends. To see an example of how to implement charts in a UI extension, check out [HubSpot's Charts example projects](https://github.com/HubSpot/ui-extensions-examples/tree/main/charts-example). Note that this is a version `2025.1` project, but the chart component implementation would be similar for projects on version `2025.2` or `2026.03`.
Learn more about creating custom charts with UI extensions by watching [this video](https://www.youtube.com/watch?v=5P6WuKyOiDE) on the HubSpot Developers YouTube channel.
On this page:
* [Formatting data](#formatting-data)
* [Configuring axes](#configuring-axes)
* [Stacking](#stacking)
* [Chart options](#chart-options)
* [Colors](#colors)
* [Design guidelines](#design-guidelines)
```js theme={null}
import { BarChart, LineChart } from "@hubspot/ui-extensions";
```
For both types of charts, there are three main props:
* `data`: an object containing the chart data, with additional `options`. Learn more about [data formatting](#formatting-data).
* `axes`: an object that specifies for the `x` and `y` axes, with additional `options`. Learn more about [configuring axes](#configuring-axes).
* `options`: an object that specifies options for the chart, such as showing data labels and tooltips. Learn more about [chart options](#chart-options).
## Formatting data
Data should be provided to a chart component in an array of objects containing key-value pairs, matching the following format `{string: number | string}`. Data will be displayed in the order it's provided to the component, so you will need to sort data beforehand if necessary. For example, to display data over time in a `LineChart`, you should sort the data in ascending/descending order of your `datetime` axis field before passing it to the chart component.
```jsx theme={null}
[
{
type: "referral",
count: 35,
location: "location_A",
},
{
type: "direct",
count: 12,
location: "location_B",
},
];
```
When building out a chart, keep the following in mind:
* A chart can only graph one dataset, so you'll need multiple charts if you're working with multiple datasets.
* For performance and readability, it's recommended for a chart to include no more than a few hundred entries, depending on the data. When working with larger datasets, it's important to consider the information you want to convey with the chart. You'll likely encounter issues with visual clarity before you encounter performance issues. For example, a `BarChart` with hundreds of bars on it will likely not be readable even if it renders quickly.
* Chart components do not support nested fields in data. Rather, all fields will need to be stored at the same level. For example, the following data format is not supported because it introduces a secondary level of data in the `type` field.
```jsx theme={null}
[
{
type: {
value: "referral",
subType: "subtypeValue",
},
count: 35,
location: "location_A",
},
];
```
### datetime values
For charts that include datetime data, you can use the following formats:
* Unix millisecond timestamp (`1758791725`)
* ISO 8601 timestamp (`2025-09-25T09:15:25+0000`)
* ISO 8601 date (`2024-09-25`)
### Data options
It's recommended to pre-format your data into human-readable text so that it doesn't need any additional relabeling. However, if there are times when you can't pre-format certain values in your data, you can include an `options` field in the `data` prop to set `propertyLabels`. Including `options` will slightly change the way you format `data`:
* When only including a dataset array, you'll format the `data` prop as `data={dataArray}`.
* When including `options`, you'll need to format the `data` prop as an object containing both `data` and `options` fields as shown below. In `options`, you'll include a `propertyLabels` object, which then contains an object for each field and labels for each value.
For example, the following chart is configured to relabel the `dealstage` and `user_id` values.
```jsx theme={null}
);
```
## Configuring axes
The `axes` prop configures the chart's axes. Charts can have two axes (`x` and `y`), and each axis is configured by a `field` and `fieldType` parameter. By default, the `field` value will be used as the axis label, but you can also include a `label` parameter to set it separately.
**Please note:**
One axis must have a `fieldType` of `linear`.
```jsx theme={null}
;
```
| Parameter | Type | Description |
| ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `field` | String | The name of the field from the dataset. |
| `fieldType` | String | The type of data in the field. You can specify one of the following types:
`category`: data that can be bucketed into categories or types, such as different types of products.
`datetime`: date and time data. Charts support the following datetime formats:
JavaScript timestamp (`1758791725`)
ISO 8601 timestamp (`2025-09-25T09:15:25+0000`)
ISO 8601 date (`2024-09-25`)
`linear`: numerical data, such as quantity. One of the axes must have this `fieldType`.
|
| `label` | String | The label to display on the axis. If not specified, the `field` value will be used instead. |
| `options` | Object | Additional configuration options for the axes:
`groupFieldByColor` (string): specify a `field` to group by color. When not specified, only one color will be used for data visualization.
`stacking` (boolean): when set to `true`, grouped data will be stacked. Default is `false`.
|
The following bar chart displays sales data by type of product and count of sales. To add visual clarity, each bar is assigned a different [color](#colors) via the `groupFieldByColor` parameter in `options`.
```jsx theme={null}
const dailyInventorySample = [
{
Product: "Hats",
Amount: 187,
},
{
Product: "Socks",
Amount: 65,
},
{
Product: "Ascots",
Amount: 120,
},
];
return (
);
```
### Stacking
Use the `stacking` axes option to stack data by group. For example, the following bar chart displays the number of deals by deal stage. The data also includes the sales rep who owns each deal. To visually distinguish sales reps in each column, `stacking` has been set to `true`.
```jsx theme={null}
const Extension = ({ context }) => {
const dealCountSample = [
{
count: 1,
dealstage: "appointmentScheduled",
user_id: "194784",
},
{
count: 2,
dealstage: "closedWon",
user_id: "295834",
},
{
count: 1,
dealstage: "closedWon",
user_id: "938453",
},
];
return (
);
};
```
Compare the above visualization to the version below without stacking:
Similarly, you can use stacking in line charts, as shown in the example chart below. This chart measures website visits by date, broken down by source. Stacking in this example helps to emphasize volume over time.
```jsx theme={null}
const visitsPerSourceOverTime = [
{
"Session Date": "2019-09-01",
Breakdown: "Direct",
Visits: 1277,
},
{
"Session Date": "2019-09-01",
Breakdown: "Referrals",
Visits: 1882,
},
{
"Session Date": "2019-09-01",
Breakdown: "Email",
Visits: 1448,
},
{
"Session Date": "2019-09-02",
Breakdown: "Direct",
Visits: 1299,
},
{
"Session Date": "2019-09-02",
Breakdown: "Referrals",
Visits: 1869,
},
{
"Session Date": "2019-09-02",
Breakdown: "Email",
Visits: 1408,
},
{
"Session Date": "2019-09-03",
Breakdown: "Direct",
Visits: 1357,
},
{
"Session Date": "2019-09-03",
Breakdown: "Referrals",
Visits: 1931,
},
{
"Session Date": "2019-09-03",
Breakdown: "Email",
Visits: 1391,
},
];
return (
);
```
Compare the above visualization to the version below without stacking:
## Chart options
Using the `options` prop, you can configure a chart with options such as displaying a chart title, legend, data labels, or specifying the color list.
1. **Title:** the title of the chart.
2. **Legend:** lists the data categories with their corresponding color for readability.
3. **Data labels:** labels for data points.
4. **Tooltips:** displays details for data points on hover.
```jsx theme={null}
;
```
| Parameter | Type | Description |
| ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
| `title` | String | The title of the chart. |
| `showTooltips` | Boolean | When set to `true`, displays tooltips for data points on hover. Default is `false`. |
| `showLegend` | Boolean | When set to `true`, displays a legend above the table to help users understand the data. Default is `false`. |
| `showDataLabels` | Boolean | When set to `true`, displays labels above data points for readability. Default is `false`. |
| `colorList` | Array | An array of strings specifying the order that colors should be used in the chart. Learn more about [colors](#colors). |
## Colors
By default, HubSpot will apply colors to chart bars or lines using a default set of colors when `groupFieldByColor` is specified in axes `options`. You can customize these colors in two ways:
* To customize the order of colors selected by HubSpot, include the `colorList` field in the top-level `options` prop, then specify the colors you want to prioritize.
* To apply colors to specific values of the field specified in `groupFieldByColor`, include the `colors` field within the axes `options`.
For example, the chart below is configured to apply colors to data in the `Product` field using `groupFieldByColor`. The three colors (`darkGreen`, `blue`, `darkPurple`) will be applied first, then the standard color order will be applied to any additional bars.
```jsx theme={null}
const dailyInventorySample = [
{
Product: "Standalone product A",
Sales: 159,
},
{
Product: "Bundle A",
Sales: 53,
},
{
Product: "Bundle B",
Sales: 99,
},
];
return (
);
```
If instead you want to manually apply colors to specific field values, rather than have HubSpot assign colors in order, you could instead include the `colors` field in the axes `options`. In colors, you'll need to specify each value from the `groupFieldByColor` field to assign a color to.
```jsx theme={null}
const dailyInventorySample = [
{
Product: "Standalone product A",
Sales: 159,
},
{
Product: "Bundle A",
Sales: 53,
},
{
Product: "Bundle B",
Sales: 99,
},
];
return (
);
```
### Default color set
Below are the available colors in their default order.
| Color | Hex value | Swatch |
| ------------ | --------- | ------------------ |
| `orange` | #fea58e | |
| `aqua` | #51d3d9 | |
| `purple` | #bda9ea | |
| `yellow` | #f5c78e | |
| `pink` | #ea90b1 | |
| `blue` | #81c1fd | |
| `green` | #a4d398 | |
| `darkOrange` | #c3705c | |
| `darkAqua` | #009ca2 | |
| `darkPurple` | #8775b2 | |
| `darkYellow` | #bb915b | |
| `darkPink` | #b05c7d | |
| `darkBlue` | #468cc4 | |
| `darkGreen` | #6b9a5b | |
## Design guidelines
* **DO:** title your data categories with human-readable text so they are easy to understand.
* **DO:** use sentence-casing for the data categories and chart title (only first letter capitalized).
* **DO:** sort your data in ascending/descending order of your x-axis field to prevent unordered rendering prior to passing it to a charting component. If you intend to display information over time (such as in a LineChart), your data will be displayed in the order you provide it.
* **DO:** display the chart legend if you’re graphing more than one category of data. This prevents your users from having to rely only on color to identify different data on your chart.
* **DO:** for readability, use larger surfaces to showcase charts, such as the record page middle column. Avoid using charts with many data points on smaller surfaces such as the preview panel or sidebar.
* **DON’T:** use more than 14 data categories unless it cannot be avoided for your use case.
* **DON’T:** use the same colors to indicate different data categories.
# Select
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/select
Learn about the Select component for use in UI extensions.
The `Select` component renders a dropdown menu select field where a user can select a single value. A search bar will be automatically included when there are more than seven options.
1. **Label:** the label that describes the field's purpose.
2. **Value:** the field's selected value.
```jsx theme={null}
import { Select } from "@hubspot/ui-extensions";
const Extension = () => {
const [name, setName] = useState(null);
const [validationMessage, setValidationMessage] = useState("");
const [isValid, setIsValid] = useState(true);
const options = [
{ label: "Bill", value: 42 },
{ label: "Ted", value: 43 },
];
return (
);
};
```
## Props
| Prop | Type | Description |
| ------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `description` | String | Text that describes the field's purpose. |
| `error` | Boolean | When set to `true`, `validationMessage` is displayed as an error message if provided. The input will also render its error state to let the user know there's an error. If left `false` (default), `validationMessage` is displayed as a success message. |
| `label` | String | The text that displays above the input. |
| `name` | String | The input's unique identifier. |
| `onChange` | `(value: string) => void` | A callback function that is invoked when the value is committed. |
| `onInput` | `(value: string) => void` | A callback function that is called and passed the value every time the search field is edited by the user. Prefer updating state in `onChange` as it fires less frequently, and if you need to update state here, consider debouncing your function. |
| `options` | Array | The options to display in the dropdown menu. `label` will be used as the display text, and `value` should be the option's unique identifier, which is submitted with the form. |
| `readOnly` | Boolean | When set to `true`, users will not be able to fill the input field. Default is `false`. |
| `required` | Boolean | When set to `true`, displays a required field indicator. Default is `false`. |
| `tooltip` | String | The text that displays in a tooltip next to the label. |
| `validationMessage` | String | The text to display if the input has an error. |
| `value` | String \| number \| boolean | The value of the input. |
| `variant` | `input` (default) \| `transparent` | The visual style of the button |
## Variants
Using the variant prop, you can set the input to be one of two styles:
* `input` (default): a standard dropdown menu.
* `transparent`: a hyperlink dropdown menu.
## Usage examples
Use this type of field when there are a range of set options to choose from, such as:
* A list of products that can be purchased.
* A list of office locations to ship to.
* A list of delivery options for a vendor.
## Guidelines
* **DO:** make label and description text concise and clear.
* **DO:** indicate if a field is required.
* **DO:** include clear validation error messages so that users know how to fix errors.
* **DO:** include placeholder text to help users understand what's expected in the field.
* **DON'T:** use this component when you want users to be able to select multiple options. For multiple options, use the [MultiSelect component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/multi-select).
* **DON'T:** use placeholder text for critical information, as it will disappear once users begin to type. Critical information should be placed in the label and descriptive text, with additional context in the tooltip if needed.
## Related components
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
* [TextArea](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text-area)
# Spacer
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/spacer
Learn about the Spacer component for use in UI extensions.
Use the `Spacer` component to add vertical space between components. This component is intended for one-off use in situations where other layout components like [Flex](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex) don't fully provide the spacing you need. You can use this component in tandem with the [AutoGrid](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/autogrid), [Flex](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex), [Box](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/box), and [Inline](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/inline) components to [manage extension layout](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/manage-ui-extension-layout).
For consistent spacing between multiple components, use the [Flex](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex) component with the `direction="column"` and the `gap` props instead. Flex automatically applies uniform spacing between children, which makes layout easier to maintain.
```jsx wrap theme={null}
import { Button, Spacer, Text } from '@hubspot/ui-extensions';
hubspot.extend(() => );
const Extension = () => {
return (
<>
These buttons have medium spacing between them:ButtonButton
>
);
};
```
## Props
"extra-large" | "extra-small" | "large" | "medium" | "small">} description={<>The amount of vertical space to render.>} />
string>} description={<>Used by findByTestId() to locate this component in tests.>} />
## Distances
The `size` prop controls how much vertical space is rendered. Each size maps to a consistent spacing value from the HubSpot design system:
* `extra-small`: minimal spacing for tightly grouped elements.
* `small`: default spacing suitable for most use cases.
* `medium`: moderate spacing for separating related content sections.
* `large`: generous spacing for creating clear visual breaks.
* `extra-large`: maximum spacing for major section divisions.
## Spacer vs. Flex
For most layouts, you should use [Flex](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex) with `direction="column"` and a `gap` value instead of `Spacer`, as it provides uniform spacing between all children without needing to manually insert `Spacer` components.
```jsx wrap theme={null}
import { Flex, Text } from '@hubspot/ui-extensions';
const Extension = () => {
return (
First sectionSecond sectionThird section
);
};
```
Use `Spacer` for situations where you need non-uniform spacing or a one-off adjustment:
```jsx highlight={8} wrap theme={null}
import { Flex, Text, Spacer } from '@hubspot/ui-extensions';
const Extension = () => {
return (
First sectionSecond sectionFinal section with extra space above
);
};
```
## Guidelines
* **DO:** use `Spacer` for one-off vertical spacing adjustments where you need different spacing at a specific point.
* **DO:** use the smallest `Spacer` size that creates adequate visual separation.
* **DON'T:** use multiple `Spacer` components when `Flex` with `gap` would achieve the same result more cleanly.
* **DON'T:** use `Spacer` for horizontal spacing. Use `Flex` with `direction="row"` and `gap` instead.
* **DON'T:** use `Spacer` when a `Divider` would better communicate the separation between sections.
## Related components
* [AutoGrid](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/autogrid)
* [Flex](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/flex)
* [Box](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/box)
* [Divider](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/divider)
* [Inline](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/inline)
# Statistics
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/statistics
Learn about the Statistics component for use in UI extensions.
The `Statistics` component renders a visual spotlight of one or more data points. Includes the `StatisticsItem` and `StatisticsTrend` subcomponents.
1. `StatisticItem` **label:** the `statisticItem`'s label text.
2. `StatisticItem` **number:** the `statisticItem`'s primary number.
3. `StatisticTrend` **value:** the percentage trend value.
4. `StatisticTrend` **direction:** the direction if the trend arrow (up or down).
```jsx theme={null}
import { Statistics, StatisticsItem, StatisticsTrend } from "@hubspot/ui-extensions";
const Extension = () => {
return (
);
};
```
## Props
**`` props**
| Prop | Type | Description |
| -------- | ---------------- | -------------------------------------------------------- |
| `id` | String | The statistic item's unique identifier. |
| `label` | String | The item's label. |
| `number` | String \| number | The string to be displayed as the item's primary number. |
**`` props**
| Prop | Type | Description |
| ----------- | -------------------------------------- | -------------------------------------------- |
| `color` | `'red'` \| `'green'` | The color of the trend arrow. |
| `direction` | `'increase'` (default) \| `'decrease'` | The direction of the trend arrow. |
| `value` | String | The text to be displayed as the trend value. |
## Variants
In `StatisticsTrend` components, use the `direction` prop to describe whether the data is trend upwards or downwards.
* `increase`: for additions or positive progression for a given time period.
* `decrease`: for subtractions or negative progression for a given time period.
Note that the positive or negative movement of a given statistic is intended solely to represent the increase or decrease in numerical value. Be mindful of how these movements can communicate sentiment. For example, a decrease in support volume can be a net positive, which can be confusing when represented by a red, downward arrow.
## Usage examples
* Calling out the progress of quarterly sales for a company.
* Monitoring the amount of traffic and social media engagement that a contact has for the month.
## Guidelines
* **DO:** keep statistics labels short and concise.
* **DO:** place statistics components towards the top of a card when possible to enable users to more easily scan information without scrolling.
* **DON'T:** include more than three statistics components per card if possible.
* **DON'T:** use more than four statistics components side by side.
* **DON'T:** include sensitive data that you don't want all users to see.
## Related components
* [Table](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/table)
* [DescriptionList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/description-list)
* [CrmPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list)
# StatusTag | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/status-tag
Learn about the StatusTag component for use in UI extensions.
The `StatusTag` component renders a visual indicator to display the current status of an item. Status tags can be static or clickable for invoking functions with the `onClick` prop.
```jsx theme={null}
import { Flex, Heading, StatusTag, Text, hubspot } from "@hubspot/ui-extensions";
const Extension = () => {
return (
Account status
Billing: Good standing
Outreach: > 2 weeks since last check-in
Support: 1 escalated support ticket
Upgrades: No upgrades
Referrals: 1 recent referral
);
};
```
## Props
| Prop | Type | Description |
| ---------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `hollow` | Boolean | When set to `true`, the status tag's dot will be a ring instead of a filled-in circle. |
| `onClick` | `() => void;` | A function that will be invoked when the status tag is clicked. It receives no arguments and its return value is ignored. |
| `onRemoveClick` | `() => void;` | A function that will be invoked when the remove icon is clicked. |
| `showRemoveIcon` | Boolean | When set to `true`, the status tag will include a small, clickable x icon to remove it. Default is `false`. |
| `variant` | `'default'` (default) \| `'info'` \| `'danger'` \| `'warning'` \| `'success'` | The color of the dot indicator. See the [variants section](#variants) for more information. |
## Variants
* Using the `variant` prop, you can configure the indicator severity color:
* `'danger'`: a red dot indicating a negative state, such as error or failure.
* `'default'` (default): a grey dot indicating a neutral state.
* `'info'`: a blue dot indicating a general or informative state.
* `'success'`: a green dot indicating a positive state, such as confirming success or completion.
* `'warning'`: a yellow dot indicating a cautionary state, for when something needs attention or is time-sensitive.
* Using the `hollow` prop, you can configure the dot to be a filled circle or a ring:
* Using the `showRemoveIcon` prop, you can include a clickable icon to remove the status indicator.
## Usage examples
* Use an `'info'` status tag to indicate that a customer is active.
* Use a `'success'` status tag to indicate that an item in a to-do list has been completed.
* Use a `'warning'` tag to indicate that a deal is expiring soon.
* Use a `'danger'` tag to indicate that an error happened when trying to sync a specific property in a table.
## Guidelines
* **DO:** make tag text concise and clear.
* **DO:** ensure that tag variants are used consistently across the extension.
* **DON'T:** use tags in place of buttons or links.
* **DON'T:** rely on color alone to communicate the tag's meaning. Ensure that tag text is clear and helpful.
## Related components
* [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag)
* [Alert](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert)
* [Icon](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/icon)
* [ProgressBar](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/progress-bar)
# StepIndicator
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/step-indicator
Learn about the StepIndicator component for use in UI extensions.
The `StepIndicator` component renders an indicator to show the current step of a multi-step process.
```jsx theme={null}
import { Flex, Box, StepIndicator, Button } from "@hubspot/ui-extensions";
hubspot.extend(() => );
function Extension() {
const [currentStep, setCurrentStep] = useState(0);
return (
setCurrentStep(currentStep - 1)}>Previous setCurrentStep(currentStep + 1)}>Next
);
}
```
## Props
| Prop | Type | Description |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `circleSize` | `'xs'`, `'extra-small'`, \| `'sm'`, `'small'` (default) \| `'md'`, `'medium'` \| `'lg'`, `'large'` \| `'xl'`, `'extra-large'` | The size of the indicator circles. See the [variants section](#variants) for examples of sizing. |
| `currentStep` | Number | The currently active step. Steps are zero-based, meaning the first step is assigned `0`. |
| `direction` | `'horizontal'` (default) \| `'vertical'` | The orientation of the indicator. |
| `onClick` | `(stepIndex: number) => void` | A function that is invoked when a step in the indicator is clicked. The function receives the current step index as an argument (zero-based). Use this to update the currently active step. |
| `stepNames` | Array | An array containing the name of each step. |
| `variant` | `'flush'` \| `'default'` (default) \| `'compact'` | Sets component spacing.
`compact`: only shows the title of the currently active step.
`flush`: only shows the title of the currently active step and removes left and right margin.
|
## Variants
By default, the step indicator will be laid out horizontally, but you can use the `direction` prop to set the orientation to `vertical` instead.
In addition, you can set the size of the step circles using the `circleSize` prop, ranging from `'xs'`/`'extra-small'` to `'xl'`/`'extra-large'`.
## Related components
* [ProgressBar](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/progress-bar)
* [LoadingSpinner](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-spinner)
* [Toggle](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle)
# StepperInput
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/stepper-input
Learn about the StepperInput component for use in UI extensions.
The `StepperInput` component renders a number input field that can be increased or decreased by a set number.
This component inherits many of its props from the `NumberInput` component, with an additional set of props to control the increase/decrease interval.
```jsx theme={null}
import { StepperInput } from "@hubspot/ui-extensions";
return (
{
setCookieCount(value);
}}
/>
);
```
## Props
| Prop | Type | Description |
| ------------------------ | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultValue` | String | The default input value. |
| `description` | String | Text that describes the field's purpose. |
| `error` | Boolean | When set to `true`, `validationMessage` is displayed as an error message if provided. The input will also render its error state to let the user know there's an error. If left `false` (default), `validationMessage` is displayed as a success message. |
| `formatStyle` | `'decimal'` (default) \| `'percentage'` | Formats the number as a decimal or percentage. |
| `label` | String | The text that displays above the input. |
| `max` | Number | The highest number allowed in the input. |
| `maxValueReachedTooltip` | String | Text that will appear in a tooltip when the user has reached the maximum value. |
| `min` | Number | The lowest number allowed in the input. |
| `minValueReachedTooltip` | String | Text that will appear in a tooltip when the user has reached the minimum value. |
| `name` | String | The input's unique identifier. |
| `onBlur` | `(value: number) => void` | A function that is called and passes the value when the field loses focus. |
| `onChange` | `(value: number) => void` | A callback function that is called with the new value or values when the list is updated. |
| `onFocus` | `(value: number) => void` | A function that is called and passed the value when the field gets focused. |
| `placeholder` | String | Text that appears in the input when no value is set. |
| `precision` | Number | The number of digits to the right of the decimal point. |
| `readOnly` | Boolean | When set to `true`, the checkbox cannot be selected. Default is `false`. |
| `required` | Boolean | When set to `true`, displays a required field indicator. Default is `false`. |
| `stepSize` | Number | The amount that the current value will increase or decrease by. Default is `1`. |
| `tooltip` | String | The text that displays in a tooltip next to the label. |
| `validationMessage` | String | The text to display if the input has an error. |
| `value` | String | The value of the input. |
## Related components
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
* [DateInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/date-input)
* [NumberInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/number-input)
# Table | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/table
Learn about the Table component for use in UI extensions.
The `Table` component renders a table for displaying and organizing data.
Below, learn how to implement buttons in a UI extension. For guidance on table design, check out the [Table design patterns](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/tables).
To format the table, you can use the following subcomponents:
* `TableHead`: the header section of the table containing column labels.
* `TableRow`: individual table rows.
* `TableHeader`: cells containing bolded column labels.
* `TableBody`: container for the main table contents (rows and cells).
* `TableCell`: individual cells within the main body.
* `TableFooter`: a row at the bottom of the table, typically to summarize the columns.
```jsx theme={null}
import { Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from "@hubspot/ui-extensions";
const Extension = () => {
return (
` props**
| Prop | Type | Description |
| ----------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bordered` | Boolean | When set to `false`, the table will not include borders. Default is `true`. |
| `density` | `default` (default) \| `condensed` \| `compact` | Use `condensed` to reduce the padding in Table rows; use `compact` to display minimal padding for readability. |
| `flush` | Boolean | When set to `true`, the table will not include bottom margin. Default is `false`. |
| `paginated` | Boolean | When set to `true`, the table will include pagination navigation. Default is `false`.See the [paginated tables](#paginated-tables) section for pagination props. |
**`` props**
| Prop | Type | Description |
| --------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `align` | `'center'` \| `'left'` \| `'right'` | Sets the alignment of a table header. |
| `sortDirection` | `'none'` (default) \| `'ascending'` \| `'descending'` \| `'never'` | A visual indicator of the current direction in which the column is sorted. Does not modify the table data. See the [sortable tables](#sortable-tables) section for more sorting props. |
| `width` | Number \| `'min'` \| `'max'` \| `'auto'` | Sets the width of a table header.
`min`: the content will only be as wide as required, overflowing if the content is wider than the table. A horizontal scrollbar will appear when there is overflow.
`max`: the content will expand to occupy the maximum available width without overflowing.
`auto`: the content will adjust its width based on the available space without overflowing.
|
**`` props**
| Prop | Type | Description |
| --------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `align` | `'center'` \| `'left'` \| `'right'` | Sets the alignment of a table cell. |
| `colSpan` | Number | Sets the number of columns a cell should span. |
| `width` | Number \| `'min'` \| `'max'` \| `'auto'` | Sets the width of a table cell.
`min`: the content will only be as wide as required, overflowing if the content is wider than the table. A horizontal scrollbar will appear when there is overflow.
`max`: the content will expand to occupy the maximum available width without overflowing.
`auto`: the content will adjust its width based on the available space without overflowing.
|
## Paginated tables
To include paginated navigation below a table, set the `Table` prop `paginated` to `true`.
You'll then include the following props to further configure pagination:
| Prop | Type | Description |
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `maxVisiblePageButtons` | Number | The maximum number of page buttons to display. |
| `onPageChange` | (pagenumber: number) => void | A function that is invoked when the page pagination button is clicked. It receives the new page number as an argument. |
| `page` | Number | Denotes the current page number. |
| `pageCount` | Number | The total number of pages available. |
| `showButtonLabels` | Boolean | When set to `false`, hides the text labels for First/Prev/Next buttons. The button labels will still be accessible to screen readers. Default is `true`. |
| `showFirstLastButtons` | Boolean | When set to `true`, displays the First/Last page buttons. Default is `false`. |
## Sortable tables
To add sorting functionality to a table, you can include the `sortDirection` and `onSortChange` props in the table's `TableHeader` components. To enable table data to dynamically reorder based on user input, you'll need to store your table data in variables rather than hard coding it into table cells. Below is an example of a sortable table with a static table footer.
```jsx theme={null}
import React, { useState } from "react";
import { Table, TableHead, TableRow, TableHeader, TableBody, TableCell, TableFooter } from "@hubspot/ui-extensions";
import { hubspot } from "@hubspot/ui-extensions";
hubspot.extend(() => );
const ORIGINAL_DATA = [
{
name: "The Simpsons",
yearsOnAir: 28,
emmys: 31,
},
{
name: "M*A*S*H",
yearsOnAir: 11,
emmys: 14,
},
{
name: "Arrested Development",
yearsOnAir: 4,
emmys: 5,
},
];
// Initial sort state: all columns are sortable but unsorted
const DEFAULT_SORT_STATE = {
name: "none",
yearsOnAir: "none",
emmys: "none",
};
function Extension() {
const [data, setData] = useState(ORIGINAL_DATA);
const [sortState, setSortState] = useState({ ...DEFAULT_SORT_STATE });
function handleOnSort(fieldName, sortDirection) {
const dataClone = [...data];
dataClone.sort((entry1, entry2) => {
if (sortDirection === "ascending") {
return entry1[fieldName] < entry2[fieldName] ? -1 : 1;
}
return entry2[fieldName] < entry1[fieldName] ? -1 : 1;
});
setSortState({ ...DEFAULT_SORT_STATE, [fieldName]: sortDirection });
setData(dataClone);
}
return (
handleOnSort("name", sortDirection)}>
Series
handleOnSort("yearsOnAir", sortDirection)}>
Years on air
handleOnSort("emmys", sortDirection)}>
Emmys
{data.map(({ name, yearsOnAir, emmys }) => {
return (
{name}{yearsOnAir}{emmys}
);
})}
Totals4350
);
}
```
| Prop | Type | Description |
| --------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disabled` | Boolean | When set to `true`, users cannot change the sort ordering. It has no effect if `sortDirection` is set to `never` or undefined. Default is `false`. |
| `onSortChange` | `(value: "none" \| "ascending" \| "descending") => void` | A function that will be invoked when the header is clicked. It receives a `sortDirection` as an argument (cannot be none or a null value). |
| `sortDirection` | `'none'` (default) \| `'ascending'` \| `'descending'` | Visually indicates with an arrow which way the rows are sorted. |
## Usage examples
* A client list containing names, phone numbers, job positions, and email addresses that salespeople can use to prioritize outreach.
* A summary table of the deals closed last quarter.
## Guidelines
* **DO:** keep text within data cells clear and concise for easier scanning.
* **DO:** always include a table header row to label columns.
* **DO:** limit the use of links in table cells.
* **DON'T:** use multiple tables on one screen when possible.
* **DON'T:** render a blank table if there's no potential for no data to display, such as with search or filters. Instead, use the [EmptyState component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/empty-state) when there are no results to display.
## Related components
* [DescriptionList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/description-list)
* [Statistics](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/statistics)
* [CrmPropertyList](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/crm-property-list)
# Tabs | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tabs
Learn about the Tabs component for use in UI extensions.
The `Tabs` component allows you to group related content into clickable tabs, with each `Tab` child component creating a new tab. Options are provided for visual variants, tooltip configuration, and more.
```jsx theme={null}
import { Alert, Tabs, Tab } from "@hubspot/ui-extensions";
// `defaultSelected` sets the initial tab,
// user controls changing tabs via clicking
const Extension = () => {
return (
Your email address was successfully updated.
Tab 2's content
);
};
```
## Props
**`` props**
| Prop | Type | Description |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `defaultSelected` | String \| Number | The ID of the tab to display by default (as set by the `Tab`'s `tabId` prop). |
| `fill` | Boolean | Whether the tabs should fill the available space. |
| `onSelectedChange` | `(selectedId: string \| number) => void` | A function that gets invoked when the selected tab changes. |
| `selected` | String \| Number | The currently selected tab ID, for [controlling the component via React state](#controlling-tabs-via-react-state). |
| `variant` | `default` \| `enclosed` | Visual style of the tabs. See [variants](#variants) for more information. |
**`` props**
| Prop | Type | Description |
| ------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `disabled` | Boolean | Whether the tab should be disabled. When set to `false`, tab will be greyed out and not clickable. |
| `tabId` | String \| Number | The tab's unique identifier. |
| `title` | String | The tab's title text. |
| `tooltip` | String | The text that appears in a tooltip on hover. Learn more about [tooltips](#tooltips). |
| `tooltipPlacement` | `top` (default) \| `bottom` \| `left` \| `right` \| | Where the [tooltip](#tooltips) should appear, relative to the tab. |
Once there are enough tabs to exceed the width of the container, HubSpot will automatically put overflowing tabs into a *More* dropdown menu, which users can click to select from the remaining tabs.
## Variants
The `variant` prop provides two options for tab styling: `default` and `enclosed`.
```jsx highlight={2} theme={null}
Your email address was successfully updated.
Tab 2's content
```
```jsx highlight={2} theme={null}
Your email address was successfully updated.
Tab 2's content
```
You can also set the `fill` prop to `true` to configure the tabs to take up the full width of the container.
```jsx highlight={3} theme={null}
Your email address was successfully updated.
Tab 2's content
```
## Tooltips
Use the `tooltip` prop to add tooltips to tabs on hover. By default, the tooltip will appear above the tab, but you can use the `tooltipPlacement` prop to configure it further.
```jsx highlight={3} theme={null}
Your email address was successfully updated.
Tab 2's content
```
```jsx highlight={3-4} theme={null}
Your email address was successfully updated.
Tab 2's content
```
## Controlling tabs via React state
In addition to being able to switch tabs by clicking on each tab, you can also control tab select via React state using the `selected` and `onSelectedChange` props.
```jsx theme={null}
import React, { useState } from "react";
import { Tabs, Tab, Button, Text } from "@hubspot/ui-extensions";
// Uses `selected` and `onSelectedChange` props to handle updates
const BasicExtension = () => {
const [selected, setSelected] = useState("second");
return (
<>
1st tab content
2nd tab content3rd tab contentSelected: {selected} setSelected("third")}>Select third tab
>
);
};
```
# Tag
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag
Learn about the Tag component for use in UI extensions.
The `Tag` component renders a tag to label or categorize information or other components. Tags can be static or clickable for invoking functions.
1. **Variant:** the color of the tag.
2. **Tag text:** the text that communicates the tag's purpose.
```jsx theme={null}
import { Tag } from "@hubspot/ui-extensions";
const Extension = () => {
return (
{
console.log("Tag clicked!");
}}
inline={true}
>
Success
);
};
```
## Props
| Prop | Type | Description |
| --------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `onClick` | `() => void` | A function that will be invoked when the tag is clicked. The function receives no arguments and its return value is ignored. |
| `overlay` | Object | Include a [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal), [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel), or [Tooltip](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tooltip) component in this object to open it as an overlay on click or hover. Learn more about [using overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays). |
| `variant` | `'default'` (default) \| `'warning'` \| `'success'` \| `'error'` \| `'info'` | The color of the alert. See the [variants section](#variants) for more information. |
## Variants
Using the `variant` prop, you can choose from one of five tag colors:
* `default` (default): for general tagging and labeling.
* `success`: for indicating or confirming the success of an action.
* `warning`: for indicating something that might be time-sensitive or of importance.
* `error`: for indicating error or failure.
* `info`: for conveying general information.
## Alignment
Using the `inline` prop, you can set a tag to align side-by-side with surrounding text. Below are examples of inline tags next to single-line text and multi-line text.
```jsx theme={null}
Some text over here;
```
```jsx theme={null}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam ac rhoncus velit, non tincidunt tortor. Aliquam nec
ligula quis risus vehicula mattis id vitae orci. Suspendisse sed mattis metus, id iaculis enim.
;
```
```jsx theme={null}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam ac rhoncus velit, non tincidunt tortor. Aliquam nec
ligula quis risus vehicula mattis id vitae orci. Suspendisse sed mattis metus, id iaculis enim. Cras nisl erat,
pulvinar sit amet nisl sit amet, feugiat pharetra urna.
;
```
## Usage examples
* Use a `default` tag to indicate that a customer is active.
* Use a `success` tag to indicate that an item in a to-do list has been completed.
* Use a `warning` tag to indicate that a deal is expiring soon.
* Use an `error` tag to indicate that an error happened when trying to sync a specific property in a table.
## Guidelines
* **DO:** make tag text concise and clear.
* **DO:** ensure that tag variants are used consistently across the extension.
* **DON'T:** use tags in place of buttons or links.
* **DON'T:** rely on color alone to communicate the tag's meaning. Ensure that tag text is clear and helpful.
## Related components
* [Toggle](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle)
* [Alert](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert)
* [ProgressBar](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/progress-bar)
# Text
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text
Learn about the Text component for use in UI extensions.
The `Text` component renders text with formatting options.
```jsx theme={null}
import { Text } from "@hubspot/ui-extensions";
const Extension = () => {
return (
<>
Truncated textPlain textBoldItalicsBold and Italic textStrikethrough Text
Microcopy text
with inner bold
>
);
};
```
## Props
| Prop | Type | Description |
| ---------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `format` | Object | Text formatting options, which include:
`{ fontWeight: 'bold' }`
`{ fontWeight: 'demibold' }`
`{ italic: true }`
`{ lineDecoration: 'strikethrough' }`
`{ lineDecoration: 'underline' }`
`{ textTransform: '...' }` - Controls text capitalization
See the [variants section](#variants) for more information. |
| `inline` | Boolean | When set to `true`, will insert text without breaking the line. Default is `false`. |
| `truncate` | Boolean \| object | Truncates long strings to a single line. If the full string doesn't fit on one line, the excess text will display in a tooltip on hover.
`false` (default): text is not truncated.
`true`: truncates text to a single line. Full text will display in a tooltip on hover.
Alternatively, set this prop to one of the following objects to specify truncate options:
`{tooltipText: 'string'}`: truncates the string and sets the contents of the tooltip.
`{maxWidth: number}`: sets the width of the line in pixels.
|
| `variant` | `'bodytext'` (default) \| `'microcopy'` | The style of text to display. See the [variants section](#variants) for more information. |
## Variants
Using the `format` prop, you can style text with a number of options:
* `{fontWeight: 'bold'}`: sets the text to bold.
* `{fontWeight: 'demibold'}`: sets the text to a lighter bold.
* `{italic: true}`: sets the text to italics.
* `{lineDecoration: 'strikethrough'}`: adds a strikethrough to the text.
* `{lineDecoration: 'underline'}`: underlines the text.
* `{textTransform: 'none'}`: no capitalization changes (default)
* `{textTransform: 'uppercase'}`: transforms all characters to uppercase
* `{textTransform: 'lowercase'}`: transforms all characters to lowercase
* `{textTransform: 'capitalize'}`: capitalizes the first letter of each word
* `{textTransform: 'sentenceCase'}`: capitalizes the first letter of the text and makes the rest lowercase
* ``: enables you to set text styling within the same line by adding more text without breaking the line if possible. Line will still break when text content would extend past the boundaries of its container. For example: `Text with inner bold.`.
You can also control text size with the `variant` prop.
* `variant="bodytext"` (default)
* `variant="microcopy"`
## Usage examples
* Use body text when you want to display a summary of the last call with a contact.
* Use microcopy to include an explanation of a displayed status on a contact record.
## Guidelines
* **DO:** use text with clear messaging.
* **DO:** use text formatting thoughtfully. For example, don't bold all of the text that can be seen. Instead, only bold key words and phrases for easier scanning.
* **DON'T:** use the text component for the primary textual information on a card. Instead, consider using the [Heading component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/heading).
* **DON'T:** use underline formatting for text that's next to a hyperlink, as it will also look clickable.
* **DON'T:** use microcopy for important or critical information. Instead, consider whether an [Alert component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) would fit better.
* **DON'T:** use text components in place of headers, alerts, and errors.
## Related components
* [Heading](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/heading)
* [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link)
* [List](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/list)
# TextArea
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text-area
Learn about the TextArea component for use in UI extensions.
The `TextArea` component renders a fillable text field. Includes props to customize the size of the field along with maximum number of characters and resizability.
1. **Label:** the field's label.
2. **Description:** the text that describes the field's purpose.
3. **Value:** an entered value.
4. **Required field indicator:** communicates to the user that the field is required for form submission.
5. **Tooltip:** on hover, displays additional information about the field.
```jsx theme={null}
import { TextArea } from "@hubspot/ui-extensions";
import { useState } from "react";
const Extension = () => {
const [description, setDescription] = useState("");
const [validationMessage, setValidationMessage] = useState("");
const [isValid, setIsValid] = useState(true);
return (
);
};
```
## Props
| Prop | Type | Description |
| ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cols` | Number | The visible width of the text field in average character widths. |
| `description` | String | Text that describes the field's purpose. |
| `error` | Boolean | When set to `true`, `validationMessage` is displayed as an error message if provided. The input will also render its error state to let the user know there's an error. If left `false` (default), `validationMessage` is displayed as a success message. |
| `label` | String | The text that displays above the input. |
| `maxLength` | Number | The maximum number of characters (UTF-16 code units) that the user can enter. If not specified, maximum length is unlimited. |
| `name` | String | The input's unique identifier. |
| `onBlur` | (value: number) => void | A function that is called and passes the value when the field loses focus. |
| `onChange` | (value: number) => void | A callback function that is called with the new value or values when the list is updated. |
| `onFocus` | (value: number) => void | A function that is called and passed the value when the field gets focused. |
| `onInput` | (value: number) => void | A function that is called and passes the value when the field is edited by the user. Should be used for validation. It's recommended that you don't use this value to update state (use `onChange`instead). |
| `placeholder` | String | Text that appears in the input when no value is set. |
| `readOnly` | Boolean | When set to `true`, the checkbox cannot be selected. Default is `false`. |
| `required` | Boolean | When set to `true`, displays a required field indicator. Default is `false`. |
| `resize` | `'vertical'` \| `'horizontal'` \| `'both'` (default) \| `'none'` | Sets whether the text field is resizable, and if so, in which directions. |
| `rows` | Number | The number of visible text lines for the text field. |
| `tooltip` | String | The text that displays in a tooltip next to the label. |
| `validationMessage` | String | The text to display if the input has an error. |
| `value` | String | The value of the input. |
## Usage example
A field where salespeople can leave comments after meeting a new client.
## Guidelines
* **DO:** make label and description text concise and clear.
* **DO:** indicate if a field is required.
* **DO:** include clear validation error messages so that users know how to fix errors.
* **DO:** include placeholder text to help users understand what's expected in the field.
* **DO:** indicate if there is a character limit.
* **DON'T:** use this field for short values, such as names, numbers, and dates.
* **DON'T:** use placeholder text for critical information, as it will disappear once users begin to type. Critical information should be placed in the label and descriptive text, with additional context in the tooltip if needed.
## Related components
* [Text](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/text)
* [Select](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/select)
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
# Tile
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tile
Learn about the Tile component for use in UI extensions.
The `Tile` component renders a square tile that can contain other components. Use this component to create groups of related components.
```jsx theme={null}
import { Tile, Text } from "@hubspot/ui-extensions";
const Extension = () => {
return (
<>
This is the default tile. It has a small amount of left paddingThis is a compact tile. It reduces the amount of padding within.This is a flush tile. It has no left padding
>
);
};
```
## Props
| Prop | Type | Description |
| --------- | ------- | ---------------------------------------------------------------------------------------------- |
| `compact` | Boolean | When set to `true`, reduces the amount of padding in the tile. Default is `false`. |
| `flush` | Boolean | When set to `true`, removes left and right padding from the tile contents. Default is `false`. |
## Variants
Using the `flush` prop, you can remove left and right padding from the tile contents.
* `flush={false}` (default)
* `flush={true}`
## Usage examples
* Group a form and its inputs together.
* Group a bulleted text summary and statistics components together.
## Related components
* [Box](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/box)
* [Divider](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/divider)
* [Accordion](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/accordion)
# TimeInput | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/time-input
Learn about the TimeInput component for use in UI extensions.
The `TimeInput` component renders an input field where a user can select a time of day. Includes options for customizing the selectable time range, timezone display, and more.
Below is an example of using both `TimeInput` and `DateInput` together to allow users to schedule appoints by date and time. Note the `setValidAppointmentTimes` logic to set the available time ranges based on the selected date.
```jsx theme={null}
import React, { useState } from "react";
import { Button, DateInput, Flex, Form, Heading, TimeInput, hubspot } from "@hubspot/ui-extensions";
hubspot.extend(() => );
function Extension() {
const [dateRange] = useState({
start: { year: 2025, month: 6, date: 7 },
end: { year: 2025, month: 6, date: 11 },
});
const [timeRange, setTimeRange] = useState({
start: { hours: 7, minutes: 15 },
end: { hours: 16, minutes: 0 },
});
const [selectedDate, setSelectedDate] = useState({ year: 2025, month: 6, date: 9 });
const [selectedTime, setSelectedTime] = useState({ hours: 10, minutes: 30 });
const areDatesEqual = (firstDate, secondDate) => {
return (
firstDate.date === secondDate.date && firstDate.month === secondDate.month && firstDate.year === secondDate.year
);
};
const setValidAppointmentTimes = date => {
console.log({ date });
console.log({ dateRangeStart: dateRange.start });
// Rudimentary "don't allow early on Monday and late on Friday" logic
if (areDatesEqual(date, dateRange.start)) {
console.log("selected start of date range");
setTimeRange({ start: { hours: 12, minutes: 0 }, end: { hours: 16, minutes: 30 } });
} else if (areDatesEqual(date, dateRange.end)) {
console.log("selected end of date range");
setTimeRange({ start: { hours: 8, minutes: 15 }, end: { hours: 12, minutes: 0 } });
} else {
console.log("selected another date");
setTimeRange({ start: { hours: 8, minutes: 15 }, end: { hours: 16, minutes: 30 } });
}
};
return (
<>
>
);
}
```
## Props
| Prop | Type | Description |
| ------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultValue` | Object | The default time value. Uses the same format as the `value` field. |
| `description` | String | Text that describes the field's purpose. |
| `error` | Boolean | When set to `true`, `validationMessage` is displayed as an error message if provided. The input will also render its error state to let the user know there's an error. If left `false` (default), `validationMessage` is displayed as a success message. |
| `interval` | Number | Sets the interval (in minutes) between the dropdown options. |
| `label` | String | The text that displays above the input. |
| `max` | Object | A Sets the latest valid time available. |
| `min` | Object | Sets the earliest valid time available. |
| `name` | String | The input's unique identifier. |
| `onBlur` | `(value: TimeInputEventsPayload) => void` | A function that is called and passes the value when the field loses focus. |
| `onChange` | `(checked: boolean, value: string) => void` | A callback function that is invoked when the value is committed. Currently, this occurs on `onBlur` of the input and when the user submits the form. |
| `onFocus` | `(value: TimeInputEventsPayload) => void` | A function that is called and passed the value when the field gets focused. |
| `readOnly` | Boolean | When set to `true`, the input cannot be edited. |
| `required` | Boolean | When set to `true`, displays a required field indicator. |
| `timezone` | `'userTz'` (default) \| `'portalTz'` | Sets the timezone that the component will use to calculate valid times.
`userTz` (default): the user's time zone.
`portalTz`: the account's default time zone.
|
| `tooltip` | String | The text that displays in a tooltip next to the label. |
| `validationMessage` | String | The text to display if the input has an error. |
| `value` | Object | The value of the input. |
## Related components
* [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/form)
* [DateInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/date-input)
* [NumberInput](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/number-input)
# Toggle
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle
Learn about the Toggle component for use in UI extensions.
The `Toggle` component renders a boolean toggle switch that can be configured with sizing, label position, read-only, and more.
```jsx theme={null}
import { Toggle } from "@hubspot/ui-extensions";
const Extension = () => {
return ;
};
```
## Props
| Prop | Type | Description |
| ------------------ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `checked` | Boolean | Whether the toggle is selected. Default is `false`. |
| `initialIsChecked` | Boolean | When set to `true`, the toggle will be selected by default. Sets the default `checked` state when the component is uncontrolled. |
| `label` | String | The text that displays above the input. |
| `labelDisplay` | `'inline'` (default) \| `'top'` \| `'hidden'` | The display option for the toggle label. |
| `name` | String | The input's unique identifier. |
| `onChange` | `(checked: boolean) => void` | A function that is invoked when the toggle is clicked. |
| `readonly` | Boolean | When set to `true`, users will not be able to select the toggle. Default is `false`. |
| `size` | `'xs'` \|`'sm'` \| `'md'` (default) | The size of the toggle. Only `'md'` sized toggles can display text on the toggle (ON, OFF). All other sizes will hide checked/unchecked text. |
| `textChecked` | String | The text that displays on the toggle when checked. Default is ON. Extra small and small toggles will not display any text. |
| `textUnchecked` | String | The text that displays on the toggle when not checked. Default is *OFF*. Extra small and small toggles will not display any text. |
## Variants
Using the `labelDisplay` and `size` props, you can customize toggle appearance.
* `labelDisplay`: set to `'inline'` or `'top'` to configure the label position, or set to `'hidden'` to hide the label.
* `size`: by default, toggles are set to `'md'`. Shrink the toggle size by setting this prop to `'xs'` or `'sm'`. Note that only medium toggles will display ON/OFF status text.
## Related components
* [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag)
* [ToggleGroup](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle-group)
* [ProgressBar](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/progress-bar)
# ToggleGroup
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle-group
Learn about the ToggleGroup component for use in UI extensions.
The `ToggleGroup` component renders a list of selectable options, either in radio button or checkbox form.
1. **Group label:** the text that displays above the group of checkboxes.
2. **Tooltip:** on hover, displays additional information about the field.
3. **Unchecked checkbox:** an unselected checkbox.
4. **Option label:** the text that displays next to the checkbox.
5. **Option description:** the text that displays below the option label to describe the option.
```jsx theme={null}
import { ToggleGroup } from "@hubspot/ui-extensions";
const options = [1, 2, 3, 4].map(n => ({
label: `Option ${n}`,
value: `${n}`,
initialIsChecked: n === 2,
readonly: false,
description: `This is option ${n}`,
}));
const Extension = () => {
return (
);
};
```
## Props
| Prop | Type | Description |
| ------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `error` | Boolean | When set to true, `validationMessage` is displayed as an error message if provided. The input will also render its error state to let the user know there is an error. If left `false`, `validationMessage` is displayed as a success message. |
| `inline` | Boolean | When set to `true`, stacks the options horizontally. Default is `false`. |
| `label` | String | The text that displays above the toggles. |
| `name` | String | The input's unique identifier. |
| `onChange` | (checked: boolean) => void | A function that is invoked when the toggle is clicked. |
| `options` | Array | An array of options to display in the group. Each object in the array contains:
`label` (string)
`value` (string)
`initialIsChecked` (boolean)
`readonly` (boolean)
`description` (string)
|
| `readonly` | Boolean | When set to `true`, users will not be able to select the toggle. Default is `false`. |
| `required` | Boolean | When set to `true`, displays a required indicator next to the toggle group. Default is `false`. |
| `toggleType` | `'radioButtonList'` \| `'checkboxList'` (default) | The type of toggle, whether checkboxes or radio buttons. Radio buttons only allow one option to be selected. |
| `tooltip` | String | Text that will appear in a tooltip next to the toggle group label. |
| `validationMessage` | String | The text to display if the input has an error. |
| `value` | String | The value of the toggle group.
Accepts a string when `toggleType` is `radioButtonList`.
Accepts an array when `toggleType` is `checkboxList`.
|
| `variant` | `'default'` (default) \| `'small'` | The size of the toggle. |
## Variants
By default, the toggle group will render as a vertical list of checkboxes. Using the `toggleType` prop, you can set the options to display as checkboxes or radio buttons. You can also use the `inline` prop to stack options horizontally.
`toggleType='checkboxList'` (default)
`toggleType='radioButtonList'`
`inline={true}`
## Usage examples
* A radio button list to enable salespeople to select one of four sales packages for a new customer.
* A checkbox list to enable customer support reps to select several options of swag to send to a delightful customer.
## Guidelines
* **DO:** use this component when the user has a small selection of items to choose from. For longer lists of options, consider using the [Select component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/select) instead.
* **DO:** keep label options concise when possible.
* **DON'T:** use toggle groups to display long lists of options.
## Related components
* [Checkbox](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/checkbox)
* [RadioButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/radio-button)
* [Toggle](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/toggle)
# Tooltip | UI components
Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tooltip
Learn about the Tooltip component for use in UI extensions.
The `Tooltip` component renders a tooltip when hovering over other UI components to provide users with additional context. This component can be inserted via the `overlay` prop in [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button), [Image](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/image), [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link), [LoadingButton](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/loading-button), and [Tag](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag) components.
```jsx theme={null}
import { Button, Tooltip } from "@hubspot/ui-extensions";
function ToolTipExample() {
return This is a tooltip}>Hover me;
}
```
## Props
| Prop | Type | Description |
| ----------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `placement` | `'top'` (default) \| `'bottom'` \| `'left'` \| `'right'` | The position where the tooltip will be displayed, relative to the parent UI component. |
## Placement
Using the `placement` prop, you can set where the tooltip will appear relative to the parent UI component (default is `top`). Below is an example of each `placement` value.
```jsx theme={null}
Tooltip on an Image component}
/>;
```
```jsx theme={null}
This is a tooltip with a link in it
}
>
Hover this link
;
```
```jsx theme={null}
Tooltip on a LoadingButton} variant="primary">
Hover loading button
;
```
```jsx theme={null}
Tooltip on a Tag component} format={{ variant: "success" }}>
Hover this tag
;
```
## Related components
* [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel)
* [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal)
* [Icon](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/icon)
# Agent tool listing requirements
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/agent-tool-listing-requirements
Review the HubSpot Marketplace requirements for listing an app that includes agent tools.
Below, learn about the additional requirements for listing an app with agent tools on the HubSpot Marketplace, as well as how to [submit it for approval](#submitting-tools-for-approval).
## Listing requirements
All apps built for the HubSpot Marketplace are subject to the [listing requirements for apps](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements), and certified apps are subject to the additional [certification requirements](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements). If you're adding an agent tool to an app intended for or already listed on the HubSpot Marketplace, you'll need to adhere to some additional criteria.
### Security & Privacy
* **Scopes:** if an agent tool accepts a HubSpot object's property values or metadata as inputs, the corresponding `.read` scope must be added to your [app's required scopes configuration](/docs/apps/developer-platform/build-apps/app-configuration#app-schema). For example, if a tool accepts deal data, the app must require the `crm.objects.deals.read` scope.
* **Inputs:** tools must only collect inputs and context that are necessary for their outputs. For example:
* A "Summarize contact" tool may consider inputs like contact properties and data from associated meetings and tasks.
* A "Create quote" tool should not require context on a contact's meeting history.
* **LLM-specific vulnerabilities:** your app will be assessed for security best practices related to LLM-specific risks (e.g. indirect prompt injection or jailbreaks).
* **Sensitive data:** at this time, your app must not access, request, or use [sensitive data scopes](/docs/api-reference/latest/crm/properties/sensitive-data).
### Compliance
Agent tools must not support any "Unacceptable risk" or "High risk" systems or use cases as described by the [EU AI Act](https://digital-strategy.ec.europa.eu/en/policies/regulatory-framework-ai).
For example:
* **Acceptable:** "Summarize contact" or "Create quote" tools
* **Unacceptable:** "Social scoring," "Employment decision," or "Parental leave predictor" tools
### Reliability & Performance
* **Tool type:** `toolType` should match the descriptions in the [agent tool reference documentation](/docs/apps/developer-platform/add-features/agent-tools/reference#agent-tool-definition):
* `GET_DATA`: retrieves information from HubSpot or external sources.
* `GENERATE`: generates content, summaries, analyses, or suggestions based on the provided inputs.
* `TAKE_ACTION`: performs CRM actions, such as creating notes or assigning tasks to owners.
* **LLM description:** `llmConfig.actionDescription` should follow the [description best practices](/docs/apps/developer-platform/add-features/agent-tools/reference#writing-effective-tool-descriptions).
* **Outputs:** your tools must return the outputs specified in their configurations. For example, if your tool's `outputFields` array includes a `success_message` of `"type": "string"`, the tool should return that property in the expected format with every run.
* **Performance matches descriptions:** your tools must perform operations successfully and as described in its `actionDescription`, `llmConfig.actionDescription`, and input and output descriptions.
* **Testing video:** you must provide a short video demoing successful agent runs using your tools (see the [Submitting your tools for approval](#submitting-your-tools-for-approval) section below).
* **HubSpot testing:** your tools must pass any user acceptance tests the HubSpot Ecosystem Quality team performs.
### Usability
* **Natural language:** all tool names, labels, descriptions, inputs, and outputs must use natural language and be clear and intuitive.
* Your tools must perform a clearly defined operation and be consistent with the specified outputs.
* **Organization names:** tool names may cite the external platform they interact with. However, they should not include an attribution to your organization in the tool name. Instead, you may use the tool description. Labeling and association will be handled by the HubSpot UI. For example:
* **Acceptable:** "Send Slack notification" or "Create Acme quote"
* **Unacceptable:** "Send Slack notification by Alpha Co." or "Create Acme quote by Omega Inc"
* **Tool names** should:
* Begin with a verb (e.g. "Create…")
* Start with a capital letter
* Be between 3-7 words
* **Tool names** should not include:
* Capitalized words except for the first word, proper nouns, and acronyms
* Underscores or camel case
* Jargon or hyperbole
* Generic terminology (e.g. "Process data")
* Specific agent names or use cases
* **Tool descriptions** should:
* Be between 50-150 characters
* Provide more detail than the name
* Be written in present tense
* Be clear about whether the tool generates a generic output or something specific in HubSpot (e.g., "This tool generates text for a landing page" or "This tool creates a HubSpot landing page draft")
* **Inputs:** every tool input should have a label and description. Each description should:
* Relate to the input's label
* Match the input's type
* Explain what to provide
* Be concise and accurately reflect the input's purpose
### Value
At this time, tools should empower front office use cases (e.g., marketing, sales, and customer service). For example:
* **Acceptable:** "Create Xero invoice" and "Send Omega gift" tools
* **Unacceptable:** a "New hire HubSpot setup" tool
## Submitting your tools for approval
To submit your tools for approval, fill out [this form](https://96it.share.hsforms.com/2tUHLydhdTM-mIskDvqP1eg) with the information below:
* **Production app ID:** identify the app ID you intend to update or list on the HubSpot Marketplace.
* **App ID and build for review:**
* **Recommended:** identify your production app ID and the latest successful build with agent tools.
* If your app is already listed on the HubSpot Marketplace, this build will have failed to deploy.
* **Alternative:** identify your staging app ID and latest deployed build.
* **Tool UIDs for review:** list the `uid` values for all agent tools.
* **Testing video:** record a short video describing the user acceptance tests you completed with this tool build. The demo video must meet the following requirements:
* Be at least three minutes long
* Include audio (preferred) or text descriptions
* Include descriptions of the tool(s) and common use cases
* Show the test runs you completed
* **Recommended:** if submitting your production app ID for review, this must be recorded while [developing locally](/docs/getting-started/quickstart).
After receiving the above information, the HubSpot Ecosystem Quality team will review your tools and share initial feedback within 10 business days. Please action this feedback as soon as you receive it, as a few reviews may be necessary for final approval.
Once approved, you will be able to deploy agent tools to your specified production app. You may then wish to [update your app listing](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app#edit-a-live-app-listing) to reflect the new feature.
# Applying for app certification
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification
App certification lets users know that your app was reviewed and approved by HubSpot, building trust and assuring prospective users of its quality.
### What is app certification?
App certification involves the HubSpot Ecosystem Quality team reviewing and confirming that your listed app meets [these requirements](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements) for security, privacy, reliability, performance, usability, accessibility, and value. Once approved, your app listing page will show a “HubSpot Certified App” badge.
### Why does it matter?
A certified badge on your app listing lets existing and prospective customers know the HubSpot Ecosystem Quality team reviewed and approved your app. It’s ultimately a way to symbolize quality and build trust with app users. Learn more about how to build customer trust through app certification [here](https://developers.hubspot.com/blog/how-to-build-customer-trust-through-certification).
### How does it work?
Any [eligible](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements) technology partner can apply for certification through their app developer account. The HubSpot Ecosystem Quality team will then review your submission and contact you to provide feedback or confirm your app’s certification.
### Is my app eligible for certification?
Make sure your app is eligible for certification by reviewing our [certification requirements](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements).You will not be able to apply unless your app has been listed for at least 6 months and has at least 60 active installs and the needed amount of API traffic. Active installs are the number of unique HubSpot production accounts, unaffiliated with your organization, showing successful app activity in the last 30 days.
### How do I apply for certification?
You can only submit one app at a time for certification. If you submit more than one app for certification at the same time, they will be rejected based on the order of submission. Once your app is certified, you can then submit another for certification.
To apply for certification:
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, click **App Listings**.
* Hover over the app you’d like to certify and click **More**. Then, select **Certify app**.
* In the dialog box, enter the following:
* **Demo video YouTube URL:** enter the **URL** for the demo video. Review the necessary requirements for the app demo video [below](#app-demo).
* **Testing Credentials:** provide testing credentials for your app so the HubSpot Ecosystem Quality team can evaluate its functionality; testing is subject to review at the team's discretion.
* At the bottom, click **Submit certification application**.
### Requirements for the app demo video
**Please note:**
Demo videos help the HubSpot Ecosystem Quality team test your app. The team will not review your app unless you submit a demo video that meets all requirements. Promotional sales and marketing videos will be rejected. HubSpot will not share or publish your demo videos.
The demo video must meet the following requirements:
* Be at least three minutes long.
* Include audio (preferred) or text descriptions.
* Include descriptions of your app's purpose and common use cases.
* E.g. "Acme App helps sales and onboarding reps coordinate across CRMs. Closed won deals in any of Acme's supported CRMs can automatically generate tickets and onboarding tasks in HubSpot."
* Demonstrate and describe how new users should:
* Install your app.
* E.g. " From the Acme App listing on HubSpot Marketplace, click Install app, select your CRM, enter your credentials, click Done, select your HubSpot account, review the requested scopes, click Connect app."
* Set up or configure your app after installation.
* E.g. "Once the app is installed, select a ticket pipeline by navigating to Settings > Integrations > Connected Apps > Acme App > Ticket Pipeline. Then, configure up to 10 default tasks in the 'Task Templates' section. When ready to enable the sync, toggle 'Ticket Sync' on."
* Use your app's primary features to support common use cases.
* E.g. "For each closed won deal in the connected CRM, the Ticket Sync feature will create a ticket record in HubSpot with any associated contacts and your configured tasks. This enables your onboarding team to immediately connect with the new customer."
* Interact with your app inside their HubSpot account to support common use cases (if applicable).
* E.g. "To create onboarding tickets from HubSpot deals, use the "Create Acme Ticket" custom workflow action in deal-based workflows. These actions allow for more customization than the app settings for other CRMs."
* Disconnect your app from their HubSpot account.
* Uninstall your app from their HubSpot account, describing how this affects users' HubSpot accounts and data.
**Tip:** [Loom](https://www.loom.com/)is a free tool you can use to record a demo video.
### Why is the certification CTA not appearing for my app?
The “Certify app” button will only appear if your app is eligible to apply. Please review our [certification requirements](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements) or reach out to your [Technology Partner Manager](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements#related-resources) if you have any questions about eligibility.
### How will users know my app is certified?
Once certified, your HubSpot Marketplace listing will show a prominent “HubSpot Certified App” badge.
When a customer hovers over the badge, they will see additional information on how apps are certified.
Find your **Technology Partner Manager** and their email information by logging into your developer account and navigating to **Development**. In the left sidebar menu, click **App Listings**. Hover over your app, then click the **More** dropdown menu and select **View listing details**.
***
#### Related docs
# HubSpot Marketplace | HubSpot Marketplace certification requirements
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements
Here's what Technology Partners need to get their app certified in HubSpot's Marketplace.
App certification involves the HubSpot Ecosystem Quality team reviewing and confirming that your [listed app](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements) meets the requirements below for security, privacy, reliability, performance, usability, accessibility, and value.
Certified apps stand out in the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps) with a reputation for quality and trustworthiness. Your app will also earn [special benefits](#certification-benefits) and receive constructive feedback from the HubSpot Ecosystem Quality team during app certification review.
**Please note:**
* These requirements are subject to change, as HubSpot is continuously making improvements to the HubSpot Marketplace and Ecosystem. HubSpot can reject an app certification request at their discretion if it doesn't meet the set standards.
* HubSpot will not review your app unless you submit a demo video as instructed when [applying for app certification](/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification).
* You can only submit one app at a time for certification. If you submit more than one app for certification at the same time, they will be rejected based on the order of submission.
* App certifications are valid on a rolling basis for two years, and must be renewed. Failure to renew your certification will result in the removal of your certified status.
* A notification will be sent 3 months before your app’s certification renewal date.
* If your app no longer meets the certification standards, the HubSpot Ecosystem Quality team will collaborate with you for up to 60 days to resolve concerns.
## Overview
Below is an overview of app certification requirements. For more detail, see the [review criteria section](#review-criteria).
Your app must:
* Be associated with a single HubSpot app ID.
* Your listed public app must be unique. If you have already listed an app and want to replace it, you should update the existing app instead of listing a new one.
* Do not create multiple apps that solve for the same use case. Apps with similar functionality and use the same APIs should be consolidated into a single app.
* Use [OAuth authentication](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth) and all scopes it requires.
* Be associated with a [verified domain](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#verified-domains).
* Public assets associated with your app must abide by security best practices.
Your app cannot use [classic CRM cards](/docs/api-reference/latest/crm/extensions/crm-cards/guide), as they are no longer supported as of June 16, 2025. Learn more about this announcement on the [HubSpot Developer Changelog](https://community.hubspot.com/t5/Developer-Announcements/Deprecating-Support-for-Classic-Legacy-CRM-Cards/m-p/1153039#M1048).
See the [detailed list of security and privacy requirements](#security-privacy).
App activity is defined by OAuth-authenticated requests to HubSpot's [APIs](/docs/api-reference/latest/overview) and [signed requests](/docs/apps/developer-platform/build-apps/authentication/request-validation) from HubSpot [webhook subscriptions](/docs/api-reference/latest/webhooks) and extensions (e.g. [app card](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data) data fetch requests).
Active installs are the number of unique HubSpot production accounts, unaffiliated with your organization, showing successful **app activity** within the past 30 days.
## Benefits of earning certification
In addition to the [benefits of listing your app](https://www.hubspot.com/partners/technology), certified apps receive:
* A "HubSpot Certified App" badge displayed on its HubSpot Marketplace listing.
* More prominent visibility in the HubSpot Marketplace:
* Inclusion in the "HubSpot Certified App" search filter.
* Eligibility for inclusion in curated HubSpot Marketplace collections.
* Access to the "HubSpot Certified App" badge and social media images to share the app's certification achievement.
* Favorable consideration in HubSpot's partnership and amplification initiatives.
## Review criteria
To earn certification, your app must demonstrate quality by meeting quantitative measures and qualitative descriptors of security, privacy, reliability, performance, usability, accessibility, and value. The requirements below are organized by these categories and include examples of constructive feedback you may receive.
### Security & Privacy
In order to be certified or recertified, your app must:
* Be associated with a single HubSpot app ID. Your app must authorize API requests with the public HubSpot app ID (and [OAuth client ID](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth)) associated with your app listing.
* A listing must not redirect to a different public or private app.
* Your listed public app must not require another public or private app to function.
* Be authenticated by the [OAuth authorization code flow](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth)
* Asking users to copy and paste OAuth codes or tokens is prohibited. Users should only be asked to grant access
* Apps must request, manage, and refresh access tokens without user involvement
* Use all [scopes](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth) it requests for installation (i.e. both in the required `scope` parameter and the `optional_scope` parameter).
* Extraneous scopes must be removed.
* If certain scopes only apply to a subset of your app's user base, they should be included as [conditionally required or optional scopes](/docs/apps/developer-platform/build-apps/authentication/scopes#app-scope-types).
* Be associated with a [verified domain](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#verified-domains).
* Pass an assessment for security best practices related to outdated software and various web server vulnerabilities and findings.
You must also complete a security questionnaire addressing questions of encryption, access controls, and token lifecycle management relating to OAuth token access.
#### Feedback example
> Your app currently requires four scopes: `contacts`, `timeline`, `forms`, and `content`. According to our logs, however, it only made requests to the CRM Contacts and Timeline Events APIs in the last 30 days. Since the `forms` and `content` scopes are not required for either of these functions, please remove them as required from the app’s settings to minimize the permissions users must accept.
### Reliability & Performance
In order to be certified or recertified, your app must do the following:
* Remain in good standing:
* In compliance with all applicable terms.
* Not have been rejected for certification in the last six months.
* Not have any unresolved support escalations with mutual customers.
* Use stable, public versions of HubSpot's APIs and extensions.
* Using the latest public versions is recommended.
* Undocumented APIs are considered unstable and must not be used in your production app.
* Using private and public beta APIs and extensions is permitted. It is recommended to exercise caution and test with set customers before making such features widely available.
* If your app uses APIs or features that are due to sunset, migrating to the latest versions is recommended.
* If your app uses APIs that are not stable or public, please surface this during the certification review or to the Technology Partner Manager team to discuss options.
* Maintain a reasonable volume of [activity](#activity) from HubSpot customer accounts unaffiliated with your organization.
* Adhere to the [API usage guidelines](/docs/developer-tooling/platform/usage-guidelines) and best practices, including:
* Respecting rate limits (i.e. 100 inbound requests every 10 seconds per connected HubSpot account).
* Refreshing OAuth access tokens before they expire.
* Caching data for repeat calls when possible.
* Using batch APIs and webhook subscriptions to reduce request volume when possible.
* Using APIs to create properties, workflows, and custom workflow actions instead of requiring user action.
* Maintain an average success rate above **95%** across all [activities](#activity).
* Requests resulting in error responses count against this success rate.
* Some unavoidable or expected errors may be excluded when calculating success rates across all [activities](#activity).
* If your app provides a browser extension to deliver supplementary functionality and value to customers:
* Browser extensions must not be created specifically for the HubSpot UI or as a workaround to HubSpot's APIs.
* Browser extensions must not inject capabilities or components into HubSpot's UI.
* Officially supported UI extensions (e.g. [App Cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) and [custom workflow actions](/docs/api-reference/latest/automation/workflow-actions/custom-action-guide)) provide more consistent user experiences for customers
* Your app will be subjected to an additional security assessment if it includes a browser extension.
* Use a supported version of the developer platform and all endpoints. Learn more about [which versions of the developer platform and API endpoints are supported](/docs/developer-tooling/platform/versioning).
* Use the [Uninstall App API Endpoint](/docs/api-reference/latest/app-management/app-uninstalls/uninstall-app) when removing the app from a customer's HubSpot account. As part of the certification process, provide a demo video that includes the following:
* A user disconnecting their HubSpot account from your app.
* The endpoint being called.
* The app being removed from the [Connected Apps](https://knowledge.hubspot.com/integrations/manage-your-connected-apps) page in the user's HubSpot account.
#### Feedback examples
> Your app’s API success rate falls below the 95% threshold required for certification. Our logs show a 83% success rate in the last 30 days. The vast majority of these requests returned `429` burst rate limit errors. To reduce this error rate, we recommend throttling requests to 100 requests per 10 seconds for each account.
> Your app is generating errors around trying to update contacts using an `undefined` email address, which will not work with this endpoint. Your app should skip these requests if a record does not have an email address.
> Your app is making requests with expired OAuth tokens and receiving `401` errors before refreshing the token. To minimize these errors, we recommend that your app keep track of when tokens expire or refresh tokens before making requests. If you start seeing `401` errors for 100% of requests and are unable to refresh the access token, consider the app uninstalled and stop making requests for the account until a user re-authenticates your app.
> Your app is generating `403` errors from trying to use the Contact Lists API with Marketing Hub Free accounts, which do not have access to contact lists. If your app repeatedly gets `403` errors for missing the proper scopes, it should stop making calls to retrieve lists from that account.
> Your app’s webhook subscriptions frequently fail with `500` and `503` errors. Make sure that your server can handle the volume of requests (currently limited to 150 per second) so that customer data is not lost.
> Your app is pulling many contacts one at a time instead of pulling them in batches. We recommend using [batch endpoints](/docs/api-reference/latest/crm/objects/contacts/guide) instead.
### Usability & Accessibility | App
* Your app must be [listed](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements) in the HubSpot [Marketplace](https://ecosystem.hubspot.com/marketplace/apps) for at least six months.
* Your app must demonstrate [usability best practices](https://www.nngroup.com/articles/ten-usability-heuristics/).
* Installation and use should not cause confusion or frustration for mutual customers or otherwise negatively affect the core HubSpot user experience.
* Testing and evaluation of your app are conducted at the discretion of the HubSpot Ecosystem Quality team.
#### Feedback example
> Your app currently requires users to manually configure workflow webhook actions to send text messages. Consider creating custom workflow actions via the app which are flexible enough to accommodate many use cases.
### Usability & Accessibility | HubSpot Marketplace Listing
Your HubSpot Marketplace listing must:
* Accurately describe your app's current functionality. If functionality changes based on a user's product or subscription level, either for HubSpot or your solution, differences must be made clear.
* Your HubSpot Marketplace listing must contain clear and accurate pricing information:
* The pricing plan in the listing must match the pricing information published on your website.
* The pricing plan in the listing must only include pricing plans that allow for the usage of your HubSpot integration.
* If a pricing plan does not support the integration, it should not be included in your listing. For example, if you have Plan A and Plan B for your app, but only Plan B can be used with the integration, only the pricing details for Plan B should be included in the listing.
* Free pricing plans should only be used for Free forever or Freemium pricing models.
* Use correct capitalization when referencing HubSpot (with a capital "H" and capital "S") in your app's marketplace listing, supporting documentation, and associated collateral.
* Use placeholder data or hide data to not display personal identifiable information (PII).
* Include:
* Informative and up-to-date visual aids, which may include screenshots or a video. Refer to the [How to Make a Great App Demo Video](https://www.hubspot.com/partners/technology/resources/how-to-make-a-great-app-demo-video) page for best practices and examples of how to create a demo video.
* An up-to-date "Setup documentation URL" that leads directly to a full setup guide for your app. You can review the [full requirements for setup documentation](/docs/apps/developer-platform/list-apps/listing-your-app/create-an-app-listing-setup-guide). This guide also includes an example template that meets all requirements.
* Not include:
* Any data or statistics, unless a case study is provided as a resource.
#### Feedback examples
> Your HubSpot Marketplace listing includes few specific details about your app’s functionality. Please enhance the listing with screenshots which depict app functionality and include more thorough descriptions of common use cases and in-app behavior.
> HubSpot customers are used to a “try before you buy” experience when purchasing our products and services. For this reason, we recommend your app provide a free trial or freemium sign-up experience. Some technology partners who do not have pricing pages or free trials have created “HubSpot plans,” offering mutual customers transparent pricing, touchless sign-up, and other benefits.
### Usability & Accessibility | Supporting Documentation
Supporting documentation for your app must:
* Exist on a live, publicly accessible URL (i.e. no paywalls or login required) and adhere to current accessibility, privacy, and GDPR standards.
* Be up-to-date and consistent with the current version of your app.
* Clearly describe:
* What your app does.
* How to install your app and connect a HubSpot account with screenshots of each step, including the scope approval screen.
* How to configure your app once it is installed.
* How to use your app, including both manual and automated interactions.
* How to disconnect HubSpot from your app.
* How to uninstall your app from a HubSpot account.
* How disconnecting and uninstalling might affect users' HubSpot accounts and data.
* Include images. Any images containing screenshots of the HubSpot UI should be up-to-date and consistent with the HubSpot design system.
* Videos are also **strongly** recommended, but not required. Videos should be updated regularly and reflect the current version of your app.
#### Feedback example
> The setup guide for your app includes a screenshot depicting the scopes your app requires for installation. This screenshot does not show the `business-intelligence` scope, which is selected in your app’s settings. Please update the screenshot so that it reflects the current required scopes.
### Value
* Your app's active install count, retention, and HubSpot Marketplace reviews are assessed as indicators of the value mutual customers find in your app.
* Your app must have at least 60 [active](#activity), unique installs to qualify for and retain certification. The accounts with installs must be unaffiliated with your organization. Test accounts will also be excluded.
* If your app has fewer than 60 active installs, then you will be asked to cancel certification request.
* If your app has fewer than the three active installs required to be listed, then your app may be removed from the HubSpot Marketplace.
* Your app listing must have responses from your team for any negative reviews of your app.
#### Feedback example
> Your app has not maintained at least 60 active installs over the trailing six month period. As such, its certified status will be removed. You may re-apply for certification in six months.
## The app certification and recertification review process
The HubSpot Ecosystem Quality team responds to [app certification requests](/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification) within **10 business days**. The entire app review and feedback process should take no more than **60 days** from the time feedback is shared. Review the criteria listed [here](/docs/apps/developer-platform/list-apps/testing-credentials) for providing testing credentials to your app.
Should your app meet all requirements, it will earn certified status and a “HubSpot Certified App” badge will be displayed to customers and prospects on the HubSpot Marketplace. Your app will also appear when users select the “HubSpot Certified App” filter.
Should your app not successfully complete the review, you may re-apply in six months.
As of August 5, 2025, all certified apps are now subject to a two year certification cycle, introduced earlier this year, to ensure ongoing compliance with HubSpot's evolving standards. This means that once certified, your app will need to be recertified every two years in order to maintain its certified status. Each app will have a set certification expiration and renewal date, and failure to submit for renewal before the expiration date will result in the removal of your app’s certified designation from the HubSpot Marketplace.
### How recertification works
You will receive notifications starting 90 days before your app’s certification expiration date, giving you ample time to make any necessary updates and resubmit your app for review. To support a smooth renewal experience, new functionality is now available in the developer account. These tools include:
* Clear tracking of your app’s certification status and expiration date
* Step-by-step guidance on the renewal process
* Improved submission workflows to reduce friction
This recertification process replaces the previous annual review and allows for more predictable, consistent quality checks across all certified apps. If your app no longer meets the certification standards during the renewal process, our Ecosystem Quality team will work with you to resolve any issues before the certification expires.
**Please note:**
If your app falls out of compliance with the certification requirements listed above at any time, HubSpot may immediately initiate a recertification process, even if your app has been certified for less than two years. As stated in the [Technology Partner Program Agreement](https://legal.hubspot.com/technology-program-agreement), HubSpot also reserve the right to unpublish your app at any time.
It's highly encouraged that you monitor your app's performance, certification requirements, Developer Changelog, and any additional HubSpot resources related to any changes in technology used and how your app could stay up to date.
## Frequently asked questions
No, we do not charge you a fee to list or certify your apps in the HubSpot Marketplace, nor a fee for installs generated through the HubSpot Marketplace. There is no revenue sharing. We are here to support you to make your app of higher quality.
No. At this time we do not have notifications enabled to notify you if and when you will be eligible to re-apply at this time. Your [Technology Partner Manager](#technology-partner-manager) would be the best resource to contact and ask if you are eligible before applying.
Feel free to use the press release template on [this page](https://www.hubspot.com/partners/technology/resources) to share the news that your app has earned certification.
If you plan to post on social media, be sure to tag HubSpot — we love to celebrate alongside our technology partners!
We recommend you reach out to your [Technology Partner Manager](#technology-partner-manager) to see if app certification is right for your app.
Our goal is to ensure your app is well built for our mutual customers and limits breaking changes, which requires your app uses the latest stable APIs. We also love seeing and supporting entrepreneurs, early adopters, and developers who are eager to experiment with the newest beta APIs.
The benefits of being featured in collections and for customers to easily filter for a certified app within the HubSpot Marketplace are continuing to evolve. We’d like to learn more about how you would find being featured the most helpful (e.g. HubSpot Marketplace, HubSpot community, HubSpot curated newsletters or other forms).
Your [Technology Partner Manager](#technology-partner-manager) would be the best contact to discuss potential future benefits and start this conversation.
With the average customer using more than five integrations, it’s imperative apps are monitored and held to privacy, security, and quality standards over time. Any public assets will be assessed using information already provided during a listing process and findings will be analyzed using a non-invasive method.
If your app loses its certified status, it will remain listed in the HubSpot Marketplace but without the certified badge. To regain certification status, ensure your app meets all current requirements and submit it for a certification review, which is separate from a standard listing update.
Find your **Technology Partner Manager** and their email information by logging into your developer account and navigating to **App Listings**. Hover over your app, click **More** > **View Listing Details**.
***
## Related resources
[How to apply for app certification](/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification)
[How to list your app](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app)
[App listing requirements](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements)
[Developer community forum](https://community.hubspot.com/t5/APIs-Integrations/bd-p/integrations)
[Contact the Technology Partner team](https://www.hubspot.com/partners/technology/join)
# App listing requirements in the HubSpot Marketplace
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements
Technology Partners can now see HubSpot's guidelines and requirements for getting an app listed on the HubSpot Marketplace in one place.
App listing submissions are manually reviewed by the HubSpot Ecosystem Quality team and will be rejected if they do not meet the criteria outlined below. Once your app meets these requirements, you can [build your app listing](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) from within your app developer account.
## Minimum requirements
* **Access:** the listing must not redirect to a different public or private app or require another public or private app to function.
* **Uniqueness:** your listed public app must be unique. If you have already listed an app and you want to replace it, update the existing app instead of listing a new one.
* **Use case:** each app you create should address a separate use case. Apps with similar functionality and use the same APIs should be consolidated into a single app.
* **Single HubSpot app ID:** your app must authorize API requests with the public HubSpot app ID (and [OAuth client ID](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth)) associated with your app listing.
* **OAuth:** your app must use OAuth as its sole authorization method. Learn more about [working with OAuth](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth).
* **Installs:** your app must have at least three active, unique installs.
* App activity is defined by OAuth-authenticated requests to HubSpot's [APIs](/docs/api-reference/latest/overview) and [signed requests](/docs/apps/developer-platform/build-apps/authentication/request-validation) from HubSpot [webhook subscriptions](/docs/api-reference/latest/webhooks) and extensions (e.g. [app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview) data fetch requests).
* Active installs are the number of unique HubSpot production accounts, unaffiliated with your organization, showing successful app activity within the past 30 days.
* **Scopes:** only request [scopes](/docs/apps/developer-platform/build-apps/authentication/scopes) your app needs.
* **Terms:** you must review and agree to the terms in [HubSpot's Technology Partner Program Agreement](https://legal.hubspot.com/technology-program-agreement).
* **Restricted industries:** your app must not fit or deliver functionality that would exclusively serve customers within any of HubSpot's [restricted industries](https://legal.hubspot.com/acceptable-use#Restricted-Industries).
* **Restricted functionality:** your app cannot use [classic CRM cards](/docs/api-reference/latest/crm/extensions/crm-cards/guide), as they are no longer supported as of June 16, 2025. Learn more about this announcement on the [HubSpot Developer Changelog](https://community.hubspot.com/t5/Developer-Announcements/Deprecating-Support-for-Classic-Legacy-CRM-Cards/m-p/1153039#M1048).
* **Developer platform versions**: once a version of the developer platform has been announced as unsupported, apps using that version cannot be listed in the HubSpot Marketplace. Learn more about [developer platform versioning](/docs/developer-tooling/platform/versioning).
* **AI connectors**: if your app is an AI connector - an app that primarily connects HubSpot to external generative AI tools - it must require user-level permissions and be built with [HubSpot's MCP Server](/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server).
### HubSpot brand requirements
* Your app and its associated assets (documentation, landing pages, etc.) must meet [HubSpot’s Branding Guidelines](https://www.hubspot.com/partners/technology/branding-guidelines). For example, capitalize the “S” in “HubSpot” any time you’re referring to HubSpot.
* Your app and its associated assets (documentation, landing pages, etc.) must not infringe on [HubSpot’s Trademark Usage Guidelines](https://legal.hubspot.com/tm-usage-guidelines). For example, do not combine HubSpot's name (including “Hub” and “HubSpot”) with your app name or logo.
### Listing requirements
Once you’ve met the minimum requirements, you can submit your app listing. When submitting your app listing, you must completely and accurately fill out all information. These fields are particularly important and failure to meet these requirements will cause your listing to be set to Draft mode only:
* The content of your listing should be specific to the integration as opposed to general product information. It should contain information about the value customers can expect specifically from downloading and using this integration. Good examples include: [Aircall](https://ecosystem.hubspot.com/marketplace/apps/aircall), [CloudFiles](https://ecosystem.hubspot.com/marketplace/apps/cloudfiles), [Trumpet](https://ecosystem.hubspot.com/marketplace/listing/trumpet-819430).
* All URLs in your HubSpot Marketplace listing must lead to live, publicly available, and functional pages.
* This will be verified using [HubSpot's SEO tools](https://knowledge.hubspot.com/seo/understand-seo-crawling-errors) to crawl pages associated with the listing.
* To prevent unnecessary delays in the review process, it is strongly recommended to work with your site administrator to add HubSpot's crawler's user agent, *HubSpot Crawler*, to the allow list as an exemption prior to submitting your app listing.
* A link to publicly available setup documentation specific to your HubSpot integration.
* Review the [full requirements for setup documentation](/docs/apps/developer-platform/list-apps/listing-your-app/create-an-app-listing-setup-guide). This guide also includes an example template that meets all requirements.
* For a live example, check out the [Salesmsg setup guide](https://help.salesmessage.com/en/collections/2430749-hubspot).
* Include a relevant Install button URL that brings customers to a page where they can easily connect your app with HubSpot.
* URLs for your app’s support resources (support website, HubSpot community forum, case study) must be live, up-to-date, and publicly available.
* URLs for your app’s Terms of Service and Privacy Policy must be live and up-to-date.
* All URL fields have a limit of 250 characters.
* *Shared data*, which lets users know how information will flow between your app and HubSpot, must be accurate, up-to-date, and reflect the [scopes](/docs/apps/developer-platform/build-apps/authentication/scopes) your app requests.
* All objects selected in your OAuth scopes should be documented in the *Shared data* table.
* If your app is requesting both read and write object scopes, the data sync should be advertised as bi-directional for these specific objects.
* Your HubSpot Marketplace listing must contain clear and accurate pricing information:
* The pricing plan in the listing must match the pricing information published on your website.
* The pricing plan in the listing must only include pricing plans that allow for the usage of your HubSpot integration.
* If a pricing plan does not support the integration, it should not be included in your listing.
* For example, if you have *Plan A* and *Plan B* for your app, but only *Plan B* can be used with the integration, only the pricing details for *Plan B* should be included in the listing.
* Free pricing plans should only be used for Free forever or Freemium pricing models.
* You must include at least one support contact method.
### App card requirements
If your app includes [app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview) built using UI extensions, your app must adhere to the following additional criteria:
* **Naming:** per the [Technology Partner Program branding guidelines](https://www.hubspot.com/partners/technology/branding-guidelines):
* Do not modify, imitate, or abbreviate any HubSpot brands or names (e.g., "HubSpot," "Hub," etc.) anywhere in the name of your app card.
* Do not use a generic product name + any HubSpot brands or names (e.g., "App card for HubSpot").
* Do not brand your app card using the word "inbound" in a way that would tie it to HubSpot's UNBOUND/INBOUND event (e.g., "Inbound Sales app card").
* **Logos and icons:**
* Per the [Technology Partner Program branding guidelines](https://www.hubspot.com/partners/technology/branding-guidelines), do not use the HubSpot company logo or sprocket without permission.
* Do not use company or brand logos other than your own as icons.
* **Sensitive data:**
* Your app must not access, request, or use [sensitive data scopes](/docs/api-reference/latest/crm/properties/sensitive-data).
* Your app card must not display sensitive information, as defined in [HubSpot's Terms of Service](https://legal.hubspot.com/terms-of-service).
#### Security and privacy
* Your app must use all of the [scopes](/docs/apps/developer-platform/build-apps/authentication/scopes#app-scope-types) that it requests during installation. Scopes that are not used must be removed. If certain scopes only apply to a subset of your app's user base, they should be included as conditionally required or optional scopes.
* If your app requires a dedicated browser extension, the extension must be listed and approved in the browser's official extension marketplace. For example, apps in Google Chrome should be listed and approved in the Chrome Web Store.
#### Reliability and performance
For linked assets such as images and JavaScript, avoid using absolute links. Instead, use relative links and include the assets in your files. Exceptions may only be made if you use a reputable CDN.
#### Usability and accessibility
* **Buttons:**
* [Forms](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/forms) must include submit [Buttons](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/button).
* Ensure destructive button styles denote a destructive behavior.
* Include only one primary button per surface (app card, modal, or panel).
* **Text:**
* It's recommended to not use underline formatting for text that's next to a hyperlink, as it will also appear clickable.
* Do not use [Tags](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tag) in place of Buttons or [Links](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link).
## Review, feedback, and approval
Once you submit your listing, the HubSpot Ecosystem Quality team will complete an initial review within 10 business days. If any of the information provided is incorrect, misleading, or incomplete, we’ll contact you with that feedback. The entire app review and feedback process should take no more than 60 days from the time feedback is shared. As stated in the [HubSpot Marketplace Terms](https://legal.hubspot.com/technology-program-agreement), HubSpot reserves the right to unpublish or refuse publication of your app listing at any time.
**Please note**: you can only submit one app at a time for approval. Any additional apps submitted while the initial app is being processed will automatically be rejected.
## Rewards for Listed Technology Partners
For full details, see the [Tech Partner Program Guide](https://www.hubspot.com/hubfs/app-partner-enablement/how-to-become-a-hubspot-technology-partner.pdf).
* Dedicated HubSpot Marketplace listing
* Priority access to developer support through a dedicated support alias
* Developer community resources, including webinars, forums, and more
* Curated marketing resources, including PR templates and launch guides
* Discounted UNBOUND/INBOUND event sponsorship, booths, and tickets
* Discounted software through the HubSpot for Startups seed-stage program
* Monthly newsletter with marketing updates, product releases, and more
## Related resources
* [How to list your app](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app)
* [App certification requirements](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements)
* [API reference documentation](/docs/api-reference/latest/overview)
* [Developer community forum](https://community.hubspot.com/t5/APIs-Integrations/bd-p/integrations)
* [Contact the Technology Partner team](https://www.hubspot.com/partners/technology/join)
# Create an app listing setup guide
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/listing-your-app/create-an-app-listing-setup-guide
Learn more about creating a publicly available setup guide to include with your HubSpot Marketplace app listing.
In order for an app to be [listed](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) or [certified](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements) on the HubSpot Marketplace, the listing must include a publicly available setup guide URL.
**Please note:**
You can create separate guides for installing, configuring, using, disconnecting, and uninstalling your app, but all guides must meet the requirements and be linked to the *Setup documentation URL* included on your HubSpot Marketplace listing.
## Setup guide requirements
In order to be [listed](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) on the HubSpot Marketplace, your app must include a setup guide that meets the following requirements:
* Not hidden behind a paywall or sign-in screen.
* Specific to setting up your app, i.e. you cannot link to your website homepage or knowledge base homepage.
* Contains the steps to install and configure the integration.
Learn more about the [full requirements for listing your app](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements) or check out an [example of a listed setup guide](https://orgcharthub.com/guides/setup) from OrgChartHub.
In order to be [certified](/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification) on the HubSpot Marketplace, your app must include a setup guide that meets the following requirements, in addition to the above:
* Adheres to current accessibility, privacy, and GDPR standards.
* Is consistent with the current version of your app and HubSpot.
* Includes images. Videos are recommended, but not required.
* Includes the following information:
* What your app does.
* How to install your app.
* How to connect a HubSpot account to your app.
* Screenshots of each step of the installation process, including the scope approval screen.
* How to configure your app, once installed.
* How to use your app, including both automated and manual interactions.
* How to disconnect a HubSpot account from your app.
* How to uninstall your app from a HubSpot account.
* Potential consequences to user data from disconnecting and uninstalling your app.
Learn more about the [full requirements for certifying your app](/docs/apps/developer-platform/list-apps/apply-for-certification/certification-requirements).
## Setup guide template
The following template uses best practices for creating a setup guide. Using this format is not required, but the template is designed to give your app a strong probability of meeting HubSpot's requirements for both listing and certification.
### Setup guide for \[App Name]
\[*App Name*] integrates \[*Platform Name*] leads and accounts with HubSpot contacts and companies, which lets users:
* \[*Description of a use case*]
* \[*Description of an additional use case*]
#### Install the app
\[*Describe instructions to install your app and connect a HubSpot account, with visual aids for each step.*] For example:
* Log in to \[*Platform Name*].
* Navigate to **Settings** > **Integrations**.
* Locate the \[*App Name*] card.
* \[*Include a screenshot here.*]
* Click **Install**.
* Select your **HubSpot account**.
* Click **Choose Account**.
* Review the requested scopes on this screen. \[*App Name*] requests access to read and write contacts & companies, as well as read and edit contact & company properties.
* Click **Connect app**.
* You will be redirected to the \[*Platform Name*] "Integrations" page, and new configuration options will appear.
#### Configure the app
\[*Describe instructions to configure your app's connection to HubSpot, including visual aids for each step.*]
* In your \[*Platform Name*] account, navigate to **Settings** > **Integrations** > **\[*App Name*]**.
* Toggle **Sync direction** to select how data will sync:
* **One-way**: \[*App Name*] will only update HubSpot.
* **Two-way**: \[*App Name*] will keep \[*Platform Name*] and HubSpot in sync.
* \[*Include a screenshot here.*]
* Map your \[*Platform Name*] lead and account fields to contact and company properties in HubSpot.
* \[*Include a screenshot here.*]
* When ready, click to toggle the **Sync** switch on.
* \[*Include a screenshot here.*]
* Select either **Sync existing records** or **Sync only new records**.
* \[*Include a screenshot here.*]
* Click **Done**.
#### Use the app
\[*Describe how to perform basic manual and/or automated actions with your app with relevant visual aids.*] For example:
\[*App Name*] automatically keeps your \[*Platform Name*] leads and accounts synced with your HubSpot contacts and companies, according to the settings configured in the previous section. Syncs run at least every 5 minutes. No manual actions are required.
#### Disconnect the app
\[*Describe instructions to disconnect a HubSpot account from your app, including visual aids for each step. You should also describe how disconnecting affects users' HubSpot accounts and data.*] For example:
**Note**: If you disconnect your HubSpot account from \[*App Name*], \[*Platform Name*] lead & account data will no longer sync to HubSpot contacts & companies and vice versa. Existing data will remain on your HubSpot records.
* Log in to \[*Platform Name*].
* Navigate to **Settings** > **Integrations**.
* Locate the \[*App Name*] card.
* \[*Include a screenshot here.*]
* Click **Disconnect**.
* \[*Include a screenshot here.*]
* Click **Yes, I am sure**.
#### Uninstall the app
\[*Describe instructions to uninstall your app from within a HubSpot account. You may simply link and refer to the HubSpot Knowledge Base article on this topic.*] For example:
To uninstall \[*App Name*] from your HubSpot account, follow the instructions in [this HubSpot Knowledge Base article](https://knowledge.hubspot.com/integrations/connect-apps-to-hubspot#uninstall-an-app).
# HubSpot Marketplace | Listing your app
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app
Follow these steps to submit an app for listing on HubSpot Marketplace.
After you’ve created an app in your developer account that meets the [HubSpot Marketplace listing requirements](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements), you can submit a listing to add it to the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps). The HubSpot Ecosystem Quality team will review your submission and follow up via email when the app has been approved or rejected.
**Please note:**
You must be a [Super admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) to update and submit an app listing.
## Create and submit an app listing
**Please note:**
Before submitting an app listing, review the [App listing requirements page](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements) to understand how to fill your listing.
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, click **App Listings**.
* In the upper right, click **Create listing**. If this button is grayed out, listings have already been created for all your existing apps.
* Select the **app** you want to create a listing for and click **Next**. Apps that are already listed on HubSpot Marketplace will not appear here.
* Click the **Select the primary listing language for \[app name]** dropdown menu and select the **default language** users will see when browsing HubSpot Marketplace.
* Click **Next**.
The app listing wizard has seven tabs of information to fill out:
* [Listing info](#listing-info)
* [App details](#app-details)
* [Pricing](#pricing)
* [App features](#app-features)
* [Support info](#support-info)
* [Testing info](#testing-info)
* [Review info](#review-info)
### Listing info
On the *Listing info* tab:
* In the *App information* section, add your **Public app name**, **Company name**, **Tagline**, and **Install Button URL**.
* Click the **Install Button URL** dropdown menu and select a **URL**. These URLs are populated from the redirect URLs set in your [app settings](/docs/apps/developer-platform/build-apps/manage-apps-in-hubspot#manage-authentication).
* In the *Sign-in configuration* section, select whether the app will require a separate partner sign-in. Learn more about the [app install flow with and without partner sign in](/docs/apps/developer-platform/list-apps/understand-app-install-flow).
* In the *App icon* section, upload an 800px by 800px icon for your app. This will appear in HubSpot Marketplace and connected users’ accounts. When creating your icon:
* **Do**: use a JPG, JPEG, or PNG file, fill the entire space (800px by 800px) - your image should touch at least two edges, and use a high-resolution, non-pixelated image.
* **Do not**: include text in your icon, use a wordmark, or leave extra whitespace around your icon.
* In the *Categorize your app* section, you can select up to two **categories** for your app. Learn more about the [different categories available](/docs/apps/developer-platform/list-apps/listing-your-app/understand-app-categories).
* You can also review and set a **URL path** and add any **search terms** that can be used to find your app in HubSpot Marketplace.
### App details
On the *App details* tab:
* In the *Demo video* section, upload a **video** to show how your app works. Refer to the [How to Make a Great App Demo Video](https://www.hubspot.com/partners/technology/resources/how-to-make-a-great-app-demo-video) page for best practices and examples of how to create a demo video.
* In the *Screenshots* section, add **images** and **alt text** showing how your app works. You can add up to eight images.
* In the *App Overview* section, enter an **overview** for your app. Your overview should include information on what your app does, the key business problems your app solves, and why users should install your app.
* Click **Add shared data**. In the *Shared data* section, let users know how data will flow between your app and HubSpot.
* In the right panel, select which app object syncs with which HubSpot object, and the direction of the sync. In the *App Overview* section, give a brief **description** of how your app can help users carry out their goals.
* To add another object sync, click **Add another object**.
* Click the **HubSpot features your app works with** dropdown menu and select the **checkboxes** next to the HubSpot features. You can add up to 10 HubSpot tools and features.
* Click the **Other tools your software integrates with** dropdown menu and select the **checkboxes** next to any external tools or apps that your app integrated with. You can select up to 6 other tools or apps.
* Click the **Languages your app is available in** dropdown menu select all **languages** available for your app. You can only create additional HubSpot Marketplace listings in these languages.
### Pricing
On the *Pricing* tab:
* Click the **currency** dropdown menu and select the **checkboxes** next to the currencies you want to list the app in. You can select from over 100 currencies.
* You can also set up pricing plans for your app by adding the **pricing model**, **plan name**, **tagline**, **pricing detail**, and **features list**.
* Depending on the pricing model selected, you may need to add more information, such as the frequency of payment, one-time fees, or monthly prices. Hover over the **information icon** to learn more about each pricing model.
* To add another pricing plans, click **Add another plan**. You can add up to 5 pricing plans.
* In the *Link to your software’s pricing plan* section, enter the **URL** where users can find more information on your pricing plans.
* In the *Agency pricing plans* section, enter the **URL** where users can learn more about pricing for partner or consulting services.
### App features
On the *App features* tab, add features and guide customers on how to use them. There is no limit on the number of app features that can be created for your app.
* In the top right, click **Add a feature**.
* On the *Feature details* page, configure your app features:
* **Feature name:** enter your feature name. This should describe your what your feature does.
* **Scopes:** select all scope groups a customer's account will need to have this feature. Scope groups are used to determine whether the customer's HubSpot account is compatible with the app features.
* **Description:** enter a detailed description for your feature, and how it can solve a customer's business problems.
* **Image:** add an image for your feature. After adding your image, click to toggle the **On your app's marketplace listing** or **As a feature discovery card** switches on to configure where your images should display.
* If you've chosen select to display the feature as a feature discovery card:
* Select an option for your primary button:
* **Link to a feature:** select which HubSpot feature the button should link to.
* **Create custom**: enter **Button text** and the **Button URL**.
* **No primary button:** no button will be displayed on your feature discovery card.
* Select a how-to guide to onboard your customers:
* **Create a guide from scratch:** enter a **title**, **description**, and **image or video**.
* **External link to guide:** enter a **Guide URL**.
* **Video only guide:** upload a **video**.
* To add another guide, click **Add another section**.
### Support info
On the *Support info* tab:
* In the *Contact info* section, add a support contact method for users who have questions while using your app. It is required add a **support email** and **languages** that customer support is offered in. You can also include links to your company website, live chat, Facebook page, and a phone number.
* In the *Support resources* section, include **links** to your app’s setup documentation.
* In the *Terms of Service and Privacy Policy* section, add **links** to your privacy documentation.
### Testing info
On the *Testing info* tab:
* In the *App review instructions* field, enter steps for HubSpot's developers to test the app as part of the review process. Learn how to [providing testing details and credentials for your app](/docs/apps/developer-platform/list-apps/testing-credentials).
* In the *Technology Partner Program points of contact* section, add details for members of your team to receive listing, technical and program information about your app listing.
* These individuals receive key updates, resources, and opportunities designed to help your business grow and succeed as a HubSpot Technology Partner. Keeping these contacts accurate and up-to-date is critical, and will ensure the right people in your business receive timely and tailored communication from HubSpot.
* HubSpot will consider the team member you provide for *Contact 1* as the **main** point of contact.
* You must include at least a *Technology Partner Program main point of contact*
* **Business Development / Partner Manager:** the person who focuses on maintaining and expanding the business relationship between your company and HubSpot.
* **Developer:** the primary contact for all technical communication for your app's integration with HubSpot. They will manage the build process, maintenance, and performance of your app to provide a smooth user experience.
* **Executive:** the executive sponsor of the partnership. This should be a senior leader on your team who provides strategic oversight of your company's relationship with HubSpot and may make decisions about partnership direction or future investments.
* **Founder/Co-founder:** the original creator or co-creator of your business.
* **Marketing manager:** a team member who manages app promotion and helps drive adoption within the HubSpot ecosystem. They likely orchestrate go-to-market resources and analyze app performance to improve visibility and user engagement.
* **Product manager:** the owner who oversees your app's feature development and aligns your integration with HubSpot's product standards.
* To add another person as a point of contact, at the bottom, click **Add another point of contact**.
The tabs below provide additional context and information about the communications that each role will receive when added as a point of contact:
**Best for:** program owner, partnership lead, or a team member responsible for managing your HubSpot partnership.
**Communications received:**
* Technical updates and breaking change announcements
* Program news, events, and opportunities
* Exclusive partner resources and best practices
* Guidance on building, marketing, and scaling success in the HubSpot ecosystem
**Best for:** a team member focused on growing and optimizing your partnership with HubSpot.
**Communications received:**
* Program news, events, and opportunities
* Exclusive partner resources and best practices
* Guidance on building, marketing, and scaling success in the HubSpot ecosystem
* Technical updates and breaking change announcements
* Does not receive technical updates or changelog posts unless the *Developer* point of contact is invalid
**Best for:** engineering lead, engineer, or a technical manager responsible for your app's infrastructure.
**Communications received:**
* API changelog posts and breaking change announcements
* Proactive technical guidance
* Updates impacting integration performance or reliability
* Early access to new developer tools and platform updates
**Best for:** founder, executive, or a senior leader who champions your HubSpot partnership.
**Communications received:**
* Strategic program updates and high-level announcements
* Partnership opportunities with business or brand impact
* Invitations to exclusive leadership initiatives and events
**Best for:** marketing lead, content strategist, or a growth manager responsible for app awareness and adoption.
**Communications received:**
* Marketing best practices and campaign opportunities
* Account and app analytics and app listing performance insights
* Updates to app listing or marketplace features
* Go-to-market resources to enhance positioning as a technology partner
**Best for:** a team member who oversees your app's strategy, roadmap, and overall user experience.
**Communications received:**
* API changelog posts and breaking change announcements
* Proactive technical guidance
* Updates impacting integration performance or reliability
* Early access to new developer tools and platform updates
### Review info
On the *Review info* tab:
In the *App review* section, a list of all listing errors will appear:
* To review details for all listing errors, at the top, click **Expand all**.
* To hide the details for all listing errors, at the top, click **Collapse all**.
* To resolve an error, click the **error name**. This will direct you to the relevant tab and section to resolve the error.
* If the *Validate & submit* button is grayed out, check that you’ve filled out all the required fields and have *Super admin* permissions.
* If you’ve missed any required fields, you will see a number in the tab heading indicating the number of missed fields. Click each **tab** and enter the missing information, then return to the *Review info* tab.
* At the bottom, click **Run validation**.
### Submit app
Once you've resolved any validation errors, you can submit your app for approval.
* Click **Submit for review** in the top right.
* In the dialog box, read through the terms and conditions, then select the **checkboxes**.
* By default, localized listings will be automatically translated into all supported languages. To customize which languages are automatically translated:
* Click to expand the **Languages selected for auto-translation** section.
* Clear the **checkbox** next to each language you don't want translated. It is recommended to translate app listings into all available languages, but you may wish to omit languages with a manually created translation.
If you select a language with an existing translation, it will be overwritten by the automatic translation. Languages with a drafted or published translation will be labeled with their current status.
* Click **Agree & submit**.
## Create and update a localized listing for an existing app listing
You will need to set a primary language on your existing app listing and have your primary listing already published in the HubSpot Marketplace in order to manually create listings in other languages. Automatically translated listings can be created when [resubmitting your app](#submit-app).
* In your HubSpot app developer account, click **Marketplace** > **App Listings**.
* If you already have an app listed in the Marketplace, you’ll see a yellow banner above the listed app asking you to set your primary listing language. Click **Set it now**. You will need to set the primary listing language for the app listing before you're able to create new language listings.
* In the dialog box, click the **Select the languages your app is available in** dropdown menu and select the **languages** your app software is available in.
* Click the **Select the primary listing language for \[app name]** dropdown menu and select the **default language** users will see when browsing HubSpot Marketplace.
* Click **Save.**
Once you have set a primary language, you will be able to add a manually translated listing:
* Hover over the **app listing** and click **More** > **Create listing in another language**.
* Click the **Language for this listing** dropdown menu and select the **language** you want to create this listing in.
* When a user has set the language in their account, then they will automatically see the listing in that same language. For example, if you've create a listing in Spanish, and a user has set Spanish as their [account's default language](https://knowledge.hubspot.com/account-management/change-your-language-and-region-settings), the listing will appear to the user in Spanish.
* Click **Create**, then follow the steps to create and submit a listing in the selected language.
## Edit a live app listing
* In your developer account, navigate to **Marketplace** > **App Listings**.
* Hover over the listing you’d like to edit and click **More**. Then, select **Edit**.
* Make any changes, then click **Submit for review** in the top right and [resubmit your app](#submit-app).
## Unpublish a live app listing
* In your developer account, navigate to **Marketplace** > **App Listings**.
* Hover over the listing you want to unpublish and click **More**. Then, select **Unpublish live listing**.
* In the dialog box, enter the reason for unpublishing the app, the click **Submit unpublish request**.
Unpublish requests will be processed by the HubSpot Marketplace team within 10 business days of submission.
# HubSpot Marketplace restrictions
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/listing-your-app/marketplace-restrictions
Learn more about app types that cannot be listed on the HubSpot Marketplace.
To maintain the security, data integrity, and user experience of the HubSpot Marketplace, certain categories of applications are subject to additional review requirements or prohibited entirely. Review app restrictions, as well as [listing requirements](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements/), before [creating an app listing](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app/).
## Integration architecture
Apps must deliver standalone value directly to end users. The following architectural patterns are not eligible for listing:
* **Credential and token brokering**: apps with a primary commercial offering of storing, managing, or distributing end-user OAuth tokens or API credentials on behalf of third-party developers.
* **Developer-only infrastructure**: apps that require a third-party developer to build a separate product before an end user can derive value.
* **White-label integration infrastructure**: apps with a HubSpot integration designed to be embedded invisibly into another party's product.
## Model Context Protocol (MCP) & AI connectors
Apps that let AI agents or assistants interact with HubSpot CRM data are subject to additional requirements. These requirements do not apply to apps with a static, pre-defined data sync that happens to use AI upstream.
* **Non-deterministic access**: apps where an AI interprets natural language and decides which CRM actions to take must enforce user-level permissions, i.e. that the app can only access data the specific HubSpot user has permissions to see. Standard OAuth tokens are not sufficient for this purpose. The underlying token must enforce what the user is permitted to do in HubSpot.
* **Build requirements**: apps must be built with either the [MCP auth app](/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server), or the [project-based MCP app](/docs/apps/developer-platform/add-features/mcp-server) if also using `isUserLevel: true`.
## Sales intelligence & data enrichment
Apps that inject, enrich, or provide contact and company data within HubSpot CRM are subject to compliance and data-sourcing review.
The following types of apps are prohibited:
* Apps that import non-opted-in email addresses into HubSpot, as this violates [Developer Policy](https://legal.hubspot.com/hubspot-developer-policy) sections A.2 and C.1.
* Apps that scrape data from LinkedIn profiles, as this violates [Developer Policy](https://legal.hubspot.com/hubspot-developer-policy) section A.3 and [LinkedIn's User Agreement](https://www.linkedin.com/legal/user-agreement) section 8.2.
* Apps that support bulk import of prospecting databases into HubSpot for mass emailing.
The following types of apps are subject to additional review:
* Apps that enrich existing HubSpot contacts with firmographic or technographic data from third-party sources.
* Apps that sync contacts who have actively engaged with the customer's own outreach (e.g., replied to an email the customer sent). These may be acceptable if every synced contact has genuinely engaged, and there is no path for cold contacts to bypass the filter.
* De-anonymized website visitor identification apps.
## Restricted industries
Apps primarily serving or positioned within certain industries are not eligible for listing, as stated in [HubSpot's Acceptable Use Policy](https://legal.hubspot.com/acceptable-use) and [Technology Partner Program Policies](https://www.hubspot.com/partners/technology). Apps that fall within a restricted industry but build functionality unrelated to that industry may be considered on a case-by-case basis.
Restricted industries include:
* Cryptocurrency.
* Non-fungible tokens (NFTs).
* Escort and dating services.
* Pharmaceutical products.
* Gambling services or products.
* List brokers or list rental services.
* Selling social media likes or followers.
## Additional restrictions
The following types of apps are also restricted, as outlined in the [HubSpot Marketplace listing requirements](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements):
* Apps using [legacy CRM cards](/docs/api-reference/legacy/crm/extensions/crm-cards/guide).
* Apps built on unsupported [developer platform versions](/docs/developer-tooling/platform/versioning).
* Apps that redirect to a different public or private app.
* Duplicate apps from the same creator.
# HubSpot Marketplace | Understand app categories
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/listing-your-app/understand-app-categories
When submitting an app for listing on HubSpot Marketplace, understand app categories before selecting the category for your app.
When [listing your app](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) in HubSpot Marketplace, you can select up to two categories for your app. Use the categories to define the functionality and purpose of your app.
**Please note:**
You must be a [super admin](https://knowledge.hubspot.com/user-management/hubspot-user-permissions-guide#super-admin) to update and submit an app listing.
* **Collaboration:** these apps facilitate communication between team members by offering a convenient, informal space to directly message one another, talk as a group, and share relevant content. This category includes whiteboard apps, screen-sharing apps, virtual workspaces, and visual collaboration platforms.
* **ETL:** Extract, transform, and load (ETL) apps are used to transfer data between databases or for external use.
* These apps are used for data replication, after which the data will be stored in database management systems and data warehouses. These solutions also help with data extraction for analytics.
* This category also includes Reverse ETL apps. These apps sync data from a data warehouse to several business applications. Pull data from data warehouses or any other central data repository and send this data to third-party integrations, API integrations, and tools, pre-built connectors.
* **Messaging Network:** these apps serve as an internal messaging system for businesses via a text-based messaging application. Messaging network apps facilitate one-on-one, direct messaging as well as messaging within predefined groups and teams.
* **Partner Management:** partner relationship management (PRM) apps, also known as partner management, give businesses tools to track sales partners and affiliates. PRM solutions offer a private portal for each partner to access documents, campaign materials, market development funds (MDF), opportunities, and deals.
* **Project Management:** use these apps to plan projects, assign tasks, and organize teams. They give you real-time status updates so you can make quick decisions and control projects for any size or team. Generally, you can also track how much time each person or team spends working on various projects to improve efficiency at an organizational level.
* **Content Management System:** these apps help users simplify creating, editing, and publishing digital content. This category includes WordPress and apps used to build and manage content on a website, blog, or platform.
* **iPaaS**: Integration platform as a service (iPaaS) apps offer a centralized console to manage, govern, and integrate cloud-based applications. These tools work by connecting cloud applications and services and controlling integration flows.
* **Form and pop-up Builder**: these apps enable non-technical users to create and deploy pop-up messages on a website to encourage users to do something, such as sign up for a newsletter.
* Pop-ups can appear as modal windows or overlays displayed on top of an existing webpage. Pop-up builder software also lets users make pop-ups more personal. They can target specific groups or send pop-up messages based on what users do.
* This category also includes *form builder* apps, which are used to create, personalize, and deploy forms on a website.
* **CRM:** these apps are used to efficiently organize, monitor, and support data about existing and prospective customers. The software centralizes data from various sources to create records. The software has a repository of a complete customer database, which stakeholders use to manage long-term customer contracts and relationships.
* **Sensitive Data:** these apps are used to locate sensitive data—such as personally identifiable information (PII), protected health information (PHI), payment card industry (PCI) data, intellectual property (IP), and other important business data—stored across multiple company systems, including databases and applications, as well as on user endpoints.
* **SMS:** SMS apps, also known as business text messaging apps, enable companies to plan and implement marketing campaigns that target mobile devices via SMS (Short Message Service)
* **Spreadsheets:** these apps organize, catalog, and keep your data in easy-to-understand charts and graphs. They also organize data that can be shared for real-time collaboration, undergo further analysis, or turned into visual representations
* **Accounting:** these apps are designed to manage financial transactions, expenses, income, and cash flows within an organization. They automate account payables and receivables, journal entries, ledgers, and financial statements for accounting and finance, HR, payroll processing, annuity, investment, and budget forecasting processes within different organizations.
* **Ecommerce:** these apps provide comprehensive software that allows organizations to manage all operations related to online sales of products or services
* **Payments:** these apps are used to process multiple types of business-to-business (B2B) payments. Companies use these apps to manage payments received from business customers and made to suppliers.
* **Subscription Management:** these apps track all activities related to the sale of subscription-based products.
* **Event Management:** these apps are used to make event planning easier. Apps in this category manage all aspects of an event from beginning to end. This can include creating an event website, collecting registrations, and more.
* **Social Networks:** these apps allow individuals and companies to connect with one another to communicate and share data, often in a public forum.
* **Account-based marketing:** these apps help marketing and sales departments work together by identifying good target accounts before putting in place a marketing plan that is specific to each account. These apps automate and reduce the process of identifying prospects and dedicating the right resources to nurturing the most promising accounts.
* **Session Replays:** these apps record and playback a user's session when they use a website or mobile app. This helps them understand how the user uses the app better. They also show how visitors use a company's website or mobile app. This shows any problems, errors, or confusing moments they may have.
* **Advertising:** these apps allow companies to buy, manage, and place display advertisements on websites, including banner, overlay, and rich media ads.
* **Social Media Management:** these apps provide the functionality to administer social media accounts, schedule posts, suggest content, and boost posts
* **Video:** these apps are used to edit and modify video content. This includes converting formats, editing files, adding effects, and more.
* **CPQ:** Configure, price, quote (CPQ) apps automate the quoting and proposal process, starting with the moment a customer supplies their needs in a company’s offering and ending with sending a detailed quote to the customer or prospect.
* **Conversation Intelligence:** these apps record, transcribe, and analyze sales calls. CI apps can be used to take notes on key conversations with potential buyers, identify risky or noncompliant topics of conversation, coach new sales representatives on best practices, and more.
* **Proposals:** these apps streamline and automate the proposal and request for proposal (RFP) process for sales operations. Use these apps to quickly generate documents in multiple file formats, share documents through multiple channels, and track the impact of RFP and proposal documents on sales success
* **Sales Compensation:** these apps automate the accounting and administration of commissions and incentive plans based on customizable rules such as employee role, tenure, or sale type. The software also provide salespeople with a detailed look into past earnings and forecasted revenue.
* **Sales Enablement:** these apps store marketing materials and sales content. They give sales representatives timely, productive, and useful materials during all parts of the selling process. Sales enablement apps also ensure that any sales representative can find the appropriate content, submit it to prospects, and track prospect engagement within that content.
* **Competitive Intelligence:** these apps enable businesses to capture, analyze, and take action on their competitive landscape. These apps can provide insight into advertising strategy, pricing changes, product additions, and more.
* **E-Signature:** these apps let users sign on documents shared online. This eliminates the need for physical documents to record signatures. Use e-signature apps to protect documents, like sales contracts or employment paperwork.
* **Lead Scoring and Routing:** these apps are used by companies to determine the potential of each business opportunity.
* By using this type of software, companies can create scales and benchmarks to rank prospects against. These apps can help teams focus on those lead opportunities that are most likely to convert into sales.
* Lead routing apps automatically qualify and distribute leads to the right sales rep or team. They use routing rules based on criteria like a rep's territory or experience.
* **Sales Intelligence:** these apps use companies’ internal and external data to increase sales and improve sales processes. Sales intelligence apps improve the quality and quantity of sales leads by using B2B contact databases to find new opportunities and salespeople with the information they need, including contact information, job titles, and firmographics. Some apps also offer buying signals or additional insights, such as recent funding, transfer of companies, and more.
* **Outbound calling apps:** use these apps to call leads directly, record the call, and prospect data. These apps usually have features that let you click to call quickly. Some apps can record calls for training purposes and sort prospects based on the probability of a successful sale.
* **Inbound calling apps:** use these apps to attribute incoming phone calls to their respective sources by generating unique, local, and toll-free numbers for advertisements, website locations, and more. These apps often go beyond basic tracking by offering advanced call recording, monitoring, routing, and interactive voice response systems to qualify leads and provide more granular reporting.
* **Customer success:** these apps track customer behavior, preferences, and usage patterns, to allow agents to coordinate their success planning with greater accuracy and prevent the likelihood of churn. Some customer success app also uses detailed analysis of past behavior to create a "health score" to predict a customer's future satisfaction.
* **Digital platform Adoption:** A digital adoption (DAP) app is a software layer integrated on top of another software application or website to guide users through tasks and functions. Digital adoption platforms aim to help new users quickly learn how to interact with a website or application or assist returning users in learning newly added functionality.
* **ERP:** Enterprise resource planning (ERP) apps manage, control, and organize daily business operations and workflows. Employees in production, manufacturing, accounting, finance, HR, and supply chain use ERP systems to support process automation, control input data, and optimize business operations to save resources.
* **Experience Management:** these apps consolidate feedback from a specific, targeted audience and deliver actionable insights and follow-up steps to close the loop. Specifically, these apps deploy analytics dashboards for feedback data that is viewable by stakeholders across an organization.
* **Field Service:** Field service management (FSM) apps help companies manage field-based workers by optimizing their positioning, availability, and skills as labor resources. These apps are primarily used by companies that provide on-site service and technical expertise such as equipment maintenance, delivery, and more.
* **Learning Management System:** these apps help companies organize, track, and manage efforts to train employees, customers, and other external partners. LMS apps are used to manage individualized training programs for onboarding, development, and compliance training purposes. Companies use a LMS to assign courses to employees or external end users, then track learners’ progress as they complete course lessons and assessments. These courses can be created using built-in tools in the LMS or a separate course authoring software.
* **Live Chat:** companies use these apps to communicate with their website visitors in real time via chat windows. Customer service representatives can utilize live chat apps to provide support to users who have questions regarding products or website navigation. Some other features include reporting and analytics, interactive chat notifications, and conversation archiving.
* **Workforce Management:** organizations use these apps to plan, manage, and track employee work, including labor requirements, employee schedules, and paid time off (PTO). These apps are also used to forecast labor demand, create and assign employee schedules, track attendance, and report on workforce efficiency.
* **Sales Engagement:** these apps streamline the sales process, combining their sales and marketing efforts to create personalized and automated sales journeys; these can include emails, calls, social posts, meetings, and text messages.
* **Email:** these apps send electronic mail from one user to another. This category also includes email client apps that manage a user’s email account or accounts through a desktop application. Email clients work similarly to webmail email managers provided by email apps available via browser, but are instead accessible through a downloaded program.
* **Direct Mail Automation:** these apps automate the process of sending electronic or physical letters, packages, and gifts. Marketers often use direct mail automation to track and target their campaigns.
* **Workflow Automation:** these apps are used to automate workflows to route tasks and information between people and systems based on predefined rules and triggers.
* **Marketing Automation:** these apps use software to automate repetitive marketing tasks, which can increase efficiency and free up time for other projects.
* **SEO:** Search engine optimization (SEO) apps, or organic search marketing software, are designed to provide information on how websites are ranked in search engines. These tools provide valuable insights for optimizing content and improving rankings.
* **Scheduling:** There are 2 types of scheduling apps:
* **Online appointment scheduling:** these apps provide customers with a portal to book an appointment online and enables businesses to track and manage those appointments.
* **Business scheduling :** these apps allows users to automatically sync multiple calendars to find shared availability without exposing individual calendars and compromising privacy.
* **Data Migration:** these apps are used to move user data from one system to another.
* **Data Quality and Backup:** these apps analyze sets of information and identify incorrect, incomplete, or improperly formatted data. After profiling data concerns, data quality apps cleanse or correct that data based on guidelines. Data backup apps create copies of important data and files to prevent data loss by corruption or infection by malware. This category also includes other kinds of data apps such as data enrichment apps, address verification apps, and more.
* **Product Analytics:** these apps provide companies visibility into user behavior by tracking and analyzing their interactions with a product.
* **Sales Analytics:** these apps report on CRM data to reveal sales insights and forecast future performance. Sales teams and managers use sales analytics to gain visibility into sales activities; locate high or under-performing salespeople, products, or communications; and forecast future sales numbers.
* **Marketing Analytics:** these apps encompass tools and processes which enable an organization to manage, evaluate, and control its marketing efforts by measuring marketing performance.
* **Connector:** these apps allow users to connect their HubSpot portal to different platforms and services.
* **Ticketing:** these apps help companies manage their incoming customer support tickets.
* **Surveys:** these apps allow users to create online surveys, quizzes, polls, and other web forms.
* **Help Desk:** these apps are used to organize, manage, and respond to service-related requests from internal and external sources. Customer inquiries are typically submitted via multiple channels, including email, phone, or social media.
* **Knowledge Base:** these apps store and organize information about businesses and their products, services, and processes in a central repository accessible by the rest of the organization.
* **Fundraising and Non Profit:** fundraising apps are used by nonprofit organizations to manage funding processes. Its main goal is to attract and retain donors, and to ensure their loyalty and continuous financing. Non profit apps are designed to help nonprofit and charity organizations meet the specific business needs of not for profit operations.
* **Applicant Tracking Systems:** these apps are used by recruiters, HR teams, and hiring managers use to source, screen, and manage job applicants for open positions.
* Companies use these apps to create and distribute job postings, parse resumes for relevant information, schedule interviews, and source candidate information, cover letters, and references.
* This category also includes recruiting apps, which facilitate the hiring and onboarding of new talent through tools that create internal and external candidate pools, produce and distribute job postings, and include applicant tracking software (ATS), onboarding, and analytics.
* **Consent Management:** companies use these apps to legally document and manage a user’s consent choices before collecting, sharing, or selling user data from online sources such as websites and apps that use cookies, embedded videos, and other tracking technologies.
# MCP server listing requirements
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/mcp-server-listing-requirements
Review the additional listing requirements for apps that include an MCP server component on the HubSpot Agent Marketplace.
Apps with an MCP server component are subject to additional Ecosystem Quality (EQ) listing requirements beyond the standard [HubSpot Marketplace listing requirements](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements). This page covers those requirements, as well as how to [submit your MCP component for approval](#submitting-your-mcp-component-for-approval).
## Listing requirements
### Security
* **`mcpUrl` format:** the `mcpUrl` value must use HTTPS, must not embed secrets or credentials (including no token-in-URL or equivalent anti-patterns), and must point to a spec-compliant MCP server that supports SSE and/or HTTP streamable transport.
* **Scopes alignment:** the scopes and permissions implied by the MCP component must align with the parent app's required scopes configuration.
* **End-to-end access:** the MCP server must support successful authenticated access to the third-party platform it wraps, including token refresh, and must expose at least one tool invocable end-to-end with real data.
* **Security risk:** the MCP server must present low security risk for data exfiltration and tool poisoning, with no unresolved critical or high vulnerabilities at the time of review.
* **Server ownership:** the `mcpUrl` must resolve to a verified, official server owned by or explicitly delegated to the named platform. Third-party connectors (for example, a partner building a connector to another company's MCP server) are not permitted at this time.
### Privacy & compliance
* **Required URLs:** `websiteUrl` and `privacyPolicyUrl` must be present, valid, and clearly associated with the MCP server's owning platform.
* **Sensitive data:** apps approved for access to [sensitive data scopes](/docs/api-reference/latest/crm/properties/sensitive-data) may not include an MCP component.
* **EU AI Act compliance:** MCP components must not support "Unacceptable risk" or "High risk" AI use cases under the EU AI Act. This is the same standard applied to agent tools.
### Reliability & testing
* **Demo video:** you must provide a short demo video showing MCP component configuration, the connection test flow, and at least one successful tool invocation within Breeze.
### Usability & labeling
* **Component name:** the component `name` must clearly identify the external platform and integration purpose. For example, "Acme MCP" is acceptable; "MCP Server" is not.
* **Component description:** the `description` must be clear, succinct, and non-marketing. Explain what the integration enables in HubSpot rather than restating the name.
* **Ownership clarity:** the owning party must be clearly inferable from the component metadata. The component must not suggest it is HubSpot-built when it is partner-built.
### MCP listing fields
For apps that include an MCP app component, the unified app listing will surface MCP-specific metadata on the functionality card via two fields defined in the project configuration: `mcpUseCases` and `mcpTools`.
#### mcpUseCases
Every app with an MCP component must define between one and five `mcpUseCases` entries. Each entry must:
* Describe in plain-language a concrete front-office use case, understandable to end users (e.g., "Summarize post-call notes from `` into HubSpot records").
* Avoid internal jargon, bare feature names (e.g., just "CRM sync"), or purely marketing slogans. They should describe what a customer can accomplish, not only how the integration works.
* Not describe clearly prohibited or high-risk AI use cases (e.g., social scoring, biometric surveillance, employment decisioning) and must not position processing of HubSpot-classified sensitive data as the primary purpose of the MCP integration.
* Be truthful and directionally accurate. They may summarize multiple MCP tools at a high level but cannot materially misrepresent what the MCP server can do (e.g., claiming automation or data access that does not exist).
#### mcpTools
Every app with an MCP component must define at least one `mcpTools` entry as follows:
* `name`: must be a machine-style identifier with no spaces, matching the MCP server function name exactly (e.g., `fetch_tasks`, not "Fetch Tasks").
* `description`: must be clear, concise, and in plain language describe what the tool does from a HubSpot user perspective (e.g., "Get a list of Supered tasks associated with a HubSpot contact"). It must not restate the name or read as marketing copy. In addition, the description must not advertise or normalize clearly disallowed high-risk AI use cases or encourage direct handling of sensitive data as a primary behavior.
* `accessType`: must accurately reflect what the tool can do with HubSpot or external data: any tool that can create, update, or delete data must be marked `"write"`. `"readOnly"` is reserved for tools that never mutate data. When in doubt, use `"write"`.
## Submitting your MCP component for approval
To submit your app for review, follow the [HubSpot Marketplace listing steps](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app).
After receiving your submission, the HubSpot Ecosystem Quality team will review your component and share initial feedback within 10 business days. Please address this feedback promptly, as additional review rounds may be required before final approval.
# Measure app performance
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/measure-app-performance
Learn more about measuring your app's performance through the app listing page details view.
After [listing your app](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) in the [HubSpot Marketplace](https://ecosystem.hubspot.com/marketplace/apps), you can review performance metrics for your app. This includes usage data, listing analytics, and customer feedback.
## Review app performance
In *Marketplace Analytics*, you can review reporting on install, listing page, and search performance.
### Analyze installs
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **App Listings**.
* Click the **name** of your app.
* To change how the data is grouped, click the **Frequency** dropdown menu in the top left and select a **frequency**.
* To set a general date range, click the **first date picker** in the *Date range* section and select a **general date range** in the left column.
* To use a specific date range:
* In the *Date range* section at the top of the page, click the **first date picker** and select a **start date**.
* Click the **second date picker** and select an **end date**.
* Review the following install data:
* Active Installs
* App Installs
* App Uninstalls
* Free vs Paid Installs
* Free vs Paid Uninstalls
* Installs by Country
* Installs by Hub
### Analyze listing page performance
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **App Listings**.
* Click the **name** of your app.
* In the left sidebar, navigate to **Listing analytics**.
* To change how the data is grouped, click the **Frequency** dropdown menu in the top left and select a **frequency**.
* To set a general date range, click the **first date picker** in the *Date range* section and select a **general date range** in the left column.
* To use a specific date range:
* In the *Date range* section at the top of the page, click the **first date picker** and select a **start date**.
* Click the **second date picker** and select an **end date**.
* To view performance for a specific localized listing page, click the **Language** dropdown menu and select a **language**.
* Review the following data on the visitors viewing your listing page:
* **Pageviews**: number of pageviews plotted by frequency.
* **Pageview Source**: percentage of pageviews attributed to a specific source, such as public or in-app.
* **CTA Usage**: number of clicks of your listing page CTAs, plotted by frequency.
* **Free vs Paid Pageviews**: percentage of pageviews attributed to paid and free users.
* **Pageviews by Country**: number of pageviews associated with specific countries.
* **Pageviews by Hub**: number of pageviews from users in specific hubs.
* **Pageviews by Domain**: number of pageviews from users associated with a particular company domain. You can see the following info for these pageviews:
* **Domain name**: company domain associated with the user who viewed the page.
* **Installed**: whether an account associated with that domain has installed the app.
* **First seen**: first date that any user associated with this domain viewed the page.
* **Last seen**: most recent date that any user associated with this domain viewed the page.
* **Pageview count**: total number of times users associated with this domain viewed the page.
### Analyze search result performance
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **App Listings**.
* Click the **name** of your app.
* In the left sidebar, navigate to **Search analytics**.
* To change how the data is grouped, click the **Frequency** dropdown menu in the top left and select a **frequency**.
* To set a general date range, click the **first date picker** in the *Date range* section and select a **general date range** in the left column.
* To use a specific date range:
* In the *Date range* section at the top of the page, click the **first date picker** and select a **start date**.
* Click the **second date picker** and select an **end date**.
* Review the following data:
* **Impressions vs Clicks**: number of times your app listing has appeared in search results plotted alongside the number of times visitors have clicked that search result.
* **Top search terms**: search terms that returned your app as a result. For each search term, you can see impressions (number of times it appeared in search results), clicks (number of times users clicked on your listing from search results), and its average position in search results (lower numbers are better).
### Analyze app performance using Breeze Assistant
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **App Listings**.
* Click the **name** of your app.
* In the left sidebar, navigate to **Breeze Assistant**.
* In the right panel, click a **suggested prompt** or enter a **custom prompt** in the field. Learn more about [using Breeze Assistant](https://knowledge.hubspot.com/ai/use-breeze-assistant).
## Customer Feedback
To view all reviews that customers have submitted for your app, click the **Customer Feedback** tab.
In the *Ratings & Reviews* section, you can filter the reviews by the number of stars given, the customer's industry, and the customer's company size.
To reply to a review:
* Below the review, click **Reply**.
* In the dialog box, enter your **reply** to the customer, then click **Send**. This reply will be publicly visible on your app listing.
* To edit notifications for reviews received:
* In the top right, click the **Actions** dropdown menu and click **Edit notifications**.
* On the *Notifications* page, search for **Marketplace** and select **Listing New Reviews** to get notified when a customer leaves a review.
* To send your review link to a customer:
* In the top right, click the **Actions** dropdown menu and click **Copy review link**.
* Email or send this link to your customer for them to leave a review.
* To review all survey responses:
* In the left sidebar menu, click the **Uninstall Survey Responses** tab.
* To export the responses, in the top left, click **Export**. In the dialog box, click the **File format** dropdown menu and select **XLSX**, **XLS**, or **CSV.**. Then, click **Export**.
* To review any private feedback responses to your team, in the left sidebar menu, click the **Private Feedback** tab.
## Key Resources
In the *Key Resources* section, review the following resources:
* **Technology partner program benefits:** the *Technology Partner Program Benefits* guide lists all benefits that you get as a partner with a listed app on HubSpot Marketplace.
* **Feedback survey:** a feedback survey to provide feedback about the technology partner experience.
* **Technology partner manager:** the contact information for your technology partner manager.
## App Activity & Usage
In the *App Activity & Usage* section, you can review usage data for your app:
* In the *Installation* report, you can view how many active installs your app has, measured by how many accounts have generated a successful API call in the past rolling 30 day period.
* In the *Top portals* report, click **View top portals** to see the accounts that have the highest usage of your app.
* On the right, under *Feature usage*, you can measure the usage of different actions and API requests over time:
* To include an additional action, click the **dropdown menu** at the top of the report, then select an **action**.
* To remove an action, click the **x** to the right of the action at the top of the report.
* To change the time period, click the **dropdown menu** at the top right of the report, then select a **time period**.
* Under the *Feature usage* report, you can also review recent errors that've occurred:
* To change the time period, click the **dropdown menu** at the top right, then select a **time period**.
* Errors are categorized into the following types:
* **Client errors**: issues usually caused by a malformed request from your app.
* **Server errors:** issues from processing a request on the backend or a problem on HubSpot's backend.
* **Unauthorized errors:** authentication or missing scope issues.
* **Rate limit errors:** caused by making too many requests in a given time period.
## Measure engagement with UTM parameters
In addition to the above metrics, you can further measure engagement by including UTM parameters in the URLs on your app listing page. This lets you view how much traffic is coming to your website from your listing page.
It's recommended to add UTM parameters to the following URLs included on your listing page:
* Supporting content on your HubSpot Marketplace listing page, such as your company website, supporting documentation, case study, and privacy policy.
* The OAuth install URL that customers use when installing the app.
For example, you could add the following string of UTM parameters to the end of your documentation URL:
`?utm_campaign=appmarketplacelisting&utm_medium=referral&utm_source=hubspot`
**Please note:**
It's recommended to use UTM parameters that are consistent with other UTM tracking you or your marketing team may be using. Learn more about [creating tracking URLs](https://knowledge.hubspot.com/settings/how-do-i-create-a-tracking-url).
To add UTM parameters to the OAuth install URL:
* In your app developer account, navigate to **Apps**.
* Click the **name** of the app to edit its details.
* In the left sidebar, navigate to **Basic info**.
* At the top, click the **Auth** tab.
* In the *Redirect URLs* section, update your redirect URL to contain your UTM parameters. This will update the app's install URL after saving, so you'll need to be sure you've updated any user-facing links to use the new URL.
* In the bottom left, click **Save**.
**Please note:**
The install button URL field has a limit of 250 characters. If your UTM parameters result in exceeding that limit, you may need to use a url shortener, such as [Bitly](https://bitly.com/).
To add UTM parameters to the app's supporting content:
* In your app developer account, navigate to **Development**.
* In the left sidebar menu, click **App Listings**.
* In the *Marketplace Listings* table, click the **name** of the app.
* In the top right, click **Edit listing**.
* On the *Listing info* tab, update the URL in the *Install button URL* field with your UTM parameters.
* At the top, click the **Support info** tab.
* In the *Contact info*, *Support resources*, and *Terms of Service and Privacy Policy* sections, update the URLs with your UTM parameters.
* Once you've updated your URLs, click **Submit for review** in the top right corner.
* Once reviewed and approved, the URLs on your app's listing page will be updated with your UTM parameters. You can then use analytics tools, such as [HubSpot](https://knowledge.hubspot.com/reports/analyze-your-site-traffic-with-the-traffic-analytics-tool) or Google Analytics, to view traffic coming from your URLs as categorized by your UTM parameters.
## Related docs
# Set up demo scheduling from the app listing page
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/set-up-demo-scheduling-from-the-app-listing-page
Learn how to let contacts submit a form or schedule a meeting from your app listing page.
When setting up your app listing page, you can add a scheduling page and/or form to let potential customers schedule a demo with you.
This feature is only available for accounts building apps on the [latest two versions](/docs/apps/developer-platform/overview) of the developer platform (`2025.2` and `2026.03`).
## Add a scheduling page and/or form to an app listing
* In your developer account, navigate to **Marketplace** > **App Listings**.
* Hover over a listing and click **More**, then select **Edit**. Learn more about [creating an app listing](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app).
* On the *Listing Info* tab, add options for scheduling a demo:
* To add a meetings link, click the **Add meetings URL** dropdown menu and select a **scheduling page**. To create a new scheduling page, click **Create meetings link**. Learn more about working with [scheduling pages](https://knowledge.hubspot.com/meetings-tool/create-and-edit-scheduling-pages).
* To add a form, click the **Add contact form** dropdown menu and select a **form**. To create a new form, click **Create contact form**. Learn more about [working with contact forms](https://knowledge.hubspot.com/forms/create-and-edit-forms).
* In the top right, click **Validate & submit**.
Once your changes have been approved, your customers will see an option in the top right of your app listing to schedule a demo or request to be contacted. If you've added a scheduling page and a contact form, both options will be available in the *Schedule demo* dropdown menu.
# HubSpot Marketplace | Provide testing credentials for your app
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/testing-credentials
Follow these steps to provide testing credentials for your app when listing or certifying it on HubSpot Marketplace.
Valid testing credentials must be provided to [list](/docs/apps/developer-platform/list-apps/listing-your-app/listing-your-app) or [certify](/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification) your app on HubSpot Marketplace. This is necessary for the HubSpot Product team to verify listing and certification requirements, and also recommend areas for improvement.
## Before you begin
Review the following questions to find the appropriate instructions for providing testing credentials to the HubSpot Product team:
1. Does your app with HubSpot require an account for a platform/application/service other than HubSpot?
* If **yes**, (i.e. at least one other account is required), proceed to question 2.
* If **no**, follow the steps listed [here](#does-not-require-account).
2. Are you able to give HubSpot Product team members access to all platforms/applications/services required to use your app?
* If **yes**, (i.e. you are able to give HubSpot Product team members access to needed platforms/applications/services), follow the steps listed [here](#able-to-invite-teams).
* If **no**, (i.e. you are not able to give HubSpot Product team members access to at least one platform/application/service), follow the steps listed [here](#unable-to-give-access).
## Provide your testing credentials
**If your app does not require a separate platform/application/service account:**
1. In the first line, enter “No testing credentials are required to test this app.”
2. Describe the steps to install the app, configure app settings, and perform common actions with the app. If these instructions are publicly documented, you may hyperlink to the relevant documentation.
**If your app requires a separate platform/application/service account and you are able to provide the HubSpot Product team with access:**
1. In the first line, list all platforms/applications/services required to fully use your app/integration.
2. For each required platform/application/service that you are able to give access to:
* Invite [marketplace-tester@hubspot.com](mailto:marketplace-tester@hubspot.com) to all new or existing accounts. Ensure that these accounts have all features and permissions required to fully use your app with HubSpot.
* Include “Invite sent to [marketplace-tester@hubspot.com](mailto:marketplace-tester@hubspot.com)” next to the platform you’ve added that user to. **Do not** ask the HubSpot Product team to create new accounts or sign up for free trials.
3. Describe the steps to install the app, configure app settings, and perform common actions with the app. If these instructions are publicly documented, you may hyperlink to the relevant documentation.
**If your app requires a separate platform/application/service account and you are not able to provide the HubSpot Product team with access:**
1. In the first line, list all platforms/applications/services required to fully use your app/integration.
2. For each required platform/application/service that you are able to give access to:
* Invite [marketplace-tester@hubspot.com](mailto:marketplace-tester@hubspot.com) to all new or existing accounts. Ensure that these accounts have all features and permissions required to fully use your app with HubSpot.
* Include “Invite sent to [marketplace-tester@hubspot.com](mailto:marketplace-tester@hubspot.com)” next to the platform you’ve added that user to. **Do not** ask the HubSpot Product team to create new accounts or sign up for free trials.
3. For each platform/application/service that you are not able to give access to:
* Include an explanation on why you're unable to give the HubSpot Product team access.
4. Describe the steps to install the app, configure app settings, and perform common actions with the app. If these instructions are publicly documented, you may hyperlink to the relevant documentation.
5. Record a demo video that shows the app/integration performing actions with all required platforms, applications, or services. The demo video must meet the requirements listed [here](/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification#app-demo) (even if you are not applying for app certification).
## Frequently Asked Questions
You may connect your app to a non-product HubSpot account. However, the HubSpot Product team reserves the right to disconnect your account and connect theirs for testing.
Credentials will be stored in a HubSpot-build admin tool and an enterprise-grade privileged access management (PAM) solution which is approved and managed by HubSpot's Security team.
Please do not deactivate the testing credentials. If this is not possible, please notify the HubSpot Product team when the listing or certification review is complete.
Yes, you will be required to provide testing credentials for your app if you apply for certification. You may reuse the same account/user or invite [marketplace-tester@hubspot.com](mailto:marketplace-tester@hubspot.com) if all features and permissions to fully use your HubSpot app remain available.
# Understand app installation flow with the option for partner sign in
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/understand-app-install-flow
Review the updated installation flow of apps with and without partner sign in features turned on.
After October 26, 2026, all apps listed on the HubSpot Marketplace that wish to make changes to the app listing will be required to use an installation flow that includes an option to authenticate using [OAuth](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth) without a separate partner sign in. You can choose to opt your app into this flow early, but once you do so, that app will be required to use that flow moving forward.
If installing your app requires the user to log in to an account in your system, it's recommended to authenticate with partner sign in. If you don't need the user to authenticate beyond using OAuth, you can authenticate without partner sign in.
## Understand limits and considerations
The updated install flow only applies to:
* Apps that have opted into using the updated install flow early. After October 26, 2026, it will apply to all apps that make changes in the listing editor.
* Apps listed on the HubSpot Marketplace.
* Customers installing the app for the first time. Existing customers who have already installed the app are not affected.
* Installs initiated from the HubSpot Marketplace. Installs initiated from external websites or the app itself are not affected.
* The option of whether customers need to sign in with an external system to authenticate. OAuth authentication itself is not affected.
* The app's installation process once you publish changes in the listing editor. Until then, the app's current installation process will not change.
## Opt into the updated install flow early
You can choose to switch to the updated install flow from the listing editor. Once in the new flow, you can specify a redirect URL and whether the app will require partner sign in.
1. In your HubSpot account, navigate to **Development**. In the left sidebar menu, click **App Listings**.
2. Hover over an app, then click the **More** dropdown menu and select **Edit draft**.
3. In the *Listing info* section, click **Enable seamless install flow**.
4. In the dialog box, select the **checkbox** that shows you understand you can't return to the previous install flow, then click **Enable seamless install**.
5. In the *Listing info* section of the listing editor, click the **Install button URL** dropdown menu and select a **URL**.
6. In the *Sign-in configuration* section, select whether the app will include partner sign in.
## Understand the install flow without partner sign in
### Developer perspective
Your install URL endpoint receives a request with these parameters:
* `code`: authorization code for completing the installation.
* `returnUrl`: URL used to direct the user back to HubSpot after installation completes. The `returnUrl` will be added as a query parameter to the end of the Redirect URL selected in the listing editor when HubSpot redirects the customer there.
* `step`: always set to `finalize`. You do not need to do anything with this parameter.
Once you receive the request, you should:
* Get the `code` and `returnUrl` from the URL parameters.
```javascript theme={null}
// Helper function
function getQueryParam(param) {
const params = new URLSearchParams(window.location.search);
return params.get(param);
}
const code = getQueryParam('code');
const returnUrl = getQueryParam('returnUrl');
```
* Use the provided `code` to [complete token exchange for OAuth](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth).
* Required and optional scopes are already included in the install URL. If you need to use conditional scopes, you'll need to initiate a separate secondary install flow.
* Store any necessary configuration data associated with the user's HubSpot account.
Redirect the user back to the `returnUrl` provided in the installation request. Redirecting back to HubSpot is required to avoid an infinite login loop.
To avoid issues with the install flow, ensure that the endpoint handling the install flow can be framed by HubSpot in an embedded browser context.
For example, if you use a Content Security Policy, it should allow HubSpot as a frame ancestor:
`Content-Security-Policy: frame-ancestors 'self' https://app.hubspot.com https://app-eu1.hubspot.com;`
### Customer perspective
1. In your HubSpot account, click the **marketplace icon** in the top navigation bar, then select **HubSpot Marketplace**.
2. Click an **app card**.
3. In the top left, click **Install**.
4. In the dialog box, review the app's requirements, then select the **checkbox** and click **Connect app**.
5. Once the app is installed, you can start using the app:
* Click **Explore app features** to start from the Feature Discovery section of the app overview page in your Connected Apps settings.
* Click **Customize app cards** to start customizing the app's [app cards](https://knowledge.hubspot.com/integrations/install-and-manage-app-cards).
* Click the **X** in the top right to return to the app listing page.
## Understand the install flow with partner sign in
### Developer perspective
Your install URL endpoint receives a request with these parameters:
* `step=authorize`: indication that this is the initial step in the installation process.
* `returnUrl`: URL used to direct the user back to HubSpot after the authentication process completes. The `returnUrl` will be added as a query parameter to the end of the Redirect URL selected in the listing editor when we redirect the customer there.
* Example URL:
```
https://www.myinstallserver.com/install?returnUrl=https://hubspotreturnurl/install-success&step=authorize
```
* Get the `step` and `returnUrl` from the URL parameters, then show a login form or page to authenticate the user.
```javascript theme={null}
// Helper function
function getQueryParam(param) {
const params = new URLSearchParams(window.location.search);
return params.get(param);
}
const step = getQueryParam('step');
const returnUrl = getQueryParam('returnUrl');
```
Once the user has authenticated, you should:
* Generate a cryptographically secure, randomized token unique to this user. This is the `state` token used in future steps.
* For example:
```javascript theme={null}
function generateStateParameter() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
const state = generateStateParameter();
```
* One option is to create a data table without RLS that stores the user's `uid` from your system and the `state` token.
* If you are using cookies, tag the cookies with *SameSite=none*.
* For security, it's recommended to have a `state` token with a relatively short expiration window, such as 10 minutes.
* Required and optional scopes are already present in the install URL.
* If you need to specify conditional scopes, it's recommended to include them in the `scope` query parameter. You can add scopes to this parameter as a list of scope names, separated by spaces.
* Add the `state` token you generated in the previous step to the `returnUrl` as a query parameter, as well as the `scope` parameter if you're using it, then redirect the user back to HubSpot. The redirect will look like this: `${returnUrl}?state=${state}`. Redirecting back to HubSpot is necessary to avoid an infinite login loop.
* For example:
```javascript theme={null}
const returnUrlObj = new URL(returnUrl);
// Set state token and conditional scopes
const scopes = [
'crm.objects.contacts.write',
'crm.objects.companies.read',
'crm.objects.companies.write'
]
returnUrlObj.searchParams.set('state', state);
returnUrlObj.searchParams.set('scope', scopes.join(' '));
// Redirect back to HubSpot
window.location.href = returnUrlObj.toString();
// e.g. returnUrlObj.toString() = "https://www.hubspotReturnUrl.com?state=123abc"
// or returnUrlObj.toString() = "https://www.hubspotReturnUrl.com?someHubSpotParam=returnUrlParam&state=123abc"
```
Your install URL endpoint receives a request with these parameters:
* `step=finalize`: indication that this is the final step in the installation process.
* `code`: the OAuth code HubSpot uses to generate your tokens.
* `state`: the secure token you generated in Step 3.
* `returnUrl`: URL used to direct the user back to HubSpot after the authentication process completes.
For example:
```
https://www.myinstallserver.com/install?code=123&state=30q94q3043&returnUrl=https://hubspotreturnurl/install-success&step=finalize
```
* Get the `step`, `code`, `state`, and `returnUrl` parameters from the URL.
```javascript theme={null}
// Helper function
function getQueryParam(param) {
const params = new URLSearchParams(window.location.search);
return params.get(param);
}
const step = getQueryParam('step');
const code = getQueryParam('code');
const state = getQueryParam('state');
const returnUrl = getQueryParam('returnUrl');
```
* Validate that the `state` token matches the original authentication request.
* Retrieve the associated user account.
* If you are able to verify the `state` token, complete the installation:
* Exchange the `code` for [OAuth access and refresh tokens](/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth).
* Redirect the customer to the `returnUrl`. Without this step, the user will be stuck in an infinite login loop.
* If you are not able to verify the `state` token, do not complete the installation.
* Redirect the customer to the `returnUrl`. Without this step, the user will be stuck in an infinite login loop.
To avoid issues with the install flow, ensure that the endpoint handling the install flow can be framed by HubSpot in an embedded browser context.
For example, if you use a Content Security Policy, it should allow HubSpot as a frame ancestor:
`Content-Security-Policy: frame-ancestors 'self' https://app.hubspot.com https://app-eu1.hubspot.com;`
### Customer perspective
1. In your HubSpot account, click the **marketplace icon** in the top navigation bar, then select **HubSpot Marketplace**.
2. Click an **app card**.
3. In the top left, click **Install**.
4. In the dialog box, click **Sign in** to sign up or log in to the app.

5. In the new window, finish the app's sign up / log in process externally.
6. After being redirected back to HubSpot, review the app's requirements, then select the **checkbox** and click **Connect app**.
7. Once the app is installed, you can start using the app:
* Click **Explore app features** to start from the Feature Discovery section of the app overview page in your *Connected Apps* settings.
* Click **Customize app cards** to start customizing the app's [app cards](https://knowledge.hubspot.com/integrations/install-and-manage-app-cards).
* Click the **X** in the top right to return the app listing page.
## Preview the install flow
Once you have opted into using the new install flow, you can test the installation process from the listing editor before publishing your changes:
1. In your HubSpot account, navigate to **Development**. In the left sidebar menu, click **App Listings**.
2. Hover over an app, then click the **More** dropdown menu and select **Edit draft**.
3. In the top right, click **Preview**.
4. Run through a test version of the installation process.
## Request an exemption
If you need to update your app listing but your app cannot support self-service installation due to complex technical onboarding or specific legal constraints, you can file an exemption request. These requests are reviewed individually by HubSpot's Eco Quality team, typically within 7-10 business days.
If the request is approved, this would allow your app to replace the mandatory *Install App* button with an option to contact you or book a meeting. If the request is denied, the app will have to use the *Install App* button required by the new install flow.
1. [Opt into the updated install flow](#opt-into-the-updated-install-flow-early).
2. In your HubSpot account, navigate to **Development**. In the left sidebar menu, click **App Listings**.
3. Hover over an app, then click the **More** dropdown menu and select **Edit draft**.
4. In the listing editor, click **Request removal** in the *Request to remove install button from your listing* section.
5. In the exemption request form, enter your name, email address, and company name, then click **Next**.
6. Enter your **Production app ID**.
7. Select an **app listing status**:
* **Listed**: your app listing is live on the *HubSpot Marketplace*.
* **Draft**: your app listing has been drafted, but isn't yet live.
8. Enter your **App Name**, then click **Next**.
9. Click the **Reason for Exemption** dropdown menu and select a **reason**.
10. In the *Exemption Context* field, enter an **explanation** for why your app cannot use an install button. It's recommended to provide as many details as possible to ensure the best chance of your request being approved.
11. Enter any questions or additional details you want to provide.
12. If you want to receive communications from HubSpot about products and services, select the **I agree to receive other communications** checkbox.
13. Select the **checkbox** to consent to your personal data being processed. This is a requirement for submitting an exemption request.
14. Submit the form.
# Understand technology partner tiers
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/understand-technology-partner-tiers
Understand the Technology Partner Program and how to view your current tier information.
Technology partner tiers allow customers to identify app developers who have consistently provided a good experience for HubSpot customers in the past. Tier is applied to the app developer, not the app, so all apps from the same creator will have the same tier.
The following technology partner tiers are available:
* Partner
* Rising
* Leading
* Premier
Customers will see your technology partner tier on your app listing pages and when they filter for tier in the HubSpot Marketplace.
Learn the full details of the program in the [Technology Partner Program Guide](https://www.hubspot.com/hubfs/technology-partner-enablement/hubspot-technology-partner-program-guide.pdf), including detailed requirements and benefits of each tier.
## Understand tier updates
Technology partner tier upgrades are evaluated quarterly on January 15, April 15, July 15, and October 15, based on the performance of the previous twelve months. Starting in 2027, tier downgrades will be evaluated semiannually, on January 15 and July 15.
The technology partner dashboard will be updated within twenty-four hours of evaluation. When a tier change occurs, you will be notified by email. If the information on your partner dashboard still looks incorrect twenty-four hours after evaluation, contact [technology-partners@hubspot.com](mailto:technology-partners@hubspot.com).
## Understand dashboard requirements
On the technology partner dashboard, you can view your current tier, what that tier means, and what is required to move to the next tier. The data on the performance dashboard updates daily.
In order to see the technology partner dashboard, you must:
* Have at least one approved HubSpot Marketplace listing.
* Have an app listing that's been approved long enough for a tier to be evaluated.
## Review technology partner status
* In your HubSpot account, navigate to **Development**.
* In the left sidebar menu, navigate to **Technology Partner**.
* On the dashboard, review the following information:
* Your current tier.
* Your overall progress toward the next tier.
* Your performance on the customer value and influenced revenue goals that contribute to your tier.
* When your performance data was last updated.
* To access the Partner POC form required for Leading and Premier tiers, click **Partner POC Form** in the upper right. Learn more in the [Technology Partner Program Guide](https://www.hubspot.com/hubfs/technology-partner-enablement/hubspot-technology-partner-program-guide.pdf).
Learn more about [submitting influenced revenue](https://www.hubspot.com/hubfs/technology-partner-enablement/hubspot-influenced-revenue-form.pdf).
# View contacts created from the Marketplace
Source: https://developers.hubspot.com/docs/apps/developer-platform/list-apps/view-contacts-created-from-the-marketplace
Learn how to access a segment of contacts who have interacted with your HubSpot Marketplace app listings.
When contacts interact with your HubSpot Marketplace listing, they are added to a specific contact segment in the associated developer account. You can then connect with them to see if the app would be a good fit for their needs. The developer account associated with the app must be using HubSpot's free tools.
## Understand requirements
* The developer account associated with the app must be using HubSpot's free tools.
* This feature is only available for accounts building apps on the [latest two versions](/docs/apps/developer-platform/overview) of the developer platform (`2025.2` and `2026.03`).
## Criteria for applicable contacts
Contacts will appear on the segment when they take one of the following actions on your marketplace listing:
* Clicked a video or screenshot.
* Clicked the *View the setup guide* link.
* Clicked the *Company Website* link.
* Clicked the *Install* button.
Only contacts who are logged into HubSpot when they access the marketplace listing will be added to the segment.
## Access marketplace listing contacts
* In the HubSpot developer account associated with your app, navigate to **CRM** > **Segments**.
* Click the **segment** named "App Marketplace - \[App Name]." If you have multiple apps, each app will have a separate segment.
* The following properties will be populated for each contact:
* First Name
* Last Name
* Company
* Email
* You can click the **name** of a contact to access its record page, where the corresponding actions they took will be listed on the timeline. Each event will also include the name of the app they were interacting with.
# Build a classic HubL theme
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/classic-hubl-quickstart
Get started with HubSpot CMS by building a classic HubL theme.
HubSpot's CMS is a powerful, flexible platform for creating HubSpot websites, including website pages, blogs, and lightweight apps. It features built-in security and reliability features, along with a globally distributed Content Delivery Network (CDN) that ensures fast page load times.
When developing on the HubSpot CMS, you can use your preferred tools, technologies, and workflows, such as [GitHub](/docs/cms/start-building/introduction/developer-environment/github-integration), while developing websites. Content creators can then create pages and publish content using drag and drop editors. And because the CMS is integrated with the CRM, you can [create dynamic website experiences](/docs/cms/start-building/features/data-driven-content/crm-objects) for your visitors based on the data you already have.
## Before you begin
Before you start, ensure you've done the following:
* Create a free test account to build without impacting a production environment. You can use either of the following types of accounts:
* Create a [developer account](/docs/getting-started/account-types#app-developer-accounts), then create a [test account](/docs/getting-started/account-types#developer-test-accounts) within it. Because you can also build private apps in developer test accounts, along with building public apps in developer accounts, you'll have one home for both CMS and app development.
* Create a [CMS developer sandbox account](/docs/getting-started/account-types#cms-sandbox-accounts).
* Install [Node.js](https://nodejs.org/en/), which enables HubSpot's local development tools. Versions 18 or higher are supported.
Once you're ready to begin, open a terminal window and create or navigate to the directory where you want your local HubSpot files to live. This working directory is where the theme and its associated files will be placed.
Next, run `npm install -g @hubspot/cli@latest` to install the HubSpot CLI, which introduces an `hs` command that allows you to easily interact with your HubSpot account.
Run `hs account auth` to connect the tools to your HubSpot account. This command will walk you through the following steps:
1. First you’ll be guided to create a personal access key to enable authenticated access to your account via the local development tools. You’ll be prompted to press "Enter" when you’re ready to open the [Personal Access Key page](https://app.hubspot.com/l/personal-access-key) in your default browser. This page will allow you to view or generate your personal access key, if necessary. (Note: You’ll need to select at least the "Design Manager" permission in order to complete this tutorial.) Copy your access key and paste it in the terminal.
2. Next, you’ll enter a name for the account. This name is only seen and used by you, For example, you might use "sandbox" if you're using a developer sandbox or "company.com" if you’re using a full customer account. This name will be used when running commands.
Once you've completed the config file setup flow, you'll see a success message confirming that a configuration file, [\~/.hscli/config.yml](/docs/developer-tooling/local-development/hubspot-cli/reference#authentication), has been created in your home directory.
Run `hs create website-theme my-website-theme` to create a `my-website-theme` directory populated with files from the [CMS theme boilerplate](https://github.com/HubSpot/cms-theme-boilerplate).
Run `hs upload my-website-theme my-website-theme` to upload your new theme to a `my-website-theme` folder in your HubSpot account.
Once this task has completed, you can view these files in the design manager of your HubSpot account. The design manager is an in-app code editor that displays the developer file system, and can be found by navigating to [Content > Design Manger](https://app.hubspot.com/l/design-manager/) in the left sidebar of your account.
To experience how content creators will use your templates and modules, create a website page using the theme you just uploaded.
* In your HubSpot account, navigate to [Content > Website Pages](https://app.hubspot.com/l/website)
* In the upper right, click **Create**, then select **Website page**.
* In the dialog box, enter a **name** for your page, then click **Create page**.
* On the next page, select the **my-website-theme** theme if it's not already selected. Then, hover over the **Homepage** template and click **Select template**.
* You'll then be brought to the website page editor where you can explore all of the options that content creators will have when working with the template. Learn more about using the editor to build and customize pages on [HubSpot's Knowledge Base](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages).
* Click the **Settings** tab in the editor, then select **General**. Enter a **Page title**, then set a **Content slug** to finalize the page's URL. Then, close out the dialog box by clicking **X** or pressing the **Escape key**.
* In the upper right, click **Publish** to take your page live.
Run `hs watch my-website-theme my-website-theme`. While `watch` is running, every time you save a file, it’ll be automatically uploaded. Open your theme's `/css/components/_footer.css` file in your editor, make a change (such as updating the `.footer__copyright` selector to have `color: red;`), and save your changes. Your terminal will show that the saved file has been uploaded.
Reload your published page to see the CSS change reflected on your website.
## What's next?
You're encouraged to continue to explore and experiment with the boilerplate theme and the page-building experience. The sandbox account you created is yours to play around in and experiment with.
You can checkout [HubSpot's Inspire gallery](https://inspire.hubspot.com/) to see websites, landing pages, and web apps built on HubSpot.
You might also want to check out the following documentation:
* [CMS developer tutorials](/docs/cms/start-building/introduction/overview)
* [HubSpot CMS overview](/docs/cms/start-building/introduction/overview)
## Join the HubSpot CMS developer community
Learning is easier when you can learn from those who came before you.
HubSpot is driven by its [Culture Code](https://blog.hubspot.com/blog/tabid/6307/bid/34234/the-hubspot-culture-code-creating-a-company-we-love.aspx), embodied by the attributes in HEART: **H**umble, **E**mpathetic, **A**daptable, **R**emarkable, and **T**ransparent. This culture extends to our ever-growing developer community, with thousands of brilliant and helpful developers around the world.
### Developer Slack community
Join the [Developer Slack](https://developers.hubspot.com/community/slack) to collaborate with 9,000+ developers and members of the HubSpot product team.
### Developer forums
Ask questions, learn from fellow developers, and submit ideas in the [CMS developer forums](https://community.hubspot.com/t5/CMS-Development/bd-p/designers_support).
# Optimize your HubSpot development workflow
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/developer-environment/creating-an-efficient-development-workflow
Creating an efficient development workflow when building websites on the HubSpot CMS.
Setting up an efficient developer workflow will help you work more effectively when building websites on the HubSpot CMS. Depending on the nature of your web development team, or the nature of a specific project, your workflow may differ.
For example, a single developer building out a new site in a new HubSpot CMS account needs to worry less about testing and collaboration. On the other hand, a team of developers working on a larger website will need a clearer dev and staging process, a deployment workflow, and code living in source control in order to work efficiently.
This guide is designed to walk you through setting up an efficient developer workflow, which you can adapt to fit your needs.
This guide assumes you build websites using the [CMS CLI](/docs/developer-tooling/local-development/hubspot-cli/reference), follow the [getting started with local development](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli) tutorial to get set up. This guide also assumes you've gone through the [quick start guide to developing on the HubSpot CMS](/docs/cms/start-building/introduction/classic-hubl-quickstart).
## Building with portability in mind
Before we begin setting up our developer workflow, it is important to recognize portability as a key concept in having an efficient developer workflow. The portability of your project ensures it is easy to move between environments with little friction and explanation, making it easy to test and stage changes before taking them live.
The [CMS Theme Boilerplate](https://github.com/HubSpot/cms-theme-boilerplate) is an example project that is portable, utilizing features like relative file paths, and true file format for all assets in the project using the [CMS CLI](/docs/developer-tooling/local-development/hubspot-cli/reference), which allows it to live in source control and work in any HubSpot account. This project is a great starting or reference point for developers working on a new project. All of the HubSpot default Themes are built using this boilerplate, and can also be used as a portable and effective starting point.
## Setting up your development environment
For your individual development environment, each developer on your team should create a free [CMS Developer Sandbox account](https://offers.hubspot.com/free-cms-developer-sandbox). These accounts never expire and have all of the functionality of paid HubSpot CMS accounts (except being able to connect custom domains).
The CMS CLI makes it easy to interact with multiple HubSpot CMS accounts. Create a new [configuration entry](/docs/developer-tooling/local-development/hubspot-cli/reference) for your CMS Developer Sandbox account. Set the name of the entry for your sandbox to be along the lines of “DEV” or “SANDBOX” so it is clear this account is a development environment. Additionally, set the `defaultPortal` to be your sandbox account, so when you run commands using the CMS CLI, it will automatically interact with your sandbox, and reduce accidental production deploys. At this point, your configuration file will look something like this:
```yaml theme={null}
defaultPortal: DEV
portals:
- name: PROD
portalId: 123
authType: personalaccesskey
personalAccessKey: >-
xxxxx-xxxxxx-xxxxxxx-xxxxxx-xxxxx-xxxxxxx-xxxxxxxx
auth:
tokenInfo:
accessToken: >-
xxxxx-xxxxxx-xxxxxxx-xxxxxx-xxxxx-xxxxxxx-xxxxxxxx
expiresAt: '2020-01-01T00:00:00.000Z'
- name: DEV
portalId: 456
authType: personalaccesskey
personalAccessKey: >-
xxxxx-xxxxxx-xxxxxxx-xxxxxx-xxxxx-xxxxxxx-xxxxxxxx
auth:
tokenInfo:
accessToken: >-
xxxxx-xxxxxx-xxxxxxx-xxxxxx-xxxxx-xxxxxxx-xxxxxxxx
expiresAt: '2020-01-01T00:00:00.000Z'
```
Now, when running commands in the CMS CLI, like [`hs upload`](/docs/developer-tooling/local-development/hubspot-cli/reference#upload), if you do not specify a portal, the files will be uploaded to your “DEV” account.
### Setting up your code editor
You can use your preferred code editor when building on HubSpot, whether you prefer [VS Code](#vs-code), or [other code editors and IDEs](#other-code-editors-and-ides).
#### VS Code
A significant amount of developers building on HubSpot use [Visual Studio Code](https://code.visualstudio.com/). That inspired the HubSpot VS Code Extension. The extension adds handy intellisense snippets, HubL code completion, HubL syntax highlighting HubL Linting. The project is [open source](https://github.com/HubSpot/hubspot-cms-vscode) and [contributions are welcome](https://github.com/HubSpot/hubspot-cms-vscode/blob/master/CONTRIBUTING.md). If you have feedback, please [file an issue on the repository](https://github.com/HubSpot/hubspot-cms-vscode/issues).
#### Other code editors and IDEs
While there is an official VS Code extension, there is no reason you can't use a different preferred editor. HubL is HubSpot's private fork of Jinjava, which is based on Jinja. Because of the similarities in syntax, Jinja syntax highlighting extensions tend to work well. Extensions and add-on tooling vary by editor.
## Testing
There are two main methods for testing changes:
* **Testing with watch/upload:** When working in your development environment, it is safe to use the [watch](/docs/developer-tooling/local-development/hubspot-cli/reference#watch) command to automatically upload changes when you save files in your text editor to rapidly develop. If you use the Design Manager “Live preview with display options” tool for a template, as you save changes, you will automatically see them reflected in the rendered output of the template preview. To view the live preview of a template, select **Preview > Live Preview** with display options within the template editor of the Design Manager.
* **Testing locally:** to preview your changes locally without uploading to the account, you can run the `hs theme preview` command in the theme's root directory. This command will run a local proxy server at [https://hslocal.net:3000/](https://hslocal.net:3000/) which you can then use to preview the theme's templates and modules. Learn more about the [hs theme preview command](/docs/developer-tooling/local-development/hubspot-cli/reference#locally-preview-theme).
#### Editor
Another critical piece of the development phase is testing your changes in the content creation tools. If you are building modules, or templates designed to be manipulated in the content editor, create pages in your development environment to ensure the content editing experience is as you intend it to be. Drag modules around into odd configurations and enter dummy content to make sure marketers can not “break” your modules when building pages. Using the content editors will help illustrate what guardrails you want to build into your templates and modules. Currently, it is not possible to move content, such as pages or blog posts, between HubSpot accounts.
#### Module Preview
When in the module editor within the Design Manager, select the “Preview” button. This will open up a preview editor for how the module and its fields will behave in the content editors. This allows you to test the fields, groups, and repeaters in your module with dummy content in a safe environment.
#### Debugging
Knowing how to debug and troubleshoot issues with your website is critical in the ongoing health and success of your website. Familiarize yourself with [debugging techniques when developing on the HubSpot CMS](/docs/cms/start-building/introduction/developer-environment/troubleshooting).
#### Sandboxes
As noted above in the section about setting up your development environment, you can create free [CMS Developer Sandbox](https://offers.hubspot.com/free-cms-developer-sandbox) accounts to use for testing and as a safe development environment.
## Deploying
Once you have tested your changes and are ready to take them live, it is time to deploy your changes to your production portal. Based on your local configuration, you will need to run the CMS CLI command with the `--portal` argument to interact with your production account, such as `hs upload my-theme/src my-theme --portal=PROD`. When uploading files to your production account, pay attention if there were any errors to diagnose, and make sure to briefly browse your live website to make sure there were not any unintended consequences of the deploy.
If you work as part of a web development team, it is recommended to have your entire production codebase source of truth in version control, and to deploy to your product portal when changes are merged in master. This way, your team of developers can use your favorite version control system to collaborate, track changes and easily roll-back changes.
To learn more about setting up continuous integration with git repositories, follow this guide on [utilizing GitHub actions to deploy to your production account when changes are merged into master](/docs/cms/start-building/introduction/developer-environment/github-integration).
# Basic coding in the design manager
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/developer-environment/design-manager
Use HubSpot's design manager tool for basic coding, viewing your default modules and themes, and more.
When building on the HubSpot CMS, you can use the design manager tool to create and manage classic HubL + HTML assets such as [templates](/docs/cms/start-building/building-blocks/templates/overview) and [modules](/docs/cms/start-building/building-blocks/modules/overview), as well as CSS and JavaScript files. The design manager is also where you'll find the code for your account's default themes and modules, which you can pull down to your local environment using the [`hs fetch` command](/docs/developer-tooling/local-development/hubspot-cli/reference#fetch-files). The design manager can be found by navigating to **Content** > **Design Manager** in your account's main navigation bar.
CMS assets built with developer projects, such as React modules, will not appear in the design manager. This includes any HubSpot default modules built with React, like the [latest version of the post listing module](/docs/cms/reference/modules/default-module-versioning#post-listing). Instead, you'll need to create and manage those assets locally via the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli).
Check out [HubSpot's Knowledge Base](https://knowledge.hubspot.com/design-manager/a-quick-tour-of-the-design-manager) for a comprehensive overview of the design manager and its various components.
## Default modules and themes
In the design manager, the `@hubspot` folder contains default modules and themes that you can import into your content or clone for customization.
You can reference these assets in your own code by importing them by their path, starting with `@hubspot`. For example, to include the default pagination module in a template, you would add the following code to the `dnd_row` where you want to add the module:
```jinja theme={null}
{% dnd_module path="@hubspot/pagination" %}
{% end_dnd_module %}
```
See HubSpot's[ CMS boilerplate blog index template](https://github.com/HubSpot/cms-theme-boilerplate/blob/main/src/templates/blog-index.html) for a full example.
To download these default assets to your local environment, use the [`hs fetch` command](/docs/developer-tooling/local-development/hubspot-cli/reference#fetch-files) and specify the asset path, which you can get by right-clicking the asset in the design manager and selecting **Copy path**.
```shell theme={null}
hs fetch /@hubspot/blog_comments.module
```
## Design manager settings
The design manager has basic IDE settings you can configure to make the experience fit you and your workflows. A settings button appears adjacent to the help button in the bottom bar and can be used to configure the settings.
You can configure settings for:
* Editor theme
* Font
* Tab size
* Indent unit
* Soft tabs
* Smart Indent
* Indent on formatting input
* Indent with tabs
* Line wrapping
* Auto close brackets
* Match brackets
* Match tags
* ESLint
See [HubSpot's Knowledge Base](https://knowledge.hubspot.com/design-manager/customize-design-manager-settings) for a breakdown of the settings for more information.
# Set up continuous integration with a GitHub repository using GitHub Actions
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/developer-environment/github-integration
Set up continuous integration with a GitHub repository using GitHub Actions.
As a part of your [development workflow](/docs/cms/start-building/introduction/developer-environment/creating-an-efficient-development-workflow), you might prefer to keep your production codebase source of truth in version control. This would be especially helpful if you work as a part of a development team so that you can track changes and quickly roll them back if needed.
Using [GitHub Actions](https://github.com/features/actions), you can set up a continuous integration with a GitHub repository. This guide walks through the integration process, and assumes that you're familiar with:
* [Using Git](https://docs.github.com/en/get-started/using-git) and GitHub
* Building websites using the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli)
Below, learn how to set up the integration using the HubSpot CMS Deploy GitHub Action (recommended) or manually.
## Send local files to GitHub
Before you can integrate with GitHub, you'll first need to gather your files locally.
* If you have an existing CMS asset that lives in HubSpot, such as a theme or set of templates, you can fetch it by running the [fetch](/docs/developer-tooling/local-development/hubspot-cli/reference#fetch) command as follows: `hs fetch `. Alternatively, you can download all files in the account's [developer file system](/docs/cms/start-building/introduction/overview#developer-file-system) by running `hs fetch /`.
* To create a new local project, it's recommended to start with the [CMS theme boilerplate](/docs/cms/start-building/building-blocks/themes/hubspot-cms-boilerplate). If you haven't worked with the CMS theme boilerplate before, check out the [quickstart guide](/docs/cms/start-building/introduction/classic-hubl-quickstart). If you've already installed the HubSpot CLI and configured your local environment, you can create a new local theme from the boilerplate by running `hs create website-theme `. You'll then need to upload your files to HubSpot with the [hs upload](/docs/developer-tooling/local-development/hubspot-cli/reference#upload) command.
With your code available locally, you'll then [add it to a GitHub repository](https://docs.github.com/en/migrations/importing-source-code/using-the-command-line-to-import-source-code/adding-locally-hosted-code-to-github). After adding your files to GitHub, proceed to the next step to either install HubSpot's pre-made GitHub Action (recommended) or [configure the Action manually](#manually-configure-the-action).
## Use the HubSpot CMS Deploy GitHub Action (recommended)
To streamline the process, HubSpot created a GitHub Action that you can install to your GitHub project to handle automatically deploying changes from a branch to your production HubSpot account.
## Create and merge a pull request in main
* With your secrets, workflows, and scripts in your GitHub repository, create a pull request and merge it into main.
* After merging the pull request, navigate to **Actions**. You should see your deploy Action run, which will then deploy your code to your HubSpot account.
## Lock your asset in the design manager
Now that your source of truth lives in GitHub, you should lock your asset in HubSpot to prevent edits from being made there. This ensures that changes only come through the deploy action.
To lock assets in the design manager:
* In your HubSpot account, navigate to **Marketing** > **Files and Templates** > **Design Tools**.
* Locate your asset's folder in the left sidebar, then **right-click** and select **Lock folder**.
# How to use JavaScript frameworks on HubSpot
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/developer-environment/js-frameworks
Using the HubSpot CMS, you can create advanced JS-based web applications. There are boilerplates available to make it easier to get up and running.
Using the HubSpot CMS, you can create JavaScript-based web applications.
## What tier of HubSpot CMS is needed?
If your website requires server-side code or a content membership mechanism, you can take advantage of HubSpot's [serverless functions](/docs/cms/start-building/features/serverless-functions/overview) and the [content membership](/docs/cms/start-building/features/memberships/overview) feature if you have an *Enterprise* subscription. However, you could alternatively build your own system using third-party providers such as AWS Lambda combined with an API gateway to run server-side code.
If you're building a web application that needs to hit API endpoints that require authentication such a [private app access token](/docs/apps/legacy-apps/private-apps/overview), you shouldn't run that code in the browser. You would be exposing your credentials to anyone who views the page. The right approach is to create a layer of abstraction between the browser and the authenticated API: a custom API endpoint that does not require exposing your credentials and is served from the same domain as the website calling it.
Hitting the custom API endpoint will run server-side code that can make the authenticated request. Then you can do any formatting of the data or business logic you want to keep secret, and send the result to the browser.
Commonly, serverless functions are used to do this because they have incredible scalability, and they don't require managing and maintaining your own server. You can use providers like AWS Lambda combined with an API gateway, or you can use HubSpot's first-party [serverless functions](/docs/cms/start-building/features/serverless-functions/overview). The advantage of HubSpot serverless functions is that you don't need to manage multiple separate services. The experience is simplified and directly integrated with the same developer file system which themes, templates and modules all exist in.
If you don't need to make authenticated API calls, then you don't need enterprise for your app. React and Vue are front end frameworks that don't need serverless functions to work, it is what you do with them that matters.
## Frameworks and libraries
For web applications, developers commonly use JavaScript frameworks that help manage state and User Interface (UI).
CMS Hub was not purpose-built to work with a specific framework in mind, but many common JavaScript frameworks work on HubSpot CMS. Building on HubSpot, you may need to think about how you work with those frameworks differently. But the core things needed to work with these frameworks are available: the ability to write [custom templates](/docs/cms/start-building/building-blocks/templates/overview), [modules](/docs/cms/start-building/building-blocks/modules/hide-modules-and-sections), and JavaScript. We also enable you to do your [coding locally](/docs/developer-tooling/local-development/hubspot-cli/reference), so that you can use a build step.
## What you should know
We are collaborating with our developer community to establish the best practices for building with common JavaScript frameworks on HubSpot. While it is possible to do it, there are aspects of how the HubSpot CMS works that may require you to consciously set up your project differently than you might on a simple HTML page.
There may also be some parts of your development workflow that aren't what you're used to. We ask that you let us know your feedback so we can improve the experience for all developers. Currently the best place to do that is our [Developer forum](https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers). As we experiment and learn, we will continue to update our documentation accordingly.
### Things to consider when building
The HubSpot CMS has a powerful [module system](/docs/cms/start-building/building-blocks/modules/hide-modules-and-sections), enabling you to create re-usable chunks of CSS, JavaScript and HTML with access to [HubL, the HubSpot templating language](/docs/cms/reference/hubl/overview). HubSpot modules provide a way for you to give a lot of control and power to content creators. Modern JavaScript frameworks often have their own module systems. These systems are all built independent from each other and as a result often have different solutions for issues you might encounter.
#### Server-side rendering and client side rendering
Server-side rendering is when the HTML for a page is generated from templating logic on the server before sending any HTML to a browser.
Client-side rendering is when a lighter or "incomplete" version of the HTML is sent from the server, and JavaScript is used to generate the HTML. This transfers the processing of logic from the server to the web browser (the client).
Hydration is the act of combining both techniques. First, on the server, as much HTML as possible is generated. Then JavaScript evaluates the HTML provided and makes smaller changes to it as needed when the user interacts with the page or data is received. This reduces the load on the client and potentially reduces the time it takes for the user to see the loaded content.
On HubSpot CMS, HubL is processed server-side and then cached at the CDN level. You can then use JavaScript to hydrate or client-side render the HTML the browser serves to the site visitor.
#### Single Page App (SPA) analytics
Analytics is important for your company's ability to grow and adapt to solve for your customers and prospects. When building a single page app that contains multiple views, you may want to track the visitor seeing different views, as pages.
Most analytics platforms provide a way to do this with JavaScript, HubSpot is no different. [Push the page-view](/docs/api-reference/latest/account/settings/tracking-code/overview#tracking-in-single-page-apps) when your app changes views.
#### Building your app utilizing HubSpot modules
HubSpot's module system is a server-side module system, generating an HTML document from HubL + HTML partials and generating minified CSS and JavaScript for each module within a page.
If you build using HubSpot modules, there are several benefits that come along with it:
* Content creators can add your module to pages that have drag and drop areas or flexible columns. They can also move and remove the module themselves.
* You can provide fields to the content creator that let them configure settings for your app.
* Your code is only rendered to the page only if the module is actually used.
* `Module.css` and `module.js` is automatically minified.
The cost of using the HubSpot module system is that it requires modules to be made up of specific files and in different places than you might normally place your code.
#### Building a full template instead
You could also build your application as a template rather than within the module framework. This gives you more flexibility with your file structure. But you do not get the benefits that modules provide; content creators will not be able to add this application to pages within drag and drop areas and flexible columns.
#### Delimiters
Some JavaScript frameworks use curly braces `{ }` to delimit their code. The HubL language uses these braces, as well. There are three strategies you can use to ensure you don't have conflicts between your framework and HubL: You can use the raw HubL tag to wrap around your JSX, set the framework to use a different delimiter, or use a build step that compiles the JavaScript beforehand.
## VueJS
The popular [Vue.js](https://vuejs.org/) framework can be used with and without a build step. See [Vue's own documentation](https://vuejs.org/guide/quick-start.html) for a more detailed breakdown of the pros and cons of each method. On HubSpot there are specific pros and cons you should also be keeping in mind.
### Without a build step
Integrating Vue.js without a build step into a module is easy.
#### Add the vue library to your module
In your `module.html` file, use [`require_js`](/docs/cms/reference/hubl/functions#require-js) to add the [Vue library](https://vuejs.org/guide/quick-start.html#Direct-lt-script-gt-Include) ensuring it will only load once when your module is added to a page.
While developing, use the dev build to get useful information for debugging. Once in production, it is recommended to use either the CDN URL for the specific Vue version, or download that file and host it as a JavaScript file in the HubSpot [developer file system](/docs/cms/start-building/introduction/overview#developer-file-system).
#### Add the HTML code
Copy the HTML code from the [Vue.js introduction](https://vuejs.org/guide/introduction.html#Declarative-Rendering), and paste it into your `module.html` file. Wrap this code in a HubL raw tag to prevent it from being evaluated as HubL.
```hubl theme={null}
{# raw prevents code within it from being evaluated as HubL #}
{{ message }}
```
#### Add your JavaScript code
Copy the JavaScript from the [Vue.js introduction](https://vuejs.org/guide/introduction.html#Declarative-Rendering), and paste it into your `module.js`. Wrap this code in an [event listener to ensure it's executed once the DOM content has finished loading](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event). Publish your module, and preview it. **You should now see your basic Vue app working.**
```js theme={null}
var app = new Vue({
el: "#app",
data: {
message: "Hello Vue!",
},
});
```
### With a build step
We've built a [boilerplate](https://github.com/HubSpot/cms-vue-boilerplate) \[BETA] to help you get up and running with the HubSpot module approach to building a VueJS application. The easiest way to take advantage of it is to run the `hs create vue-app` command from the [CMS CLI](/docs/developer-tooling/local-development/hubspot-cli/reference). Directions can be found in the [repository](https://github.com/HubSpot/cms-vue-boilerplate/).
This boilerplate is new and we would love to hear your feedback! Let us know what could be improved and any issues you encounter. The best way to provide feedback is by [submitting issues to the GitHub repository](https://github.com/HubSpot/cms-vue-boilerplate/issues).
### Working with HubSpot forms and CTAs within Vue components
HubSpot CTAs and forms have their own script tags, and manage their own HTML themselves. To ensure your vue component doesn't modify the form or CTA, create an HTML element around the CTA/form embed code. [Apply `v-once` to that element.](https://vuejs.org/api/#v-once) This ensures the code will be rendered once and then ignored by your Vue component.
## ReactJS
Rather than using HubL to build modules and partials, you can use [JavaScript and React](/docs/cms/start-building/introduction/react-plus-hubl/modules). In addition to stitching server-rendered React components into the HTML generated by HubL, JavaScript modules and partials support both server-side and client-side interactivity. Learn more in HubSpot's [Introduction to JS Building Blocks](https://github.hubspot.com/cms-js-building-block-examples/).
You can also check out the [React boilerplate](https://github.com/HubSpot/cms-react-boilerplate) to get up and running quickly with a [React](https://reactjs.org/) app inside of a HubSpot module. The easiest way to take advantage of it is to run the `hs create react-app` command from the [CMS CLI](/docs/developer-tooling/local-development/hubspot-cli/reference). From there follow the instructions in the [repository](https://github.com/HubSpot/cms-react-boilerplate).
This boilerplate is new and we would love to hear your feedback! Let us know what could be improved and any issues you run into. The best way to provide feedback is by [submitting issues to the GitHub repository](https://github.com/HubSpot/cms-react-boilerplate/issues).
## Other JavaScript libraries
There are a lot of JavaScript libraries out there and it is impossible for us to document all of them individually. There are some core best practices to know and understand when using JavaScript libraries on HubSpot.
### Use require\_js instead of script tags
You can have dozens of modules, and templates that use shared JavaScript libraries, and not worry about loading those libraries multiple times. To do this you need to use the [`require_js`](/docs/cms/reference/hubl/functions#require-js) HubL function. Scripts loaded using this function will only load once per page regardless of how many modules, partials, and the template, requires them.
```hubl theme={null}
{{ require_js(get_asset_url('/js/jquery-latest.js')) }}
{{ require_js("https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js") }}
```
Use `get_asset_url()` to require files stored within the developer file system. The advantage aside from just co-locating your development files and consolidating security of these files, is that it will result in fewer DNS lookups.
Using require can be amazing for performance, because not only will you only load the file once. If assets on a page page don't need that library, it won't be loaded at all. You can even use requires with HubL logic to load resources only when you truly need it.
## Recommended tutorials and guides
* [Optimizing for performance](/docs/cms/best-practices/testing-staging-performance/speed)
* [Accessibility is not a feature](/docs/cms/best-practices/improve-existing-sites/accessibility)
* [How to use web components on HubSpot](https://developers.hubspot.com/blog/use-web-components-in-hubspot-cms-development)
* [Getting started with modules](/docs/cms/start-building/building-blocks/modules/quickstart)
* [Getting started with serverless functions](/docs/cms/start-building/features/serverless-functions/getting-started-with-serverless-functions)
* [Creating an efficient developer workflow](/docs/cms/start-building/introduction/developer-environment/creating-an-efficient-development-workflow)
* [Building dynamic pages with HubDB](/docs/cms/start-building/features/data-driven-content/hubdb/overview)
* [Build modules with React](/docs/cms/start-building/introduction/react-plus-hubl/overview)
# HubSpot CMS overview
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/overview
High level overview of the HubSpot Content Hub for developers, showing you all of the key concepts like themes, templates, and modules, and how they fit together.
This section is designed to help you understand key aspects of HubSpot's *CMS* and build great websites on it. To get the most out of this, a professional-level understanding of web development basics, including HTML, JavaScript, and CSS, is expected.
## Getting started
If you're just getting started with developing on HubSpot's CMS, it's recommended to begin with the following:
* Create a free [developer account](/docs/getting-started/account-types#app-developer-accounts), then create a [test account](/docs/getting-started/account-types#developer-test-accounts) within it. This will give you a testing environment to build out your CMS assets without impacting a standard HubSpot account. Because you can also build private apps in developer test accounts, along with building public apps in developer accounts, you'll have one home for both CMS and app development. Alternatively, you can create a [CMS developer sandbox account](/docs/getting-started/account-types#cms-sandbox-accounts).
* Follow the [CMS quickstart guide](/docs/cms/start-building/introduction/react-plus-hubl/react-plus-hubl-quickstart) to walk through some basics, such as using the [CMS theme boilerplate](https://github.com/HubSpot/cms-theme-boilerplate), running commands using the HubSpot CLI, and the relationship between local development and content creation in HubSpot.
## Building for content creators
HubSpot's CMS is designed to help businesses grow their web presence with an emphasis on enabling marketers to create and manage web content. The website's content, lead collection, and analytics are integrated with the [HubSpot CRM](https://www.hubspot.com/products/crm), making it easy to create personalized experiences for visitors and integrate those experiences with the rest of the business.
A well-crafted website should be developed in close collaboration with your content creators to understand their needs. To that end, it's recommended that you [preview how the page building experience looks and feels for content creators](/docs/cms/start-building/introduction/developer-environment/creating-an-efficient-development-workflow#testing) while you build. This ensures they can work independently with the site as much as possible.
HubSpot takes care of hosting and maintaining your pages, so you don’t have to worry about plugin management, updates, hosting, scaling, or security. The tradeoff is that the system puts a few more restrictions on what you can do compared to self-hosted CMS's. For example, you can’t alter or extend system fundamentals manually or via plugins, manipulate low-level rendering, or access and alter database content directly.
Developer-built content (e.g., [themes](/docs/cms/start-building/building-blocks/overview), [templates](/docs/cms/start-building/building-blocks/templates/overview), [modules](/docs/cms/start-building/building-blocks/modules/hide-modules-and-sections), JavaScript, and CSS) is created in a developer file system, while page content (pages, blog posts) is laid out and built in a powerful block-based what you see is what you get (WYSIWYG) editor, and media files (content creator-built images, PDFs, etc.) are stored in a web app-based file manager.
When a page is rendered, HubSpot routes the request to one of many servers based on domain, renders the page on our servers, and caches it to a content delivery network (CDN) if possible.
## Types of content
There are many types of content that you create using HubSpot's CMS. The user interface for content creators is slightly different depending on content type, which has implications that you as a developer need to be aware of.
### Website pages and landing pages
Website and landing pages are built independent of one another, but all pages are based on templates. For content creators, the process of building a landing page or a website page is nearly identical. The distinction between them is that website pages are made to present information that’s part of your website and designed to be found organically, while a landing page is [generally associated with a specific marketing offer or campaign](https://blog.hubspot.com/marketing/landing-page-best-practices) (e.g., linked from a marketing email sent to a specific list of contacts).
In the UI for marketers, the analytics and organization of these page types are also organized separately since landing pages often have specific conversion goals.
### Blogs
HubSpot blogs have two views—one for the listing page and one for the individual post page, then each blog post is populated into each of them. You can set a blog to share the same template for blog posts and listing pages, or have separate templates for the listing page and for blog posts. Blog posts must share the same template. Learn more about [blog template markup](/docs/cms/start-building/building-blocks/templates/blog) and [how to create and manage blogs in HubSpot](https://knowledge.hubspot.com/blog/manage-your-blog-template-and-settings).
### Emails
Emails can be built in a few ways in HubSpot:
* **Classic email:** build email templates and modules in a similar way to website and landing pages. You can also build [coded email templates](/docs/cms/start-building/building-blocks/templates/email-template-markup) to have full control of the markup.
* **Drag and drop emails:** build customizable [drag and drop](https://knowledge.hubspot.com/marketing-email/create-marketing-emails-in-the-drag-and-drop-email-editor) email templates that enable content creators to build email layout and content using HubSpot's drag and drop interface.
**Please note:** building custom email modules and templates requires a ***Marketing Hub*** *Professional* or *Enterprise* subscription.
## Working with data
In addition to creating page content through the in-app editors or hard-coding in templates, you can also use structured data sources to populate [dynamic page content](/docs/cms/start-building/features/data-driven-content/crm-objects) with HubL. You can use the following data sources to populate pages:
* [HubDB](/docs/cms/start-building/features/data-driven-content/hubdb/overview): store data in cells of HubDB tables.
* [CRM records](/docs/cms/start-building/features/data-driven-content/crm-objects#crm-object-dynamic-pages): store data in CRM records, such as contacts, companies, or custom objects.
Building dynamic pages using structured content means that you can create, edit, and remove website pages and page content by updating the data sources directly. Similar to a HubSpot blog, a set of dynamic pages will include a single listing page to display the instances of your data source, then a separate page for each individual instance. Using HubL, you can fully configure the data that the pages display.
For example, you can create a HubDB table that stores a row of information for each member of a sales team. Using that HubDB table, HubSpot can then generate a listing page to display key details from each table row (such as a name and image for each sales rep), along with a separate page per sales rep to display more information (such as their bio and phone number). Should a sales rep later be promoted to a different team, you can delete their row from the HubDB table, and HubSpot will automatically delete their detail page and remove them from the listing page.
### Serverless functions
In addition to using CRM records and HubDB data to populate pages, you can use [serverless functions](/docs/cms/start-building/features/serverless-functions/overview) to write server-side code that interacts with HubSpot and third-party services through APIs. Serverless Functions are a ***Content Hub*** *Enterprise* feature.
## Developer file system
The core assets—templates, [themes](/docs/cms/start-building/building-blocks/overview), and [modules](/docs/cms/start-building/building-blocks/modules/hide-modules-and-sections), as well as the JavaScript, CSS files, and images that support them—are created in a developer file system. You can view this file system either in the left panel of the [design manager](/docs/cms/start-building/introduction/developer-environment/design-manager) or in folders synchronized locally using the local development tools. Within the file system, assets can refer to each other with absolute or relative paths.
**Please note:**
React-based assets, such as some HubSpot default modules and custom CMS React assets, will not appear in the design manager file system. These assets are intended to only be worked on in your local environment using the HubSpot CLI to fetch and upload.
Behind the scenes, these files are mapped to entries in a database. This is why access to the developer file system is through the HubSpot [CLI](/docs/developer-tooling/local-development/hubspot-cli/reference) tools rather than direct SSH or FTP access, and some file system features you may expect, like permissions and symlinks, are not offered in the developer filesystem.
This differs from the approach of traditional CMS's, but means that broken references between file or syntax errors are caught at publish time rather than at runtime, providing you with extra insulation against accidental failures when live traffic is hitting a website.
Templates in the file system will be automatically detected and will be presented to content creators as they’re making new pages, so the structure of the file system is up to you. There’s no requirement that modules live in a `/modules/` folder or JavaScript lives in a `/js/` folder. However, it's recommended to organize your assets in a similar way to the [boilerplate example code for the CMS](https://github.com/HubSpot/cms-theme-boilerplate).
**Please note:**
By default, HubSpot automatically minifies JavaScript and CSS included in the design manager to remove unnecessary spaces, line breaks, and comments. This also applies to JavaScript and CSS [uploaded to the design manager through the CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). This means that you should not add already minified code directly to the design manager.
Learn more about [JavaScript and CSS minification](/docs/cms/best-practices/testing-staging-performance/overview#javascript-and-css-minification).
## Themes, templates, modules, and fields
[Themes](/docs/cms/start-building/building-blocks/overview), [templates](/docs/cms/start-building/building-blocks/templates/overview), [modules](/docs/cms/start-building/building-blocks/modules/hide-modules-and-sections), and [fields](/docs/cms/reference/fields/module-theme-fields) are the most common types of assets you'll be working with. Using each type of asset effectively gives content creators the freedom to work on websites independently while staying inside defined style and layout guardrails.
Themes are the highest-level container that you can use to package other assets to create a cohesive site. One level down, templates are the files that content creators use for building individual pages, blog posts, emails, and more. Then, modules are elements on the page, such as a pricing card or image gallery. HubSpot provides a set of [default web modules](/docs/cms/reference/modules/default-modules) you can use out of the box for pages, and a set of [default email modules](/docs/cms/start-building/building-blocks/modules/default-email-modules) for building emails.
**Please note:** building custom email modules and templates requires a ***Marketing Hub*** *Professional* or *Enterprise* subscription.
Themes and modules contain fields, which are settings of specific data types, such as numbers, strings, rich text, and images. You can control how these are used in rendering these objects, as well as how they should be organized and appear in the drag and drop content editor. Content creators can set values for fields in the editor, which are applied to the theme or module at render time.
Learn more in the [CMS building blocks overview](/docs/cms/start-building/features/multi-language-content).
## The HubL Language
The main language that you'll use to build website assets on HubSpot's CMS is the HubSpot Markup Language or [HubL](/docs/cms/reference/hubl/overview) (pronounced “Hubble”). HubL is HubSpot’s extension of [Jinjava](https://github.com/HubSpot/jinjava), a templating engine based on [Jinja](https://palletsprojects.com/projects/jinja/). HubL uses a fair amount of markup that is unique to HubSpot and does not support all features of Jinja. It’s executed completely on the server-side when a page is rendered.
HubL has the features you’d expect of a simple templating language like [variables](/docs/cms/reference/hubl/variables), [for loops](/docs/cms/reference/hubl/loops), and [if statements](/docs/cms/reference/hubl/if-statements), but also supports more complex rendering [macros](/docs/cms/reference/hubl/variables-macros-syntax#macros), data fetching, and mapping with [tags](/docs/cms/reference/hubl/tags/standard-tags), [functions](/docs/cms/reference/hubl/functions), and [filters](/docs/cms/reference/hubl/filters).
If you reach the limits of what's possible with HubL, HubSpot provides APIs for creating more customized solutions. ***Content Hub*** *Enterprise* accounts can use [serverless functions](/docs/cms/start-building/features/serverless-functions/overview), enabling more sophisticated server side programming.
You can refer to the [HubL language reference](/docs/cms/reference/hubl/overview) for more details on specific language features.
## Logged in pages
Using the [Membership](/docs/cms/start-building/features/memberships/overview) feature of ***Content Hub*** *Enterprise*, you can require your CRM contacts to be logged in to view specific content of your site. Content behind membership pages can be highly personalized to the logged-in contact, and can even render Contacts, Companies, Deals and Products data from the CRM.
## Multi-language support
With HubSpot’s CMS, users can create [multi-language variations](/docs/cms/start-building/features/multi-language-content) of their content. This will allow end-users to see content in the language with which they’re most comfortable. In addition, HubSpot provides tools to help developers ensure that the right language is available to the end-user.
# Fetching data
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/react-plus-hubl/data-fetching
Learn how to fetch data into your React modules through a variety of methods, including GraphQL and HubL.
When you need to fetch data in your React modules, you can use a variety of methods on both the server- and client-side, depending on your use case. This includes:
* GraphQL (*Professional* and *Enterprise* only)
* HubL
* `getServerSideProps` function (*Professional* and *Enterprise* only)
Below, learn more about each available method.
**Please note:** the methods described in this guide are only for reading data. At this time, there are no methods for updating HubSpot data through React modules.
## Server-side fetching
The sections below detail the methods available for fetching data on the server-side.
### GraphQL
If you have a *Professional* or *Enterprise* subscription, you can bind GraphQL queries to your modules to fetch data from HubDB, CRM objects (including custom objects), blogs, and knowledge base.
You can explore your account's schema and run test queries by using the [GraphiQL tool](http://app.hubspot.com/l/graphiql) in your account.
Using GraphQL comes with some key advantages, such as:
* Co-locating the query and component for compartmentalization.
* Needing only a single query for fetching association data.
* Triggering prerendering of pages using the query when you make updates to the query or relevant data.
Learn more about [binding GraphQL queries to modules](/docs/cms/start-building/introduction/react-plus-hubl/modules#graphql).
### hublParameters
You can pass data from the template to the module by using HubL at the template level, then using `props.hublParameters` at the React component level.
In the template, add parameters for each data property in the HubL module tag, then use HubL tokens to fetch the data.
```hubl theme={null}
{% module "contact_profile"
path="@projects/contact-profile-project/contact-profile-app/components/modules/ContactProfile",
firstName="{{contact.firstname}}",
lastName="{{contact.lastname}}",
email="{{contact.email}}"
%}
```
Then, use `props.hublParameters` to pass the data to the React module.
```jsx theme={null}
// contact-profile-project/contact-profile-app/components/modules/ContactProfile/index.jsx
export const Component = props => {
return (
);
};
```
Since the data is passed at the template level, it will not work for cases where modules are added to the page via the drag and drop content editor (as that module doesn't exist at the template level). For those cases, use the `hublDataTemplate` API instead.
### hublDataTemplate
To automatically attach and pass HubL context variables to your React modules:
* In the module, export a string via `hublDataTemplate`. In the string, include a HubL statement to set the `hublData` variable.
```js theme={null}
export const hublDataTemplate = `{% set hublData = "Hello from HubL!" %}`;
```
* The data will come through in your React module as a top level prop of `hublData`. Use `props.hublData` to access the returned data.
```jsx theme={null}
export function Component(props) {
return
{props.hublData}
;
}
```
You can add any valid HubL to this template, including [filters](/docs/cms/reference/hubl/filters), [functions](/docs/cms/reference/hubl/functions), and [module field references](/docs/cms/reference/fields/module-theme-fields). This can be especially useful for building more complex maps of data.
```jsx theme={null}
import ModuleFields from "./ModuleFields.js";
import ModuleMeta from "./ModuleMeta.js";
export function Component(props) {
return (
My total posts: {props.hublData.totalBlogPostCount}
);
}
export const meta = ModuleMeta;
export const fields = ModuleFields;
export const hublDataTemplate = `
{% set blogId = module.blog_field %}
{% set hublData = {
"totalBlogPostCount": blog_total_post_count(blogId),
"blogAllPostsUrl": blog_all_posts_url(blogId)
}
%}
`;
```
Learn more about [passing HubL data in modules](/docs/cms/start-building/introduction/react-plus-hubl/modules#passing-hubl-data).
### getServerSideProps
If you have a *Professional* or *Enterprise* account, you can export a `getServerSideProps` function from React modules. This function must return an object with a `serverSideProps` property and a `cacheConfig` property, which configures [caching of the module](#getserversideprops-caching).
In the React component, the information returned in `serverSideProps` can be accessed via `props.serverSideProps`. For fetching specific data based on dependencies such as URLs, query parameters, or the contact object, HubSpot provides the following utility functions:
* [withModuleProps](#withmoduleprops)
* [withUrlPath](#withurlpath)
* [withUrlAndQuery](#withurlandquery)
* [withContact](#withcontact)
These helper functions wrap your data fetching functions and automatically inject relevant dependencies (with TypeScript types), ensuring that your module has the necessary context to fetch and process the data.
For example, when you use `withUrlPath`, the wrapped function will receive a URL without query parameters, making it easy to fetch data based on the path alone. Similarly, `withContact` ensures that your function has access to the URL, query, and contact information, allowing for more complex data-fetching scenarios.
#### withModuleProps
Wraps a function to provide module properties without additional dependencies. Access to `fieldValues`, `hublData`, and `dataQueryResult` etc. is available.
```jsx theme={null}
import { withModuleProps } from 'path/to/helpers';
const fetchData = (props: ModulePropsWithoutSSP) => {
// Your data fetching logic
};
export const getServerSideProps = withModuleProps(fetchData);
```
#### withUrlPath
Provides module properties like `withModuleProps`, but also includes the page URL (without query parameters). Caching at the module level is partly based on the props and dependencies used in data fetching. By omitting the query parameters, you can optimize this caching, as it won't create new cache records for every query parameter variation.
To include query parameters, use `withUrlAndQuery` instead.
```jsx theme={null}
import { withUrlPath } from 'path/to/helpers';
const fetchData = (props: ModulePropsWithoutSSP, { url }: { url: URLWithoutQuery }) => {
// Your data fetching logic
};
export const getServerSideProps = withUrlPath(fetchData);
```
#### withUrlAndQuery
Builds on `withUrlPath` and provides module properties like `withModuleProps`, but also includes the page URL with query parameters. A new cache record will be created for each permutation of the URL with query parameters for this module.
```jsx theme={null}
import { withUrlAndQuery } from 'path/to/helpers';
const fetchData = (props: ModulePropsWithoutSSP, { url }: { url: URL }) => {
// Your data fetching logic
};
export const getServerSideProps = withUrlAndQuery(fetchData);
```
#### withContact
Like `withUrlAndQuery`, this function provides the module properties, along with a URL with query parameters, but also includes a contact object.
```jsx theme={null}
import { withContact } from 'path/to/helpers';
const fetchData = (props: ModulePropsWithoutSSP, { url, contact }: { url: URL; contact: Contact }) => {
// Your data fetching logic
};
export const getServerSideProps = withContact(fetchData);
```
#### getServerSideProps caching
The caching that happens between HubSpot's edge CDN and the data center where React modules are rendered is outside of the standard [prerendering](/docs/cms/best-practices/testing-staging-performance/prerendering) caching behavior. This means that parts of the page outside of the React module can be statically prerendered, and the module itself can be dynamic or cache by caching rules that you define. By default, HubSpot provides a 10-second cache (`Cache-control: max-age=10`) for data fetching modules.
Cache keys are based on the following:
* Project build number
* Module props (e.g., `fieldValues`, `hublData`, `dataQueryResult`)
* Injected dependency values (e.g., using `withUrlPath` will result in a new cache key being created each time the module is rendered on a page at a new URL path).
This means that, if you haven't made any changes to the data flowing into a module, you can bust the cache by triggering a new project build.
The `getServerSideProps` function includes a `caching` property which you can use to control caching. This property only contains the `cacheControl` property, which represents the [Cache-Control header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control), and it can include any of the [standard directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control#cache_directives).
In the example below, the module will be cached for 60 seconds, and any request after that will trigger a re-cache.
```jsx theme={null}
import { withModuleProps } from 'path/to/helpers';
const fetchData = async (props: ModulePropsWithoutSSP) => {
// Your data fetching logic
const results = await fetch(...).then(response => response.json())
return {
serverSideProps: {
results
},
caching: {
cacheControl: {
maxAge: 60
}
}
}
};
export const getServerSideProps = withModuleProps(fetchData);
```
In local development, the module is rendered on your machine, so no caching is at play.
## Client-side fetching with serverless functions
Use [serverless functions](/docs/cms/start-building/features/serverless-functions/getting-started-with-serverless-functions) to fetch data on the client-side securely without needing to spin up and maintain a back-end server. There are multiple ways to [authenticate serverless function requests](/docs/cms/reference/serverless-functions/serverless-functions-in-projects#authentication) if needed, either by using private app access tokens or [secrets](/docs/cms/start-building/introduction/react-plus-hubl/secrets).
Serverless functions for the CMS are executed when their public endpoint is called. The URL is determined by the [`serverless.json` configuration](/docs/cms/reference/serverless-functions/serverless-functions-in-projects#serverless.json), but always follows a common structure: `https:///hs/serverless/`.
The above path is used for calling serverless functions built with projects, which is the recommended method. You can invoke [serverless functions built with the design manager](/docs/cms/reference/serverless-functions/serverless-functions) in CMS React modules with: `https:///_hcms/api/`.
You can choose to package the serverless function in the same project as your CMS assets, but you can also invoke serverless functions that exists outside of the project. Below is an example of what your project file structure might look like if it contained CMS React assets alongside a private app with a serverless function.
```shell theme={null}
project-folder/
└── src/
└── cms-assets/
├── assets/
├── components/
├── styles/
├── cms-assets.json
└── package.json
└── app/
├── app-hsmeta.json
└── app.functions/
├── function.js
├── package.json
└── serverless.json
```
For debugging, HubSpot provides serverless function logs in the [app settings page of HubSpot](/docs/guides/crm/understanding-the-crm#view-the-app-in-hubspot).
Check out the following documentation for more information about building and using serverless functions:
* [HubSpot's example serverless function project](https://github.com/HubSpot/cms-react/tree/main/examples/serverless)
* [Serverless functions overview](/docs/cms/start-building/features/serverless-functions/overview)
* [Serverless functions reference](/docs/cms/reference/serverless-functions/serverless-functions-in-projects)
* [Get started with serverless functions](/docs/cms/start-building/features/serverless-functions/getting-started-with-serverless-functions)
* [Secrets management](/docs/cms/start-building/introduction/react-plus-hubl/secrets)
# Migrate a HubL theme to a project
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/react-plus-hubl/migrations/migrate-a-hubl-theme-to-projects
Learn how to migrate a classic HubL theme to the projects framework to build with React.
Migrating an existing HubL theme to the projects framework involves restructuring the theme directory, updating file extensions, and adding necessary configuration files. This guide outlines the steps to perform this migration.
After following the steps in this guide, you'll have built and deployed a copy of your existing theme using the projects framework. You can then use the theme in your HubSpot account like any other theme, including swapping your existing pages to the new templates or using them to create new pages.
This migration will not replace your existing theme files. Instead, you'll recreate a subset of your previous files in a new project. As a result, there are some manual steps involved for moving existing pages to the new theme.
## Project setup
To build and deploy your theme on the latest version of the developer platform, you'll need to create a project to contain your theme and its assets. After creating the project, you'll then need to modify your theme directories and files to be compatible with the CMS React [project structure](/docs/cms/start-building/introduction/react-plus-hubl/project-structure#project-structure) requirements.
Before starting the migration, your theme structure will resemble the one shown below.
```plaintext theme={null}
theme/
├── assets/
├── modules/
├── partials/
├── styles/
├── templates/
├── fields.json
└── theme.json
```
* In the terminal, navigate to the directory where you'll be storing your project using the `cd` command.
```shell theme={null}
cd Documents/Dev/serverless-function-project
```
* Run `hs project create` to create a new project.
```shell theme={null}
hs project create
```
* Follow the terminal prompts to create your project. For the template, select **Create an empty project (no template)**.
After following the prompts, the project will be created with the following structure.
```shell theme={null}
projectName/
├── hsproject.json
└── src/
```
Next, you'll begin to build out the rest of the project to incorporate your theme.
The `src/` directory in the project is what contains the project's various components, whether an app, theme, or CMS React module.
* Create a new `theme` directory in `src`.
```shell theme={null}
projectName/
├── hsproject.json
└── src/
└── theme/
```
* In the `theme/` directory, create a `theme-hsmeta.json` file with the following content:
```json theme={null}
{
"uid": "my-cool-theme",
"type": "theme",
"config": {
"themePath": "my-migrated-theme",
"secretNames": []
}
}
```
| Field | Type | Description |
| ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uid` | String | An internal unique identifier for the theme. Must be globally unique within the project. Can be any string up to 64 characters. Characters can be uppercase or lowercase, and can include numbers, underscores (`_`), dashes (`-`), and periods (`.`). |
| `type` | String | The type of component. Must be `theme`. |
| `themePath` | String | The path of the directory containing the [theme.json file](/docs/cms/start-building/building-blocks/overview#theme-json) and theme assets. |
The file name `theme-hsmeta.json` is only for the purposes of this tutorial. You can name the file anything you'd like, as long as it ends in `-hsmeta.json`.
* In addition to the `-hsmeta.json` file, create a new directory that you'll be copy/pasting your original theme folder into. You can name this folder anything you'd like, but it will need to match the name set in the `themePath` field of `-hsmeta.json`.
Your project so far should resemble the following:
```shell theme={null}
projectName/
├── hsproject.json
└── src/
└── theme/
├── theme-hsmeta.json
└── my-migrated-theme/
```
## Convert your theme files
With the skeleton of your project set up, you can now copy/paste your existing theme files into the project, then modify the theme structure to fit the CMS React requirements.
Copy the contents of your theme folder into the `src/theme/my-migrated-theme/` directory. It's recommended to copy/paste rather than move the files so that you have your original as a backup. This should result in a structure similar to the following.
```shell theme={null}
projectName/
├── hsproject.json
└── src/
└── theme/
├── theme-hsmeta.json
└── my-theme/
├── assets/
├── modules/
├── styles/
├── templates/
├── fields.json
└── theme.json
```
To make your theme compatible with the latest version of the developer platform, you'll need to make the following updates to your files and folders:
* Classic HubL (i.e., non-React) modules must be in a `hubl-modules` directory.
* HubL module files and templates (including partials) must be renamed to include `.hubl` in the file extension, except for `.json` files.
* `module.html` becomes `module.hubl.html`
* `module.css` becomes `module.hubl.css`
* `module.js` becomes `module.hubl.js`
* `template.html` becomes `template.hubl.html`
* `partial.html` becomes `partial.hubl.html`
```shell highlight={8-11, 15, 17-18} theme={null}
projectName/
├── hsproject.json
└── src/
└── theme/
├── theme-hsmeta.json
└── my-theme/
├── assets/
├── hubl-modules/
├── pricing-card.module
├── pricing-card.hubl.html
├── pricing-card.hubl.css
├── fields.json
└── meta.json
├── styles/
└── main.hubl.css
├── templates/
├── home.hubl.html
└── about.hubl.html
├── fields.json
└── theme.json
```
Because you're renaming these files, you'll also need to update any references in your code to match the new extensions. For example, if one of the theme's stylesheets includes another, you would need to update the path to use `hubl.css`:
```css theme={null}
{% include './objects/_layout.css' %} /* [!code --] */
{% include './objects/_layout.hubl.css' %} /* [!code ++] */
```
**Please note:**
Keep in mind that these renamed files will look and function the same but will exist as new assets in your account. When swapping a page to a new `.hubl.html` template, existing page content will not be impacted (i.e., live website content will stay the same). However, if you want to update that content in the editor, you'll need to manually rebuild it by adding the new module to the page and recreating the content.
In the directory that contains your `theme.json` file, add a `package.json` file to include the required dependencies: `@hubspot/cms-components` and `@hubspot/cms-dev-server`. You can use the example code below to get started:
```json theme={null}
{
"name": "cms-react-theme",
"version": "1.0.0",
"description": "CMS React theme",
"scripts": {
"start": "hs-cms-dev-server .",
"deploy": "hs project upload"
},
"type": "module",
"keywords": [],
"dependencies": {
"@hubspot/cms-components": "latest",
"react": "^18"
},
"devDependencies": {
"@hubspot/cms-dev-server": "latest"
}
}
```
With the file added, install the dependencies by navigating into the directory via the terminal, then running `npm install`.
With these changes in place, you can upload the theme to your account by running `hs project upload`. This command will build and deploy both your HubL and React assets, so you'll no longer need to use `hs upload`.
If you would like to watch for changes (i.e., upload on save), you can use the project-specific watch command: `hs project watch`. However, this is less necessary when working with CMS React because you can run the local development server to view your local changes in real-time without deploying.
## Running local development
With your theme recreated on the developer platform, you can preview your theme assets (modules and templates) locally using the CMS local development server.
To start the local development server:
* In your terminal, ensure you're in the directory that contains your `theme.json` file.
* Run `npm run start`.
* Open [http://localhost:3000](http://localhost:3000) in your browser to view the local development server dashboard.
On the page, you should see your theme's HubL modules and templates, along with high-level information about them, such as template type.
* To view more information about a module, click the **module name**, then view its details in the right sidebar.
* To view the local preview of a module or template, click **View local version** in the *Actions* column.
While viewing the local version of a module or template, local changes that you save will automatically appear in the browser without needing to refresh.
**Please note:**
The local development server will not pick up on changes made to `.json` configuration files. To view changes to those files during local development, you'll need to upload them to your account first by running `hs project upload`.
As you iterate, you can send your changes to your HubSpot account with `hs project upload` as needed.
**Please note:**
CMS assets built using projects will not be editable in the design manager. Instead, you'll continue managing and building locally using [hs project CLI commands](/docs/developer-tooling/local-development/hubspot-cli/project-commands).
## Next Steps
Continue exploring HubSpot's developer platform by checking out some of the following resources:
* [Build CMS React modules](/docs/cms/start-building/introduction/react-plus-hubl/modules)
* [Learn more about local development](/docs/cms/reference/react/local-development)
* [Create Islands for client-side interactivity](/docs/cms/reference/react/islands)
# Migrate a CMS project to version 2025.2
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/react-plus-hubl/migrations/migrate-to-2025-2
Learn how to migrate an existing project with CMS assets from version 2023.2 or 2025.1 to 2025.2 HubL theme to the projects framework to build with React.
If you have an existing project with CMS assets that you built using version `2023.2` or `2025.1`, you can migrate it to the latest version (`2025.2`) using the CLI. Being on the latest version of the developer platform will allow you to package the latest [app features](/docs/apps/developer-platform/overview#features) with your CMS assets if needed.
The method for migrating will depend on whether your project includes serverless functions:
* If your project doesn't include serverless functions, you can migrate using the `hs project migrate` command.
* If your project includes one or more serverless functions, you'll need to manually migrate the project, as the `hs project migrate` command doesn't currently support serverless functions.
## Migrate without serverless functions
Follow the steps below to migrate an existing project built on version `2023.2` or `2025.1` to the latest version (`2025.2`). Migration for projects that don't contain a serverless function can be done through the `hs project migrate` command, which will update the project and its component files to use the latest schemas and configuration details.
Before starting, it's important to note that:
* This command will permanently upgrade your project to platform version `2025.2`, and this action cannot be undone.
* The command will create an archived copy of your original theme files to ensure you have a backup.
* The command will guide you through the process, and request your input when needed for required fields.
Ensure your latest changes are included in the migration by running `hs project upload` in your local project directory. If auto-deploy is turned off for the project, run `hs project deploy`.
```shell theme={null}
hs project upload
```
With your latest changes in HubSpot:
* Run `hs project migrate` in the local project directory.
* The CLI will first check for eligible project components, then prompt you to proceed with the migration.
* The migration script will run, and you'll see your local files updating in real-time. Once completed, the terminal will display a message confirming that the migration was successful.
Before uploading your updated files to HubSpot, review your project to confirm that the correct updates have been made.
* In `hsproject.json`, the `platformVersion` should be `2025.2`.
* The `src` directory should now contain a new `*-hsmeta.json` file. The first part of the file name will match the directory containing your theme. Learn more about this file in the [project schema reference documentation](/docs/cms/start-building/introduction/react-plus-hubl/project-structure#project-schema).
* At the root of your project, you should find a new directory named `archive`, which contains a backup of your original theme files. You may want to move it elsewhere for safekeeping.
After reviewing your migrated files, run `hs project upload` to upload these changes to your account. If auto-deploy is turned off for the project, run `hs project deploy` after the upload completes.
To view your updated project in HubSpot, run `hs project open`. A browser tab will open to the project's home page, where you can view recent build and deploy logs, and review additional details for your project.
## Migrate with serverless functions
If your project includes serverless functions, the migration process will be more manual, as the `hs project migrate` command doesn't currently support serverless functions.
In your `hsproject.json` file, update the `platformVersion` number to `2025.2`.
```json hsproject.json theme={null}
{
"name":"MyCMSTheme",
"platformVersion":"2023.2", /* [!code --] */
"platformVersion":"2025.2", /* [!code ++] */
"srcDir":"src"
}
```
Platform version `2025.2` introduced an additional directory layer for CMS theme development, along with an additional configuration file in that directory.
To match the new required project structure:
* Add a `theme` directory in `src/`, then move your theme directory into it.
```html theme={null}
projectName/
├── hsproject.json
└── src/
└── theme/
└── your-original-theme-directory/
```
* Within the new `theme/` directory, create a `*-hsmeta.json` configuration file, which configures the project's theme component (not the theme itself). You can give this file any name, but it must end with `-hsmeta.json`. Copy the code below into the new file, then modify it as needed.
```json theme={null}
{
"uid": "my-theme-uid",
"type": "theme",
"config": {
"themePath": "website-theme",
"secretNames": []
}
}
```
| Field | Type | Description |
| ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid` | String | The unique identifier for the project's theme component. This is separate from your theme's display label, which is still configured in the `theme.json` file. |
| `type` | String | The type of component, which must be `theme` for CMS themes. |
| `themePath` | String | The relative path of the directory that contains the `theme.json` file (i.e., the name of the theme directory inside `src/theme/`). |
To migrate your serverless functions, you'll need to update the app and serverless function schemas, and rename the functions directory.
* In the `src/app/` folder, rename the `app.json` file to `app-hsmeta.json`. Then, update the file contents to match the new [schema](/docs/apps/developer-platform/build-apps/app-configuration#app-schema). The code block below includes tabs to compare the new schema to the old.
Note that the `uid` value must be unique within the project. For example, your app and your theme `*-hsmeta.json` files cannot have the same `uid`. This value should not be changed after uploading your project.
```json theme={null}
{
"uid": "serverless-function-app-uid",
"type":"app",
"config": {
"name": "Serverless function app",
"description": "This app runs a serverless function.",
"distribution": "private",
"auth": {
"type" : "static",
"requiredScopes": [
"crm.objects.contacts.read",
"crm.objects.contacts.write"
],
"optionalScopes": [],
"conditionallyRequiredScopes": []
},
"permittedUrls": {
"fetch": ["https://api.hubapi.com"],
"iframe": [],
"img": []
},
"support": {
"supportEmail": "support@example.com",
"documentationUrl": "https://example.com/docs",
"supportUrl": "https://example.com/support",
"supportPhone": "+18005555555"
}
}
}
```
```json theme={null}
{
"name": "Serverless function app",
"description": "This app runs a serverless function.",
"uid": "serverless-function-app",
"public": false,
"scopes": [
"crm.objects.contacts.read",
"crm.objects.contacts.write"
]
}
```
* Next, rename the `app.functions` directory to `functions`.
```html theme={null}
projectName/
├── hsproject.json
└── src/
└── theme/
└── app/
├── app-hsmeta.json
└── app.functions/
└── functions/
```
* Lastly, similar to the app schema step above, rename the `serverless.json` file to `serverless-hsmeta.json`. Then update its contents to match the new schema format, shown below.
```json theme={null}
{
"uid": "my-serverless-function",
"type": "app-function",
"config": {
"entrypoint": "app/functions/function.js",
"secretKeys": [],
"endpoint": {
"path": "fetch-quote",
"methods": ["GET"]
}
}
}
```
```json theme={null}
{
"appFunctions": {
"quote-function": {
"file": "function.js",
"secrets": [],
"endpoint": {
"path": "fetch-quote",
"method": ["GET"]
}
}
}
}
```
With your project updates in place, upload your changes to your account by running `hs project upload`. If you have auto-deploy turned off for the project, you'll need to run `hs project deploy` to then deploy it.
After the upload, run `hs project open` to open the project in HubSpot. In the top right, you'll see the version number now shows `2025.2`.
# Modules
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/react-plus-hubl/modules
Learn more about CMS React modules, including how to structure your files and use fields.
CMS React modules are similar to [traditional HubL modules](/docs/cms/start-building/building-blocks/modules/hide-modules-and-sections), in that they have fields, can be edited in the content editor, and can be used in [drag and drop areas](/docs/cms/start-building/building-blocks/drag-and-drop/overview). But the HTML for a React module is generated by a React component instead of HubL, and its fields can be generated with either JSX or JSON.
Below, learn more about building CMS React modules, including required directory structure, defining fields, and working with data.
## Directory structure
CMS React modules must be located in the `/components/modules` subdirectory of your [CMS React project](/docs/cms/start-building/introduction/react-plus-hubl/project-structure). The JavaScript file can either live within that directory or be contained within another directory, using the module name as the file/directory name.
`/components/modules/MyModule/index.jsx`
```hubl theme={null}
projectName/
└── src/
└── cms-assets/
└── my-react-assets/
└── components/
└── modules/
└── MyModule/
└── index.jsx
```
`/components/modules/MyModule.jsx`
```shell theme={null}
projectName/
└── src/
└── cms-assets/
└── my-react-assets/
└── components/
└── modules/
└── MyModule.jsx
```
The path will determine how you reference the module in HubL, as shown below.
```hubl theme={null}
// ExampleModule/index.jsx
{% module "ExampleModule"
path="@projects/your-project/js-package/components/modules/MyModule",
%}
// ExampleModule.jsx
{% module "ExampleModule"
path="@projects/your-project/js-package/components/modules/MyModule.jsx",
%}
```
Whichever path you choose, the file (i.e., either `MyModule/index.jsx` or `MyModule.jsx`) must contain the following named exports:
* `Component`: a React component to be rendered. It may contain [islands](/docs/cms/reference/react/islands).
* `meta`: a JavaScript object, equivalent to the `meta.json` file in HubL modules.
* `fields`: a JSX tree using components from `@hubspot/cms-components/fields` to define module fields or a traditional JavaScript fields object.
```shell theme={null}
// Directory Structure for module-as-directory
└── components/
└── modules/
└── ExampleModule/
├── ExampleModuleFields.jsx
├── ExampleModuleComponent.jsx
├── ExampleModuleMeta.js
└── index.jsx
```
As an example, your `index.jsx` or `ExampleModule.jsx` file exports might look similar to the following:
```jsx theme={null}
import { ModuleFields, TextField } from '@hubspot/cms-components/fields';
import footerStyles from '../../../styles/footer.module.css';
export function Component({ fieldValues }: any) {
return (
);
}
export const fields = (
);
export const meta = {
label: 'Footer Module',
};
```
Note that you may use re-exports, as shown in the example `index.jsx` file below:
```jsx theme={null}
// Note: index.js re-exports ExampleModuleFields.jsx,
// ExampleModuleMeta.js, and ExampleModuleComponent.jsx
// as named exports
export { default as Component } from "./ExampleModuleComponent.jsx";
export { fields } from "./ExampleModuleFields.jsx";
export { meta } from "./ExampleModuleMeta.js";
```
## Module fields
Module fields can be expressed as a JSX tree using field components from `@hubspot/cms-components/fields`. These are the same [module fields](/docs/cms/reference/fields/module-theme-fields) that HubL modules use. TypeScript definitions are included so that you can benefit from autocomplete and validation when defining fields. You can find all available fields in the [fields reference documentation](/docs/cms/reference/fields/module-theme-fields).
You may express field definitions as an array of JavaScript objects identical to the traditional HubL module [JSON structure](/docs/cms/reference/fields/module-theme-fields), exporting the same way as `fields`. However, using JSX syntax is recommended, as it comes with the benefits of improved readability, sharing field logic, and more.
### Building fields with JSX
By writing module fields using JSX, you'll benefit from improved readability, along with the ability to dynamically generate fields, share field logic between modules, and create custom abstractions around field definitions.
For example, below is a `FullNameField` custom field component that abstracts out a group of two or three text fields:
```jsx theme={null}
import {
ModuleFields,
TextField,
FieldGroup,
BooleanField
} from '@hubspot/cms-components/fields';
const FullNameField = ({ includeMiddleName = false }) => (
{includeMiddleName && (
)}
);
export const fields = (
{/*Using the custom field component alongside other fields*/}
);
```
Note that the root component of the `fields` export must be `ModuleFields`. Additionally, the above code makes use of `FieldGroup`, which is the component type that creates the [field groups](/docs/cms/reference/fields/overview#field-groups) you would use in HubL modules.
In the `FullNameField` React component for the module fields defined above, props will have the following shape:
```jsx theme={null}
// FullNameField module field props
{
example_field: "Placeholder text",
group_of_fields: {
child_boolean_field: true,
child_text_field: "Child Field"
},
full_name: {
given_name: "HubSpot",
family_name: "Developer",
}
}
```
Note that the default was used to fill in the value field once it was passed. This is because module values are passed from the server, so if someone changes the value of a field in the page editor, the new value will be passed to your module. But in the above case where no page-level field value is set, the server passes the default value to your props.
### Repeated field groups
In addition to `ModuleFields` and `FieldGroup`, another component type from `@hubspot/cms-components/fields` is `RepeatedFieldGroup`. It creates a field group repeater, and can be used as shown below:
```jsx theme={null}
export const fields = (
);
```
### Using field values
Field values are passed as props to the `Component` export of the module. For example, the code below would use the field structure from the previous example inside your module's component:
```jsx theme={null}
export const Component = ({ fieldValues }) => {
return (
{fieldValues.example_field}
{fieldValues.group_of_fields.child_text_field}
);
};
```
## GraphQL
Like in HubL modules, you can [bind a GraphQL data query](/docs/cms/start-building/features/data-driven-content/graphql/query-hubspot-data-using-graphql) to a React module to [fetch data](/docs/cms/start-building/introduction/react-plus-hubl/data-fetching). The GraphQL integration currently supports querying data from HubDB and custom objects.
Adding a named `query` export to a module will provide the query result to render in the component props as `dataQueryResult`. You can import and re-export a `.graphql` query file or a JavaScript expression that evaluates to a GraphQL query (e.g., with [gql-query-builder](https://www.npmjs.com/package/gql-query-builder)).
```jsx theme={null}
// index.js
import ModuleComponent from "./ModuleComponent.js";
import ModuleFields from "./ModuleFields.js";
import ModuleMeta from "./ModuleMeta.js";
import myQuery from "./myQuery.graphql";
// This component will receive the query result via `dataQueryResult`
export const Component = ModuleComponent;
export const meta = ModuleMeta;
export const fields = ModuleFields;
export const query = myQuery;
```
Accessing the data in `ModuleComponent` is shown below:
```jsx theme={null}
//ModuleComponent.jsx
export default function ModuleComponent(props) {
return (
);
}
```
Using GraphQL in this way will connect any module and subsequent downstream pages to updates to the query and upstream data. Updates to data sources referenced from the query will cause the page to prerender. Learn more about [fetching data](/docs/cms/start-building/introduction/react-plus-hubl/data-fetching).
To explore your account's GraphQL schema and build queries, you can use the [GraphiQL](http://app.hubspot.com/l/graphiql) tool in your account. Learn more about [testing and running queries using GraphiQL](/docs/cms/start-building/features/data-driven-content/graphql/query-hubspot-data-using-graphql#test-and-run-queries-interactively-using-graphiql).
## Passing HubL data
To automatically attach and pass through HubL context variables to your React modules, you can use the `hublDataTemplate` API.
First, export a string from your module via `hublDataTemplate`. In that string, set the `hublData` variable.
```jsx theme={null}
export const hublDataTemplate = `{% set hublData = "Hello from HubL!" %}`;
```
The template will accept any valid HubL, including [filters](/docs/cms/reference/hubl/filters) and [functions](/docs/cms/reference/hubl/functions) and [module field references](/docs/cms/reference/fields/module-theme-fields). The data will come through on your React module as a top level prop of `hublData`.
```jsx theme={null}
import ModuleFields from "./ModuleFields.js";
import ModuleMeta from "./ModuleMeta.js";
export function Component(props) {
return (
My total posts: {props.hublData.totalBlogPostCount}
);
}
export const meta = ModuleMeta;
export const fields = ModuleFields;
export const hublDataTemplate = `
{% set blogId = module.blog_field %}
{% set hublData = {
"totalBlogPostCount": blog_total_post_count(blogId),
"blogAllPostsUrl": blog_all_posts_url(blogId)
}
%}
`;
```
While developing modules in `cms-dev-server`, update the URL to include `/preview/` between the port number of your locally running server and the `/module/...` routes to have your local `hublDataTemplate` string resolve.
For example: `hslocal.net:3000/preview/module/...`
# CMS React overview
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/react-plus-hubl/overview
Learn how to build modules using React.
Rather than using HubL to build modules, you can use JavaScript and React instead. JavaScript modules stitch server-rendered React components into the HTML generated by HubL, and also support client-side interactivity with [islands](/docs/cms/reference/react/islands). This enables you to have more precise control over where and when JavaScript is shipped and run in the browser.
Just like HubL-based modules, CMS React modules include a set of [HubSpot-provided fields](/docs/cms/start-building/introduction/react-plus-hubl/modules#module-fields) that you'll use to configure and customize your modules. These are the same types of fields as HubL-based module fields, but with the added benefit of TypeScript definitions for autocompletion and validation.
Get started by following the [CMS React module quickstart guide](/docs/cms/start-building/introduction/react-plus-hubl/react-plus-hubl-quickstart), or check out the [fields reference documentation](/docs/cms/reference/fields/module-theme-fields) for the full list of available fields.
## Benefits of building with React vs. HubL
At a high level, building with React comes with benefits including component composability, code reuse, broader community resources, and real access to JavaScript on the server at render time.
Rendering React on the server means that there's less of a divide between your code that serves the initial page HTML and your interactive browser code. In contrast, creating complex and interactive pages with HubL can lead to:
* An increase of client-side JavaScript that slows down the page until it's all been downloaded and executed.
* Needing to replicate or maintain UI logic across HubL and JavaScript in order to have HTML that is immediately visible and interactive.
But by building with JavaScript, you can code complex interactive components that either share code or are directly rendered on the server. When paired with the [islands](https://www.patterns.dev/posts/islands-architecture/) model, you can code web experiences that have good Core Web Vital scores (LCP, FID, CLS) even as complexity increases.
Additionally, by building on top of JavaScript and the open source framework of React, you can use the wealth of tooling, third-party libraries via npm, example code, and more that are available in the broader ecosystem. For example, since JavaScript modules and partials are built on top of Vite, you'll get things like ESM, TypeScript, JSX, CSS modules, and tree-shaking out of the box.
## Local development
When developing a CMS React project, you can start a Express + Vite local development server to view your local changes in the browser. This includes being able to view local changes on live HubSpot pages and page previews. Additionally, the local development server includes a Storybook integration so that you can start a Storybook instance alongside local development.
Learn more about [local development for CMS React projects](/docs/cms/reference/react/local-development).
## Building and deploying
CMS React is built on the [developer projects framework](/docs/cms/start-building/introduction/react-plus-hubl/project-structure), which uses a local build and deploy process. A project can be uploaded for build and deploy using the `hs project upload` command. This command can be run in the root of your project, or you can include a directory path in the command.
```shell theme={null}
// In root directory
hs project upload
// Specified path
hs project upload path/to/project
```
The upload command will kick off the project build. During the build process, CMS React projects go through [build health checks](/docs/cms/reference/react/build-health-checks) to validate the project and prevent unexpected production behavior. By default, projects are configured to [auto-deploy](/docs/cms/start-building/introduction/react-plus-hubl/project-structure#building-and-deploying) after a successful build. You can turn this setting off in HubSpot after your first successful deploy. If you turn off auto-deploy, you can manually deploy your project after a successful build by running `hs project deploy`.
For more information about `hs project` CLI commands, check out the [documentation](/docs/developer-tooling/local-development/hubspot-cli/project-commands) or run `hs project --help`.
## Fetching data
There are multiple methods of fetching data into React modules depending on your use case and HubSpot account subscription.
* On the server-side, you can fetch data using GraphQL, HubL, and the `getServerSideProps` function.
* On the client-side, you can use [serverless functions](/docs/cms/start-building/features/serverless-functions/overview#serverless-functions-for-the-cms) to execute server-side code without exposing credentials on the front-end. Because CMS React uses the projects framework, you can package a private app in the project to authenticate serverless function requests, or use secrets as needed.
Learn more about [fetching data](/docs/cms/start-building/introduction/react-plus-hubl/data-fetching).
## Styling
HubSpot CMS React projects support a number of [styling methods](/docs/cms/reference/react/styling), including the following libraries:
* Tailwind
* styled-components
* styled-jsx
* CSS Modules
Beyond these libraries, you can use any CSS-in-JS library that provides a server-side rendering API and doesn't depend on a Babel plugin.
## Limitations
The following features are not currently available for React-based modules:
* Importing JavaScript modules into JavaScript partials.
* Online code editing within the design manager.
* Some HubL features, such as certain tags and filters, may not be supported in JavaScript components.
# Build and deploy with projects
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/react-plus-hubl/project-structure
Learn about building and deploying CMS content on the latest version of HubSpot's developer platform.
On HubSpot's developer platform, you can use projects to create CMS content, such as themes and modules, using the HubSpot CLI. By building on the latest version of the platform, you can build with both React and HubL to take advantage of component composability, code reuse, broader community resources, and real access to JavaScript on the server at render time.
Below, learn about how to structure your project along with steps to build and deploy to your HubSpot account.
## Prerequisites
* Because CMS React projects are developed locally, you'll need to have the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli) installed, along with Node v20.
* If you haven't worked with CMS React projects before, it's recommended to start with the [quickstart guide](/docs/cms/start-building/introduction/react-plus-hubl/react-plus-hubl-quickstart).
## Project structure
Your project structure will vary slightly based on whether your project includes a CMS theme or just React modules. Use the tabs below to view the basic project structure for theme or standalone React module development.
Below is the basic directory structure for a project with a CMS theme component on the [latest version](/docs/developer-tooling/platform/versioning) of the developer projects platform (`2025.2`).
If you're familiar with building [classic HubL themes](/docs/cms/start-building/building-blocks/overview#theme-file-structure) locally, it may be helpful to think of this structure as having two parts:
* The project-specific files and directories that enable you to build your theme on version `2025.2` of the developer platform. This includes everything from the project root folder to `src/theme/` and the `*-hsmeta.json` file.
* The theme directory itself, which contains all of your theme assets and `theme.json` configuration file. In the example structure below, this is the `my-theme` directory.
```shell theme={null}
projectName/
├── hsproject.json
└── src/
└── theme/
├── theme-hsmeta.json
└── my-theme/
├── assets/
├── components/
├── styles/
├── templates/
├── fields.json
├── package.json
└── theme.json
```
| Name | Type | Description |
| ------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `projectName` | Directory | Top-level directory that contains all other project files. This directory can have any name, but must contain `hsproject.json`. |
| `hsproject.json` | File | The project's configuration file, which must be at the root of the project. |
| `src` | Directory | The container for your project's components. This directory can have any name as long as it's specified in the `hsproject.json` configuration file at the root of the project. If your project also includes an app, the `src` folder would contain both a `theme` and `app` directory. |
| `theme` | Directory | The directory that contains your theme directory and `*-hsmeta.json` configuration file. |
| `theme-hsmeta.json` | File | The configuration file for the project's theme component. |
| `my-theme` | Directory | The directory containing the theme itself, including the `theme.json` configuration file and all theme assets (e.g., modules and templates). |
| `assets` | Directory | The directory that contains supporting static assets, such as image files. Static assets with common extensions will resolve to public URLs automatically when used in modules. Learn more in [Vite's static asset documentation](https://vitejs.dev/guide/assets.html). |
| `components` | Directory | The directory that contains the project's [islands](/docs/cms/reference/react/islands) and [modules](/docs/cms/start-building/introduction/react-plus-hubl/modules) directories. |
| `styles` | Directory | The directory containing the project's stylesheets. |
| `theme.json` | File | The theme configuration file, which includes fields such as the display label, screenshot path, version number, and more. Learn more about the [theme.json file](/docs/cms/start-building/building-blocks/overview#theme-json). |
| `package.json` | File | The file used for loading scripts and third-party dependencies. |
Below is the basic structure for a project that contains React modules, but no theme. While similar to the theme project structure, note the following differences:
* The `src/` directory contains a `cms-assets/` directory, rather than `theme/`.
* The directory containing your CMS assets must include a `cms-assets.json` folder, rather than `theme.json`.
```shell theme={null}
projectName/
├── hsproject.json
└── src/
└── cms-assets/
├── cms-assets-hsmeta.json
└── my-react-assets/
├── assets/
├── components/
├── styles/
└── cms-assets.json
```
| Name | Type | Description |
| ------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `projectName` | Directory | Top-level directory that contains all other project files. This directory can have any name, but must contain `hsproject.json`. |
| `hsproject.json` | File | The project's configuration file, which must be at the root of the project. |
| `src` | Directory | The container for your project's components. This directory can have any name as long as it's specified in the `hsproject.json` configuration file at the root of the project. If your project also includes an app, the `src` folder would contain both a `cms-assets` and `app` directory. |
| `cms-assets` | Directory | The directory that contains the `*-hsmeta.json` configuration file and a directory for your React assets. |
| `cms-assets-hsmeta.json` | File | The configuration file for the project's CMS assets component. |
| `my-react-assets` | Directory | The directory containing the `cms-assets.json` file and React modules. |
| `assets` | Directory | The directory that contains supporting static assets, such as image files. Static assets with common extensions will resolve to public URLs automatically when used in modules. Learn more in [Vite's static asset documentation](https://vitejs.dev/guide/assets.html). |
| `components` | Directory | The directory that contains the project's [islands](/docs/cms/reference/react/islands) and [modules](/docs/cms/start-building/introduction/react-plus-hubl/modules) directories. |
| `styles` | Directory | The directory containing the project's stylesheets. |
| `cms-assets.json` | File | The CMS assets configuration file, which includes fields such as the display label. |
| `package.json` | File | The file used for loading scripts and third-party dependencies. |
## Project schema
The `*-hsmeta.json` and `.json` schema requirements differ slightly based on whether your project includes a CMS theme or just React modules. Use the tabs below to view the basic project structure for theme or standalone React module development.
The top-level configuration file for your project's theme component is specified in the `theme-hsmeta.json` file in the `src/theme/` directory.
```shell theme={null}
my-project-folder/
└── src
└── theme/
└── theme-hsmeta.json/
```
This file configures the unique identifier for the theme component, along with details such as the path of the main theme folder.
```json theme={null}
{
"uid": "my-theme",
"type": "theme",
"config": {
"themePath": "my-theme",
"secretNames": []
}
}
```
| Field | Type | Description |
| -------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uid`\* | String | An internal unique identifier for the theme. Must be globally unique within the project. Can be any string up to 64 characters. Characters can be uppercase or lowercase, and can include numbers, underscores (`_`), dashes (`-`), and periods (`.`). |
| `type`\* | String | The type of component. Must match the name of the parent folder (`theme`). |
| `themePath`\* | String | The path of the directory containing the [theme.json file](/docs/cms/start-building/building-blocks/overview#theme-json) and theme assets. |
The top-level configuration file for your project's CMS assets component is specified in the `cms-assets-hsmeta.json` file within the `src/cms-assets/` directory.
```shell theme={null}
my-project-folder/
└── src
└── cms-assets/
└── cms-assets-hsmeta.json
└── my-assets/
```
This file configures the unique identifier for the project's CMS assets component, along with details such as the path of the main assets folder.
```json theme={null}
{
"uid": "my-react-module-project",
"type": "cms-assets",
"config": {
"themePath": "my-assets",
"secretNames": []
}
}
```
| Field | Type | Description |
| -------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uid`\* | String | An internal unique identifier for the theme. Must be globally unique within the project. Can be any string up to 64 characters. Characters can be uppercase or lowercase, and can include numbers, underscores (`_`), dashes (`-`), and periods (`.`). |
| `type`\* | String | The type of component. Must be `cms-assets`. |
| `themePath`\* | String | The path of the directory containing the `cms-assets.json` file and CMS assets. |
The `cms-assets.json` file should be structured as shown below.
```json theme={null}
{
"label": "react-modules-only",
"outputPath": ""
}
```
## Building and deploying
Using [project-specific CLI commands](/docs/developer-tooling/local-development/hubspot-cli/project-commands), you can build and deploy your project to HubSpot. An `hsproject.json` file must be in the root directory of your project folder for the CLI to recognize your project.
To build and deploy your project, run `hs project upload`.
```shell theme={null}
hs project upload
```
By default, projects are configured to auto-deploy successful builds. You can [turn off auto-deploy in the project's settings](#auto-deploy) in HubSpot after initial upload. Projects with auto-deploy turned off can be deployed with the `hs project deploy` command.
### View build and deploy history
In HubSpot, you can view your project's build and deploy history as well as redeploy previous builds as needed.
To view project builds and deploys:
* Navigate to the project page in HubSpot:
* To open the project page from your terminal, run `hs project open` from within the local project directory.
* Alternatively, in your HubSpot account, navigate to **Development**. Then, click the **name** of your project.
* On the *Overview* tab, you can view the most recent builds and deploys.
* Click the **Builds & Deploys** tab to view a full build and deploy history. To view more details about a particular build or deploy, click **View details** next to the build or deploy.
To turn off auto-deploy:
* Navigate to the project page in HubSpot:
* To open the project page from your terminal, run `hs project open` from within the local project directory.
* Alternatively, in your HubSpot account, navigate to **Development**. Then, click the **name** of your project.
* Click the **Settings** tab, then toggle the **Auto-deploy successful builds** switch off.
## Delete a project
To delete a project:
* Navigate to the project page in HubSpot:
* To open the project page from your terminal, run `hs project open` from within the local project directory.
* Alternatively, in your HubSpot account, navigate to **Development**. Then, click the **name** of your project.
* On the project home page, click the **Settings** tab.
* At the bottom of the tab, click **Delete**.
* In the dialog box, enter the **project name**, then click **Delete project**.
# CMS React quickstart
Source: https://developers.hubspot.com/docs/cms/start-building/introduction/react-plus-hubl/react-plus-hubl-quickstart
Get started with HubSpot CMS by building a React + HubL theme.
Build a CMS React theme locally on HubSpot's developer platform, preview local changes in your browser, and deploy to your HubSpot account to start creating web content. Unlike traditional HTML + HubL, CMS React themes support modern React components, TypeScript, and an updated suite of build tools.
Following this tutorial, you'll use the HubSpot CLI to create a theme with React components and compatible templates, which you can use to create web content with via HubSpot's visual editor. Along the way, you'll learn how to use the local development server to preview your local changes before uploading.
## Prerequisites
Before getting started, you'll need:
* A HubSpot account. It's recommended to [create a CMS Sandbox account](https://offers.hubspot.com/free-cms-developer-sandbox), where you can create test accounts for safer iteration.
* [Node.js](https://nodejs.org/en/download) 18+ installed locally. It’s recommended to use a package manager like [Homebrew](https://brew.sh/) or [nvm](https://github.com/nvm-sh/nvm) to install Node.
* Basic familiarity with React and running terminal commands.
It’s also recommended to have a basic familiarity with the following:
* HTML, HubL, and React. Some knowledge of TypeScript may also be helpful.
* Using the command line to run commands locally. When developing locally with HubSpot, you'll use the HubSpot CLI, which this guide will walk through installing.
## Create a project
* Using the terminal, navigate into the local directory where you'll be storing your project (e.g., `/Dev/Themes/newProject`).
```shell theme={null}
cd ~/Dev/Themes/newProject
```
* Run the setup script, which will:
* Generate a JavaScript-based HubSpot theme with sample components
* Install the latest version of the HubSpot CLI (if not already installed)
* Install all necessary dependencies
* Authenticate with your HubSpot account to connect it to the HubSpot CLI
```shell theme={null}
npx @hubspot/create-cms-theme@latest
```
* Follow the prompts in your terminal to proceed through the setup.
During the authentication step, the terminal will prompt you to provide the personal access key of the HubSpot account you want to upload to. You can select to either open a browser window to the personal access key page of your account, or enter the value directly.
* If you haven't created a personal access key yet, select **Open HubSpot to copy your personal access key**.
* In the browser window, ensure all permissions checkboxes are selected.
* Click **Generate personal access key**.
* In the *Personal Access Key* section, click **Show**, then **Copy**, then paste your key into the terminal.
After authentication is completed, a `~/.hscli/config.yml` file will be created in your home directory, and will look similar to the example below:
```json expandable theme={null}
defaultAccount: account-name
portals:
- name: account-name
accountId: 1234567
env: prod
authType: personalaccesskey
auth:
tokenInfo:
accessToken: >-
expiresAt: '2025-02-13T15:44:59.399Z'
accountType: STANDARD
personalAccessKey: >-
```
If you're used to using `hubspot.config.yml` files to store HubSpot CLI account authentication details, learn how to migrate to the [new global config file](/docs/developer-tooling/local-development/hubspot-cli/reference#migrate-or-merge-your-config-files).
With the setup script complete, your working directory should be structured as follows:
```shell theme={null}
projectName/
├── hsproject.json
└── src/
└── theme/
├── theme-hsmeta.json
└── my-theme/
├── assets/
├── components/
├── styles/
├── templates/
├── fields.json
├── package.json
└── theme.json
```
At a high level, the key project components to note are:
* `hsproject.json`: at the root of the project, this file configures the project and allows the CLI to recognize it as a project.
* `src/theme/`: the theme component of the project, equivalent to the `src/app/` directory for [apps developed via projects](/docs/apps/developer-platform/overview).
* `theme-hsmeta.json`: configures the project's theme component, including the `uid` and path of the directory that contains your theme assets.
* `my-theme/`: the directory that contains all CMS assets and the `theme.json` configuration file.
If you've developed [classic HubL themes](/docs/cms/start-building/building-blocks/overview#theme-file-structure) in the past, the `my-theme` directory above is what you would have worked with when building your theme locally. The rest of the file structure above that directory is part of the new projects platform.
View the [project structure documentation](/docs/cms/start-building/introduction/react-plus-hubl/project-structure) for more information.
## Start local development
With your theme created locally, you can run the local development server to preview theme assets as you iterate before uploading.
* In the terminal, navigate into the project directory that was created by the setup script.
```shell theme={null}
cd projectName
```
* Start the local development server with the `npm run start` command.
After the local development server starts, the terminal will provide a URL to open in your browser ([http://hslocal.net:3000](http://hslocal.net:3000)).
* Open the URL in the browser to view the local development server page.
At the top of the page, you can view a summary of the assets that are available to preview in your project. For this example, you'll see one module listed.
* To quickly view the details of a module, including its local path and `fields.json` code, click the **name** of the module, then review its details in the right sidebar.
* To preview the module, click **View local version**. A new browser tab will open containing a preview of the module.
With the local development server running, the local version preview will automatically update when you save code changes. As an example, make a change to the module's styling and view how it updates the module preview:
* In your local editor, open the `getting-started.module.css` file in `/src/theme/my-theme/styles`.
* Note that the `.wrapper` declaration block contains a `background-color` rule with a value of `var(--accent-color)`. This is the rule you'll need to update in order to change the module's background color.
* In the `:root` declaration block at the top of the file, add a new custom color variable of `--custom-color:#5e6ab8`.
```css theme={null}
:root {
--primary-color: #ff7a59;
--accent-color: #2d3e50;
--custom-color: #5e6ab8;
}
```
* In the `.wrapper` declaration block, update the `background-color` rule to use the new color variable.
```css theme={null}
background-color: var(--custom-color);
```
* With the module preview open, save your changes. On save, the browser will automatically refresh and display your updated styling.
## Upload to HubSpot
Upload your project by running the [project upload command](/docs/developer-tooling/local-development/hubspot-cli/project-commands#upload-to-hubspot).
```shell theme={null}
hs project upload
```
After the project builds and deploys to your account, the terminal will confirm that the project has been successfully uploaded. Note that, by default, HubSpot auto-deploys projects after a successful build. You can [turn this off in the project settings](#settings-tab) in HubSpot if you'd prefer to manually deploy with the [`hs project deploy`](/docs/developer-tooling/local-development/hubspot-cli/project-commands#deploy-to-hubspot) command.
Run the command below to open the project in HubSpot, where you can view its initial build and deploy history, project settings, and more.
```shell theme={null}
hs project open
```
The project overview page will then open in your browser.
Alternatively, in your HubSpot account, navigate to **CRM Development** in the main navigation bar of your account, then select **Projects** in the left sidebar. Then, click the **name** of the project.
* On the *Overview* tab, view an overview of the current state of the project along with recent activity.
* On the *Builds & Deploys* tab, view a full history of builds and deploys. You can click **View details** to see more information about each activity.
* On the *Settings* tab, you can manage project settings such as auto-deploy and GitHub repository linking, as well as delete the project.
To create a page using your theme:
* In your HubSpot account, navigate to **Content** > **Website Pages**.
* If you're working in a new account:
* Click **Start from scratch**, then locate the **theme** that you just uploaded and click **Set as active theme**.
* Next to the *Getting started with CMS React* template, click **Select template**.
* If you're working in an existing account with a theme already selected:
* In the upper right, click **Create**.
* In the dialog box, enter a **name** for the page, then click **Create page**.
* On the *Choose a template* page, click the **Theme** dropdown menu, then select **Change theme**.
* Locate the **theme** you just uploaded, then click **Set as active theme**.
* Next to the *Getting started with CMS React* template, click **Select template**.
You'll then be brought to the page editor where you can continue [editing the page content](https://knowledge.hubspot.com/website-pages/edit-page-content-in-a-drag-and-drop-area) before [publishing the page](https://knowledge.hubspot.com/website-and-landing-pages/create-and-customize-pages#publish-pages).
## Next steps
Having successfully uploaded the starter theme to your account, you can continue to make changes locally, then build and deploy using the `hs project upload` command. To learn more about developing on the HubSpots CMS, check out some of the following resources:
* [Managing theme settings in HubSpot](https://knowledge.hubspot.com/website-and-landing-pages/edit-your-theme-settings)
* [Understand the various building blocks of HubSpot's CMS](/docs/cms/start-building/introduction/overview)
* [Install and use the HubSpot VS Code extension](/docs/developer-tooling/local-development/vs-code-extension)
* [CLI commands for projects](/docs/developer-tooling/local-development/hubspot-cli/project-commands)
* [Get started with CMS React](/docs/cms/start-building/introduction/react-plus-hubl/overview)
# Module and Theme Field Best Practices
Source: https://developers.hubspot.com/docs/cms/best-practices/content-editing/fields
The ability to create fields and group fields poses a User Experience (UX) issue. We encourage you to use this page as a guide to providing an intuitive experience for content creators.
When incorporating fields into your module or theme, there are a few best practices to keep in mind, including:
* Grouping fields together
* Organizing fields logically across modules
* Providing a consistent styling experience with style fields
In this article, learn about some recommended best practices to create an efficient and consistent module and theme field editing experience for content creators.
## Style fields vs. content fields
You can add [style fields](/docs/cms/reference/fields/overview#style-fields) to both modules and themes, and they enable the content creator to adjust the module's appearance, such as borders or background images behind text. Style fields should not communicate meaning or be required to understand the page's content. For example, if you have a text and image module and the image is required to understand the text content, the image should be a content field rather than a style field.
When added to a module, the style options will appear in the *Styles* tab of the page editor. When added to a theme, style options will appear in the left sidebar of the theme editor.
Keep accessibility in mind when deciding between using an image or background image field. If the content creator should have the ability to add alt text, use an image field rather than a background image field.
## Organizing fields
How you organize fields plays a significant role in the content creator’s ability to quickly make changes. Below are recommended best practices for organizing fields:
* Group style fields based on the component they control rather than the stylistic element.
* Leave only the highest-level, most important fields outside of groups.
* Favor creating a component group over unnested groups. If you ever need to add functionality to your module later, you can't move your modules to a group later without manually updating all of the instances of the module on pages.
* Order field groups in the order in which the components appear based on the primary language of the majority of the content creators that will be maintaining the site. Example: English is read from left to right, top to bottom. If the users maintaining the site typically read right to left in their language, you should provide it in that order. When in doubt, base this off of the primary language of the content for your site.
Learn more about [field groups](/docs/cms/reference/fields/overview#field-groups).
### Example
Below is an example card module.
The styles panel groups the module fields into 3 groups based on components in the card that can be styled.
* Icon
* Button
* Card
When viewing the card, the icon is seen first, then the text and button. The Icon group and its styling options appear before the button group and its styling options. Therefore, the color field for the icon would be found in the *Icon* group, while the color field for the button's background color would be found within the *Button* group.
## Required fields
Required fields are fields that the content creator must set in order to display the module and publish the page.
* Only require fields to have a value if it breaks the module to not have a value.
* If you need to have a required field, provide a default value if possible.
For example, you're building an image carousel module which allows you to configure how many slides to display at the same time. A minimum value of 1 is needed, and without a value, you don't know how to display the image carousel. This is a situation where requiring a value, but setting a default value of one or two, might make sense.
## Typography
Because rich text fields provide more typographical control, it's recommended to use rich text fields over a combination of text field and font field when possible.
There may be cases where you need to be able to provide typographic styles that apply across multiple pieces of content within the same module, such as a [text field](/docs/cms/reference/fields/module-theme-fields#text) intended for headers. In that case, you may be able to make the content creator's work more efficient by providing [font](/docs/cms/reference/fields/module-theme-fields#font) and [color](/docs/cms/reference/fields/module-theme-fields#color) style fields in the text field.
When including rich text fields, it's recommended to allow the field's typographic controls to override manually added style fields. If a style field does control a rich text field, it may be worthwhile setting help text, to ensure the content creator is aware.
## Toggle vs checkbox in boolean fields
When including a [boolean field](/docs/cms/reference/fields/module-theme-fields#boolean), you can set it to display as either a toggle or a checkbox.
* Set the boolean to a toggle when the field controls a major design or layout option. For example, converting the layout of a card from vertical to a horizontal orientation.
* Set the boolean to a checkbox when the change is less drastic. For example, hiding or showing a publish date for a blog post in a featured blog posts module.
## Related articles
* [Module and theme field types](/docs/cms/reference/fields/module-theme-fields)
* [Module and theme fields overview](/docs/cms/reference/fields/overview)
* [Modules overview](/docs/cms/start-building/building-blocks/modules/hide-modules-and-sections)
* [Themes overview](/docs/cms/start-building/building-blocks/overview)
#### Tutorials
* [Getting started with modules](/docs/cms/start-building/building-blocks/modules/quickstart)
* [Getting started with themes](/docs/cms/start-building/building-blocks/themes/getting-started)
# How to provide a good experience in the page editor
Source: https://developers.hubspot.com/docs/cms/best-practices/content-editing/page-editor
How to make sure your CSS/JS displays well in the HubSpot CMS editors.
The page editor provides an inline editing experience to help content authors create and edit their content in a what you see is what you get (WYSIWYG) interface. To do this, HubSpot renders content within an iframe with a preview of the page that includes code from modules and templates as well as the HubSpot application CSS/JavaScript. Because of this extra iframe layer and HubSpot app code, sometimes CSS/JavaScript from a template or module renders unexpectedly in the editors.
Below, learn about best practices to avoid issues when your code is rendered in the context of HubSpot's editors.
## Test in the editor
It’s important to test your assets in HubSpot’s content editors before delivering them. By testing in the editor, you can identify styling conflicts to create a more seamless experience for the content creator.
It's recommended to test the following functionalities in the editor to ensure your asset works as expected:
* Text formatting using the *Style* dropdown menu as well as other toolbar options such as alignment and colors.
* Insert options in the rich text toolbar, such as embed codes, images, links, and personalization tokens.
* Inline rich text element configuration options that appear on click, such as when editing an inserted hyperlink or image.
## Be specific
Broadly speaking, you may run into issues in the content editor when your CSS or JavaScript is not specific enough. This can manifest differently depending on how the code is written. Consider the function below as an example.
```js theme={null}
$("body").click(function (event) {
$(".blog-listing-wrapper .post-listing .post-item").siblings().find(".right").removeClass("social-active");
event.stopPropagation();
});
```
This function will run when there’s a click on the body element. Because the code calls `event.stopPropagation()`, the event will not bubble up to the `document` or `window`. If there are event listeners on those elements, the code in those listeners will not run. The issue with the above code is that the click handler will run on every click, which causes problems in the inline editor because it adds click handlers to the window element via React. Instead of listening for every click, it would be more appropriate to only add the click handler when needed — for example, after a user has clicked a button to open a side menu — and then removing the handler once the click has fired.
### CSS specificity
Problems can occur in CSS when using selectors that are generic like `label`. When possible, you should instead use selectors that are specific to a portion of the webpage, such as `.hs-form label`.
Being more specific with CSS selectors allows you to pinpoint the elements you want to style without impacting other elements unintentionally. You can also take advantage of our [boilerplate CSS file](/docs/cms/start-building/building-blocks/themes/hubspot-cms-boilerplate)) to have a better sense of selectors that should be used to avoid CSS bleed issues.
### Avoid using !important tags
`!important` tags are used to make styling rules take precedence over others. While you can use an `!important` tag when styling is being overridden, it's not recommended to use this tag on bare element selectors like `label` or `input[type="text"]`.
For example, you might consider applying an `!important` tag for styling the `