Use this file to discover all available pages before exploring further.
The UI extensions SDK is the foundation for adding app card functionality, providing an assortment of methods and utilities that enable you to:
Access account, extension, and user context
Perform actions like displaying alert banners
Log custom messages for debugging
Render the UI through UI components
Below, learn more about configuring your UI extension to access context and perform actions. For documentation on the UI components included in the SDK, check out the UI components reference documentation.
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.
The provided arguments can then be passed to the extension component as props.
// Define the extension to be run within HubSpothubspot.extend(({ context, actions }) => ( <Extension context={context} sendAlert={actions.addAlert} />));// Define the Extension component, taking in context, and sendAlert as propsconst 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.
The context object contains data related to the authenticated user and HubSpot account, along with data about where the extension was loaded. 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 hook directly within your component.
Geographic identifier that denotes the region where the current portal is hosted. See HubSpot Cloud Infrastructure FAQ 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').
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.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.
import React, { useState } from "react";import { Text, Button, LoadingSpinner } from "@hubspot/ui-extensions";import { hubspot } from "@hubspot/ui-extensions";hubspot.extend(({actions}) => <Extension addAlert={actions.addAlert}/>);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 ( <> <LoadingSpinner /> <Text>Fetching user data...</Text> </> ); } return ( <> <Text> Click the button to fetch the total number of registered users from the API. </Text> <Button onClick={fetchUserData} variant="primary"> Fetch user data </Button> {totalUsers !== null && ( <Text format={{ fontWeight: "demibold" }}> Number of users: {totalUsers} </Text> )}</> );};
There are multiple ways to fetch CRM property data via the SDK:
The useCrmProperties hook, which fetches properties from the current CRM record. While similar to fetchCrmObjectProperties, it offers automatic state management, supports property formatting, and automatically updates as properties are changed without needing to refresh.
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.
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 hook instead, which fetches properties from the current CRM record. While similar to fetchCrmObjectProperties, it offers automatic state management, supports property formatting, and automatically updates as properties are changed without needing to refresh.
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.
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. This method will work for the CRM objects that you include in the extension’s .json file in the objectTypes array.
import React, { useState } from 'react';import { Divider, Button, Input, Flex, hubspot} from '@hubspot/ui-extensions';hubspot.extend(({ actions }) => ( <Extension refreshObjectProperties={actions.refreshObjectProperties} />));const Extension = ({ refreshObjectProperties,}) => {// Your extension logic goes here// Refresh all properties of the object on the page refreshObjectProperties(); } }); };return (// Your extension body)
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:
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.
To add another layer of UI to your extension, you can include overlays using the Modal and 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.
Add the Modal or Panel component into the overlay prop.
Panel
Modal
import { Button, Panel, PanelSection, PanelBody, PanelFooter, Text, hubspot } from "@hubspot/ui-extensions";hubspot.extend(({ actions }) => <OverlayExampleCard actions={actions} />);const OverlayExampleCard = ({ actions }) => { return ( <> <Button overlay={ <Panel id="my-panel" title="Example panel"> <PanelBody> <PanelSection> <Text>Welcome to my panel. Thanks for stopping by!</Text> <Text>Close the panel by clicking the X in the top right, or using the button below</Text> </PanelSection> </PanelBody> <PanelFooter> <Button variant="secondary" onClick={() => { actions.closeOverlay("my-panel"); }} > Close </Button> </PanelFooter> </Panel> } > Open panel </Button> </> );};
import { Button, Modal, ModalBody, ModalFooter, Text, hubspot } from "@hubspot/ui-extensions";hubspot.extend(({ actions }) => <OverlayExampleCard actions={actions} />);const OverlayExampleCard = ({ actions }) => { return ( <> <Button overlay={ <Modal id="default-modal" title="Example modal" width="md"> <ModalBody> <Text>Welcome to my modal. Thanks for stopping by!</Text> <Text>Close the modal by clicking the X in the top right, or using the button below</Text> </ModalBody> <ModalFooter> <Button onClick={() => actions.closeOverlay("default-modal")}>Close modal</Button> </ModalFooter> </Modal> } > Open modal </Button> </> );};
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.
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
The URL to load in the iframe.
height
Number
The height of the modal in pixels.
width
Number
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.
import { Link, Button, Text, Box, Flex, hubspot } from "@hubspot/ui-extensions";hubspot.extend(({ actions }) => <Extension openIframe={actions.openIframeModal} />);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 ( <> <Flex direction="column" align="start" gap="medium"> <Text> 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 . <Link href="https://developers.hubspot.com/docs/platform/create-ui-extensions#open-an-iframe-in-a-modal"> here </Link> </Text> <Box> <Button type="submit" onClick={handleClick}> Click me </Button> </Box> </Flex> </> );};
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.
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.
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 and follows the same requirements.This action only works after the user has interacted with the page after loading (transient activation).
import React from "react";import { Button, Flex, hubspot, TextArea } from "@hubspot/ui-extensions";hubspot.extend(({ actions }) => <Extension actions={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 ( <Flex direction="column" gap="md"> <TextArea label="Text" value={textToCopy} /> <Button onClick={handleOnClick}>Copy text</Button> </Flex> );}
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:
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(); }, []);
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.
Both approaches provide identical functionality, so the choice is a matter of preference. As a general guideline:
Hook-based approach: cleaner component APIs, no prop drilling.
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 Access context data.
For a complete list of available actions, see 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.
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 Access context data.The following example accesses the current extension location and renders it in a Text component:
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 Actions. The actions available depend on the extension point location.The following example displays an alert when a button is clicked:
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.
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.
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.
Call
Response
Example usage
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, }, }, });
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.
{ 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}
A function to refetch the latest associations data, with formatting applied as specified in the original hook call. The current page will be preserved.
// ✅ Good - Typed for better IntelliSense and type safetyconst actions = useExtensionActions<'crm.record.tab'>();const context = useExtensionContext<'crm.record.tab'>();// ❌ Avoid - Less type safety and IntelliSenseconst actions = useExtensionActions();const context = useExtensionContext();
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.The following methods are available:
logger.info
logger.debug
logger.warn
logger.error
Each method accepts a single string argument.For example, the following extension code includes few different log messages to help better identify where an error has occurred:
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 }) => <MiddleTabLogging context={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 ( <Flex direction="column" align="start" gap="small"> <Text>Test out the logger with the following buttons.</Text> <Text variant="microcopy">The browser's developer console will show your events in local dev.</Text> <Divider /> <Text>Test fetch functions</Text> <Flex gap="small" wrap="wrap"> <Button onClick={callFetchSuccess}>Fetch success ✅</Button> <Button onClick={callFetchFail}>Fetch error ❌</Button> </Flex> <Divider /> <Flex direction="column" gap="small"> <Text>Test different log levels.</Text> <Flex gap="small" wrap="wrap"> <Button onClick={() => logger.info("Logging an info!")}>logger.info()</Button> <Button onClick={() => logger.debug("Logging a debug!")}>logger.debug()</Button> <Button onClick={() => logger.warn("Logging a warning!")}>logger.warn()</Button> <Button onClick={() => logger.error("Logging an error!")}>logger.error()</Button> </Flex> </Flex> <Divider /> <Text> 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. </Text> <Button variant="destructive" onClick={() => { throw new Error("Card crashed"); }} > Crash the card </Button> </Flex> );};
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 copy.