Create custom cards with projects (BETA)

  • Sales Hub
    • Enterprise
  • Service Hub
    • Enterprise

Customize your CRM by creating custom cards for CRM records including contacts, companies, deals, tickets, and custom object records. A custom card is a type of UI extension that can display information from external systems, organize HubSpot information, and more. You can position cards in the record's middle pane and the sidebar, and you can customize the content using components.



In the context of HubSpot projects, UI extensions are defined within private apps and are powered by serverless functions. An app must include scopes for the CRM object that a custom card will display on. For example, to display a custom card on a contact record, the app must have and crm.objects.contacts.write scopes.

Below, learn how to set up a custom card and configure its options, such as components and actions.

To view example projects that contain fully built custom cards, check out HubSpot's example extension library on GitHub.

Set up custom card files

To get started, you can follow the quickstart guide to create a custom card that retrieves and displays data from an external API on button click.

If you followed the guides to create a project and private app from scratch, start creating a custom card by adding the following to the project's app folder.

  • An custom-card.json file, which configures the card, including which types of CRM records it will display on. 
  • A serverless folder which will contain serverless function files. Within this folder, create the following files:
    • package.json: contains metadata about the serverless function. 
    • serverless.json: the serverless function config file. 
    • custom-card.js: contains the code that the serverless function runs.

Below, learn more about each file along with example code to add to each.




type  string

The type of UI extension. For CRM cards, use crm-card.

data  object

Defines the custom card and its functionalities. Includes the following fields:

title  string

The name that displays in the card on CRM records.

location  string

Where the card appears on the CRM record.

  • "": places the card in the middle pane.
  • "crm.record.sidebar": places the card in the sidebar.
fetch  string 

Defines the data that gets fetched. Contains targetFunction and objectTypes.

targetFunction  string

Defines an optional target app function to fetch data. This function will be contained in the serverless.json file of the serverless function folder.

objectTypes  array

Defines which object types will display the card, along with the HubSpot properties to send to the serverless function.

name  string  

The name of the object. For custom objects, use p_objectName, where objectName matches the name of the custom object (case sensitive).

propertiesToSend  array

The properties to make available to the serverless function. For example, when wanting to display the contact's first name, include "firstname" in the array. Properties pass through this array do not count against your API call limits.

// custom-card.json { "type": "crm-card", "data": { "title": "Example custom card", "location":"", "fetch": { "targetFunction": "custom-card", "objectTypes": [ { "name": "contacts", "propertiesToSend": ["firstname"] } ] } } }

Serverless function files

After configuring the card's JSON file, set up the serverless function files to add components and functionality to the card. As an example, the code below is taken from the Getting Started project template, which you can learn more about in the quickstart guide.


// example package.json { "name": "demo.functions", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "@hubspot/api-client": "^7.0.1", "axios": "^0.27.2" } }


// example serverless.json { "runtime": "nodejs16.x", "version": "1.0", "appFunctions": { "crm-card": { "file": "custom-card.js", "secrets": [] } } }


// For external API calls const axios = require("axios"); exports.main = async (context = {}, sendResponse) => { // Store contact firstname, configured as propertiesToSend in crm-card.json const { firstname } = context.propertiesToSend; const introMessage = { type: "alert", title: "Your first UI extension is ready!", variant: "success", body: { type: "text", format: "markdown", text: "Congratulations! You just deployed your first **HubSpot UI extension**. This example CRM card uses the [ZenQuotes public API]( to display a daily quote. It also includes custom button actions using serverless functions.", }, }; const nextSteps = [ { type: "divider", distance: "small", }, { type: "heading", text: "Next Steps", }, { type: "text", format: "markdown", text: "You can explore more [code samples]( or UI components in the [CRM card builder](, or try code samples. If you get stuck, take look at your [build or deploy logs]( or [serverless functions logs](", }, ]; try { const { data } = await axios.get(""); const quoteSections = [ { type: "tile", body: [ { type: "text", format: "markdown", text: `**Hello ${firstname}, here's your quote for the day**!`, }, { type: "text", format: "markdown", text: `_${data[0].q}_`, }, { type: "text", format: "markdown", text: `_**Author**: ${data[0].a}_`, }, ], }, { type: "button", text: "Get new quote", onClick: { type: "SERVERLESS_ACTION_HOOK", serverlessFunction: "crm-card", }, }, ]; sendResponse({ sections: [introMessage, ...quoteSections, ...nextSteps], }); } catch (error) { // "message" will create an error feedback banner when it catches an error sendResponse({ message: { type: "ERROR", body: `Error: ${error.message}`, }, sections: [introMessage], }); } };

When configuring a serverless function, note that you need to call the sendResponse function which takes in an object with two available properties:

  • sections: an array that contains component objects. In the Getting Started template, components are defined first with constants, then later inserted into the sections array. You can also build components within the sections array itself, or use a combination of both.
  • messages: an object that defines a floating success or error banner. For example, a success alert that appears on successful form submission.

Compatible objects

Custom cards can be created for any CRM objects, including standard and HubSpot-defined objects, as well as custom objects. In the card's JSON file, you'll define this within the objectTypes array. 

When building a custom card for custom objects, you'll reference the object with the following format: p_objectName, where objectName matches the name of the custom object (case sensitive). You'll also use this format when referencing custom objects in the property list and table components for cards in the middle pane.

// Object types in the CRM card JSON file "objectTypes": [ { "name": "p_Cats", "propertiesToSend": [] } ]

Card location

In the custom card's JSON file, you can configure positioning either within the middle pane or right sidebar of a record by setting the location property. To see an example, follow the quickstart guide to create an extension that displays in the middle pane.

By default, when creating a custom card for the middle pane, it will be added to the Custom tab. However, you can add it to the Overview tab by clicking Customize this tab on a record page.

customize-tab-options-selectIn addition, when customizing a tab, you can create views based on teams to control which users can view which extensions.

  • To display a custom card in the middle pane of the CRM record, set location to "". By default, the card will be added to the Custom tab, but you can add it to the Overview tab by clicking Customize this tab on a record page.


  • To display a custom card in the right sidebar of the CRM record, set location to "crm.record.sidebar".


// crm-card.json { "type": "crm-card", "data": { "title": "Custom CRM card", "location":"", "fetch": { "targetFunction": "crm-card", "objectTypes": [ { "name": "contacts", "propertiesToSend": [] } ] } } }

Display CRM property data

You can display property values from the CRM record by including those properties in the custom card's JSON file and serverless function. Accessing property data through this method does not count against your API call limits, unlike using your private app's access token to retrieve data.

To display property data in standard components: 

  • In the card's JSON file, add the properties to the propertiesToSend array (line 10).
  • Then, in the serverless function (crm-card.js), define a const to retrieve those values from the context argument (line 2).
  • Reference those properties within components in the serverless function's sections array (lines 15, 19, 23)
{ "type": "crm-card", "data": { "title": "Custom CRM card", "fetch": { "targetFunction": "crm-card", "objectTypes": [ { "name": "contacts", "propertiesToSend": ["email", "firstname", "lifecyclestage"] } ] } } } exports.main = async (context = {}, sendResponse) => { const {propertiesToSend: { email, firstname, lifecyclestage }} = context; try { sendResponse({ title: "This card retrieves key contact details.", sections: [ { "type": "descriptionList", "direction": "row", "items": [ { "label": "Email", "value": email }, { "label": "First name", "value": firstname }, { "label": "Lifecycle stage", "value": lifecyclestage } ] } ] }); } catch (error) { console.error(error); } };


Custom cards in the middle pane can also use the property list and table components to display CRM property data without having to send properties. To do so, reference the properties directly as strings.

// crm-card.js exports.main = async (context = {}, sendResponse) => { sendResponse({ sections: [ { "type":"text", "format":"markdown", "text": "A table that displays contact data." }, { type: 'crm::table', objectTypeId: '0-1', properties: ['email', 'hubspot_owner_id', 'firstname', 'lastname'], pageSize: 3 } ], }); };

You can also use GraphQL to query CRM data through the /collector/graphql endpoint. Learn more about querying CRM data using GraphQL.

For example, the serverless function on the right uses GraphQL to query the currently displaying CRM record's first name property, then displays it in a text component. Note that the card's JSON config file would also need to include "propertiesToSend": ["hs_object_id"].

Please note: to make GraphQL requests, your app must include the following scopes:

  • collector.graphql_query.execute
const hubspot = require('@hubspot/api-client'); exports.main = async (context = {}, sendResponse) => { const { propertiesToSend: { hs_object_id } } = context; const query = `query myQuery($id: String!) { CRM { contact(uniqueIdentifier: "id", uniqueIdentifierValue: $id) { firstname lastname } } }` const queryBody = { "operationName" : "myQuery", "query": query, "variables": {"id": hs_object_id } } const hubspotClient = new hubspot.Client ({ accessToken: context.secrets.PRIVATE_APP_ACCESS_TOKEN, basePath: ''}) const gqlRequest = await hubspotClient.apiRequest ({ method: 'POST', path: '/collector/graphql', body: queryBody, }) const gqlRequestResults = await gqlRequest.json(); sendResponse({ sections: [{ type: "text", format: "markdown", text: `**First Name**: ${}` }] }); };

Card components

Components are the UI elements that make up the content of the custom card, such as forms and tables. Components should be included in the sections array of your serverless function. Standard components can be used by cards in all locations, while other components can only be used by cards in the middle pane.

You can find a full list of available components in the components reference guide.

try { sendResponse({ sections: [ { "type": "text", "format": "markdown", "text": "**Markdown**" }, { "type": "button", "text": "Click me", "onClick": { "type": "SERVERLESS_ACTION_HOOK", "serverlessFunction": "crm-card" } }, ], }); } catch (error) { throw new Error(`There was an error fetching the quote': ${error.message}`); }


When a custom card fails to load, an error message will appear on the CRM record.


  • To navigate to the app's CRM card logs, click the link in the error message.
  • On the Logs tab, click the error to view more information, including the type of error, the error message, and full log details.

Learn more about debugging serverless functions.

Card actions

To enable your extension to interact with data, either externally or within HubSpot, you can add actions through components, such as a button or image, with the onClick property.


"sections": [ { "type": "text", "text": "Click the button below to retrieve data." }, { "type": "button", "text": "Primary button", "tooltip": "Hover text.", "variant":"primary", "onClick": { "type": "SERVERLESS_ACTION_HOOK", "serverlessFunction": "crm-card" } } ]

Using the UI Extensions Playground, you can view the payload of different actions types by using the Action details tab:

  • In your HubSpot account, navigate to CRM Development in the main navigation bar. 
  • In the left sidebar menu, click UI Extensions Playground.
  • In the UI Extensions Playground, click the button component to add it to the middle pane.
  • In the right pane, set the type of action. For example, SERVERLESS_ACTION_HOOK.
  • In the middle pane, click the button to trigger the event.
  • In the right pane, click the Action details tab to view the action output.


Below, learning more about the available types of actions.

Serverless action hooks

Call a serverless function included in the project. You'll reference the name of the function within the targetFunction field of the extension's JSON config file. The name of the function should match the function's name within the serverless.json file as well as the function that gets called in the JavaScript file. 

To see an example of a serverless action hook, use the quickstart guide to download and view the Getting Started project template files.

type  string (required)

The type of action. For serverless action hooks, use SERVERLESS_ACTION_HOOK.

serverlessFunction  string (required)

The name of the serverless function as declared in the project's serverless.json file.

associatedObjectProperties  array 

A list of properties on the displayed CRM record. These property values will be included in the serverless function's context.

// Button with serverless action hook { "type": "button", "text": "Submit Form", "onClick": { "type": "SERVERLESS_ACTION_HOOK", "serverlessFunction": "my-custom-function", "associatedObjectProperties": ["additional_crm_property"] } }


Open a modal dialog within an iframe to display content.

To see an example of using an iframe, check out HubSpot's extension example library on GitHub.

type  string(required)

The type of action. For iframe hooks, use IFRAME.

height  number (required)

The height of the frame.

width  number (required)

The width of the frame.

uri  string (required)

The URI of the content.

associatedObjectProperties  array

A list of properties on the displayed CRM record. Property values will be appended to the iframe uri as query parameters.

// Button with iframe action type { "type": "button", "text": "Submit Form", "onClick": { "type": "IFRAME", "width": 890, "height": 748, "uri": "", "associatedObjectProperties": ["additional_crm_property"] } }


Submits a form to the specified serverless function with a form's current state. This action type should only be used with forms. To see an example of a simple form with a submit action, check out HubSpot's extension example library on GitHub.

Using the payload object, you can retrieve the end user's submitted value. For example, to retrieve the value submitted for an input with a name of example_select_input, use payload.formState.example_select_input


type  string (required)

The type of action. For form submissions, use SUBMIT.

serverlessFunction  string (required)

The name of the serverless function to invoke, as declared in the project's serverless.json file.

// Button with Submit action type { "type": "button", "text": "Submit Form", "onClick": { "type": "SUBMIT", "serverlessFunction": "exampleFunction" } }


Events are added to the serverless context when a user's action triggers a serverless function. Events will be included in the serverless functions context under event if the serverless function was triggered by a specific user event, similar to web events.

To see an example of a project that uses a form and event, check out HubSpot's extension example library on GitHub.

To use events, you'll first set up a for submit type action within a button component that triggers a serverless function:

// Button with Submit action type { "type": "button", "text": "Submit Form", "onClick": { "type": "SUBMIT", "serverlessFunction": "exampleFunction" } }

Then, in the serverless function you can reference the event within the context argument. The example below checks for a SUBMIT event, then creates a task in Asana using the values in the submitted form:

try { const client = asana.Client.create().useAccessToken(process.env.ASANA_PAT); if (context.event && context.event.type === "SUBMIT") { await client.tasks.createTask({ name: context.event.payload.formState.taskName, notes: context.event.payload.formState.taskNotes, projects: [`${process.env.ASANA_PROJECT}`], pretty: true, }); } sendResponse({ sections, message: { type: "SUCCESS", body: "Task added to Asana!", }, }); } catch (error) { sendResponse({ message: { type: "ERROR", body: `Error: ${error.message}`, }, }); }

Success and error handling

If your extension includes an action that's triggered by user input, such as a button, you can display a notification to indicate whether the action succeeded or failed. After the action completes, you can trigger the notification by providing a message in a sendResponse call.

To see examples of success and error banners, check out HubSpot's extension example library on GitHub.

type  string (required)

The type of notification banner. Use 'SUCCESS' for a green banner and 'ERROR' for a red banner.

body  string (required)

The message displayed within the notification banner.

sendResponse({ message: { type: 'SUCCESS', body: 'Successfully executed action!' } });


For example, if your extension has a button that triggers an API call, you could specify the success and error handling within a separate serverless function (e.g., handle-response.js), then reference that serverless function within the original JavaScript file (e.g., crm-card.js)

const hubspot = require('@hubspot/api-client'); exports.main = async (context = {}, sendResponse) => { // Instantiating HubSpot node API client const hubspotClient = new hubspot.Client({ accessToken: context.secrets.PRIVATE_APP_ACCESS_TOKEN, }); try { await hubspotClient.crm.contacts.basicApi.getPage() .then((data) => { sendResponse({ message: { type: "SUCCESS", body: `Successfully executed action!` } }) }) .catch((error) => { sendResponse({ message: { type: 'ERROR', body: `There was an error: ${error}` } }) }); } catch (error) { throw new Error( `There was an error executing the serverless function: ${error.message}` ); } };exports.main = async (context = {}, sendResponse) => { try { sendResponse({ sections: [ { "type": "text", "text": "Trigger action" }, { "type": "button", "text": "Action with message", "variant": "primary", "onClick": { "type": "SERVERLESS_ACTION_HOOK", "serverlessFunction": "handle-response" } }, ] }); } catch (error) { throw new Error( `There was an error creating these cards': ${error.message}` ); } };

Was this article helpful?
This form is used for documentation feedback only. Learn how to get help with HubSpot.