Last modified: August 22, 2025
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.

Registering 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.
hubspot.extend(({ context, actions }) => (
  <Extension context={context} sendAlert={actions.addAlert} />
));
The provided arguments can then be passed to the extension component as props.
// Define the extension to be run within Hubspot
hubspot.extend(({ context, actions }) => (
  <Extension
    context={context}
    sendAlert={actions.addAlert}
  />
));
// 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.
hubspot.extend(() => <Extension />);
const Extension = () => {
  ...
};

Access context data

The context object, passed to the extension component via hubspot.extend(), contains data related to the authenticated user and HubSpot account, along with data about where the extension was loaded. It has the following fields:
FieldTypeDescription
location'crm.record.tab' | 'crm.record.sidebar' | crm.preview | helpdesk.sidebarThe UI extension’s location.
crm.objectIdNumberThe ID of the CRM record (e.g., contact ID).
crm.objectTypeIdStringThe ID of the CRM record’s object type (e.g., 0-1). See the full list of object IDs for reference.
extension.appIdNumberThe extension’s app ID.
extension.appNameStringThe name of the extension’s app.
extension.cardTitleStringThe extension’s title.
portal.idNumberThe ID of the HubSpot account.
portal.timezoneStringThe 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 for more details.
teamsArrayAn 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.emailStringThe user’s email address.
user.emailsString[]All of the user’s associated email addresses.
user.firstNameStringThe user’s first name.
user.idNumberThe user’s ID.
user.localeStringThe user’s locale.

Actions

Below are the actions that the SDK enables you to perform. Note that some UI components include a set of actions separate from the SDK actions below, such as the CRM action components.

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. ui-extension-alert-example 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`,
        variant: "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}`,
        variant: "error"
      });
    } 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>

      {error && (
        <Alert title="Error" variant="error">
          {error}
        </Alert>
      )}

      {totalUsers !== null && (
          <Text format={{ fontWeight: "demibold" }}>
            Number of users: {totalUsers}
          </Text>
      )}
</>
  );
};


PropTypeDescription
titleStringThe bolded text of the alert.
messageStringThe main alert text.
variant'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.

Fetch CRM property data

There are three ways to fetch CRM property data:
  • fetchCrmObjectProperties, which can be included in your React files to fetch 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.
fetchCrmObjectProperties 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.
hubspot.extend(({ actions }) => (
  <HelloWorld fetchProperties={actions.fetchCrmObjectProperties} />
));

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 (
    <Text>
      Hello {firstName} {lastName}
    </Text>
  );
};
You can specify individual properties or fetch all properties with an asterisk:
fetchCrmObjectProperties('*').then((properties) => console.log(properties));
The response for fetchCrmObjectProperties is formatted as:
{
  "property1Name": "property1Value",
  "property2Name": "property2Value"
}

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

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:
export type onCrmPropertiesUpdateAction = (
 properties: string[] | '*',
 callback: (
   properties: Record<string, string>,
   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.
onCrmPropertiesUpdate(['firstname', 'lastname'], (properties) =>
  console.log(properties)
);
You can subscribe to all properties by using an asterisk.
onCrmPropertiesUpdate('*', (properties) => console.log(properties));
To handle potential errors, pass the error argument to the callback.
onCrmPropertiesUpdate(['firstname','lastname'], (properties, error) => {
   if(error) {
    console.log(error.message}
   }
   else {
     console.log(properties)
   }
})

Open overlays

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.
To add either type of overlay to your extension: By default, overlays include a close button in the top right, as shown below. ui-extension-component-panel-close You can add a secondary closing mechanism by including a Button, LoadingButton, Link, Tag, or Image component within overlay that triggers the closeOverlay action in an onClick event. To use this action, you’ll need to include the actions argument in hubspot.extend(). Below are examples of a panel overlay and a modal overlay.
panel-example-gif
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>
    </>
  );
};

Open an iframe in a modal

Similar to addAlert and fetchCrmObjectProperties, you can pass openIframeModal to the extension through the actions object. This action includes a callback, which you can use to run a function when the iframe modal is closed. The callback doesn’t receive any parameters.
export type OpenIframeModalAction = (
  action: OpenIframeActionPayload,
  onClose?: () => void
) => void;
openIframeModal takes the following payload:
interface OpenIframeActionPayload {
  uri: string;
  height: number;
  width: number;
  title?: string;
  flush?: boolean;
}
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';

// Define the extension to be run within the Hubspot CRM
hubspot.extend(
  (
    { actions }
  ) => <Extension openIframe={actions.openIframeModal} />
);

// Define the Extension component, taking in openIframe as a prop
const Extension = ({ openIframe }) => {
  const handleClick = () => {
    openIframe(
      {
        uri: 'https://wikipedia.org/', // this is a relative link. Some links will be blocked since they don't allow iframing
        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>
    </>
  );
};

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 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();
  }, []);

Upload files

While there is no UI component for uploading files, there are a few ways you can upload files:

Send custom log messages for debugging

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. logger-debug-on-crm-record Using that trace ID, you can then locate the custom log messages within the private app’s logs.

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.